Compare commits

...

15 Commits
0.2.0 ... main

Author SHA1 Message Date
9d7a64a371 Merge pull request 'Improve code readability #1' (#3) from readability_refactor_1 into main
Reviewed-on: #3
2024-06-16 13:59:06 +00:00
29d40969c2 Improve code readability #2
+ Improve syntax
2024-06-16 15:28:12 +02:00
68636d5e64 Improve code readability #1
+ Improved tests readability of common_utils_test.go
+ Fixed minor typos
2024-06-16 15:27:55 +02:00
8cd3ed2127 Fix install command 2024-03-24 15:40:35 +01:00
9f4d85e707 Update README.md with release information 2024-03-24 15:29:09 +01:00
806ff9b4c1 Improve tests stability 2024-03-24 15:19:47 +01:00
d7615a90ba Fix import naming 2024-03-24 15:19:47 +01:00
14dcb9a66e Update to the latest required packages 2024-03-24 15:19:44 +01:00
5445ce1ccf Restructure project directories
Follow the standard of all go projects
2024-01-13 15:39:40 +01:00
741f18efd1 Upgrade to Go 1.19 2022-12-11 15:33:53 +01:00
2d06e8e09c Add taskfile 2021-11-11 20:51:50 +01:00
19e68df732 Add a skip field to the repository configuration 2021-11-08 22:09:12 +01:00
62dadc53bf Limit number of concurrent goroutines 2021-11-08 18:45:15 +01:00
5fd9bc851b Add "echo" as logger
This allow to test ouput, and specify ouput other then os.Stdout
2021-11-07 19:59:11 +01:00
23e4547e52 Add possiblity to limit actions to tagged or named repository 2021-11-07 14:08:34 +01:00
31 changed files with 1478 additions and 703 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build
.idea

View File

@ -11,9 +11,10 @@ Create a yaml file with the given structure
workspace: ${HOME}/workspace
# List with repositories, that you want to manage with GRM
repositories:
- src: "git@github.com:Revalus/GitRepositoryManager.git"
- src: "git@github.com:Revalus/GitRepositoryManager.git" # Required - specified repository to clone/fetch data
name: GRM # Optional, Uniq - if no name is specified, the repository will be treated as a name
dest: manager # Optional, Uniq - if no value is specified, name will be taken as destination
tags: ['companyX', 'departmentY'] # Optional - tags to specify to limit/exclude actions on the repository
```
### Note
@ -24,19 +25,27 @@ By default, the config file is searched for in `[HOME_DIR]./config/grm/config.ya
### Global args
| argument | type | default | Description |
|-------------------|--------|--------------------------------------|----------------------------------------------------------------------|
| -c, --config-file | string | `[HOME_DIR]./config/grm/config.yaml` | Path to configuration file, where the repositories must be specified |
| argument | type | default | Description |
|------------------------------|----------|--------------------------------------|------------------------------------------------------------------------|
| **-c**, **--config-file** | *string* | `[HOME_DIR]./config/grm/config.yaml` | Path to configuration file, where the repositories must be specified |
| **-v**, **--version** | *bool* | `false` | Display current version |
| **--no-color** | *bool* | `false` | Turning off the display of output in color |
| **-n** **--name** | *string* | `empty` | Limit action to the specified repository name |
| **-t** **--tag** | *string* | `empty` | Limit action to the specified repository tag (may be more than one tag)|
| **max-concurrent-process** | *string* | `empty` | Determine how many tasks can run simultaneously |
### Commands
| command | Description |
|---------|-------------|
| sync | Fetches changes from repositories or pulls a repository if one does not exist.
| status | Get repository information - what is the current branch, how many commits are above and behind it for each remote.
| command | Description |
|---------|--------------------------------------------------------------------------------------------------------------------|
| sync | Fetches changes from repositories or pulls a repository if one does not exist. |
| status | Get repository information - what is the current branch, how many commits are above and behind it for each remote. |
## Changelog
- 0.3.2 3rd party security libs update
- 0.3.1 Upgrade to Go 1.19
- 0.3.0 Adding the ability to limit actions to repositories containing a given name or tags
- 0.2.0 Add status command - get information about the current status in the repository
- 0.1.1 Allow to use env vars in config
- 0.1.0 Add sync command - allow to fetch and clone repositories

26
Taskfile.yml Normal file
View File

@ -0,0 +1,26 @@
version: '3'
tasks:
pull:
cmds:
- go mod download
silent: true
test:
cmds:
- go test $(find './internal' -name "*.go" -exec dirname {} \; | uniq | grep '/') -cover
build:
dir: "cmd"
deps:
- pull
cmds:
- mkdir -p ../build
- go build -o ../build/grm
install:
dir: "build"
deps:
- build
cmds:
- cp grm ${GOPATH}/bin/grm

View File

@ -1,93 +0,0 @@
package app
import (
"os"
"gitlab.com/revalus/grm/commands"
"gitlab.com/revalus/grm/config"
)
const (
APP_NAME = "Git repository manager"
APP_DESCRIPTION = "Manage your repository with simple app"
VERSION = "0.2.0"
)
type GitRepositoryManager struct {
cliArguments config.CliArguments
configuration config.Configuration
console ConsoleOutput
}
func (g *GitRepositoryManager) Parse(args []string) {
co := ConsoleOutput{}
checkCriticalError := func(err error) {
if err != nil {
co.ErrorfMsg("%v", err.Error())
os.Exit(2)
}
}
arguments, err := config.ParseCliArguments(APP_NAME, APP_DESCRIPTION, args)
checkCriticalError(err)
configFileContent, err := getFileContent(arguments.ConfigurationFile)
checkCriticalError(err)
fileExcension, err := getFileExcension(arguments.ConfigurationFile)
checkCriticalError(err)
configuration, err := config.GetRepositoryConfig(configFileContent, fileExcension)
checkCriticalError(err)
co.Color = arguments.Color
g.console = co
g.cliArguments = arguments
g.configuration = configuration
}
func (g *GitRepositoryManager) Run() {
if g.cliArguments.Sync {
g.console.InfoFMsg("Synchronizing repositories")
println()
sync := commands.NewSynchronizer(g.configuration.Workspace)
g.runCommand(sync)
println()
g.console.InfoFMsg("All repositories are synced")
}
if g.cliArguments.Status {
status := commands.NewStatusChecker(g.configuration.Workspace)
g.runCommand(status)
}
if g.cliArguments.Version {
g.console.InfoFMsg("Current version: %v", VERSION)
}
}
func (g GitRepositoryManager) describeStatus(status commands.CommandStatus) {
if status.Error {
g.console.ErrorStatusF("Repository \"%v\": an error occurred: %v", status.Name, status.Message)
return
}
if status.Changed {
g.console.ChangedStatusF("Repository \"%v\": %v", status.Name, status.Message)
} else {
g.console.UnchangedStatusF("Repository \"%v\": %v", status.Name, status.Message)
}
}
func (g *GitRepositoryManager) runCommand(cmd commands.Command) {
statusChan := make(chan commands.CommandStatus)
for _, repo := range g.configuration.Repositories {
go cmd.Command(repo, statusChan)
}
for range g.configuration.Repositories {
g.describeStatus(<-statusChan)
}
}

View File

@ -1,94 +0,0 @@
package app
import (
"fmt"
"os"
"testing"
"gitlab.com/revalus/grm/config"
)
func prepareConfigContent() (string, string) {
checkErrorDuringPreparation := func(err error) {
if err != nil {
fmt.Printf("Cannot prepare a temporary directory for testing! %v ", err.Error())
os.Exit(2)
}
}
baseTmp := fmt.Sprintf("%v/grmTest", os.TempDir())
if _, ok := os.Stat(baseTmp); ok != nil {
err := os.Mkdir(baseTmp, 0777)
checkErrorDuringPreparation(err)
}
tempDir, err := os.MkdirTemp(baseTmp, "*")
checkErrorDuringPreparation(err)
configFilePath := fmt.Sprintf("%v/config-file.yaml", tempDir)
file, err := os.Create(configFilePath)
checkErrorDuringPreparation(err)
defer file.Close()
yamlConfig := fmt.Sprintf(`
workspace: %v
repositories:
- src: "https://github.com/golang/example.git"`, tempDir)
_, err = file.WriteString(yamlConfig)
checkErrorDuringPreparation(err)
return tempDir, configFilePath
}
func TestParseApplication(t *testing.T) {
workdir, configFile := prepareConfigContent()
t.Cleanup(func() {
os.Remove(workdir)
})
args := []string{"custom-app", "sync", "-c", configFile}
grm := GitRepositoryManager{}
grm.Parse(args)
if workdir != grm.configuration.Workspace {
t.Errorf("Expected to get %v, instead of this got %v", workdir, grm.configuration.Repositories)
}
if !grm.cliArguments.Sync {
t.Error("The value of \"sync\" is expected to be true")
}
expectedRepo := config.RepositoryConfig{
Name: "example",
Src: "https://github.com/golang/example.git",
Dest: "example",
}
if expectedRepo != grm.configuration.Repositories[0] {
t.Errorf("Expected to get %v, instead of this got %v", expectedRepo, grm.configuration.Repositories[0])
}
}
func Example_test_sync_output() {
grm := GitRepositoryManager{
configuration: config.Configuration{
Workspace: "/tmp",
},
cliArguments: config.CliArguments{
Sync: true,
Version: true,
},
console: ConsoleOutput{
Color: false,
},
}
grm.Run()
// Output:
// Info: Synchronizing repositories
// Info: All repositories are synced
// Info: Current version: 0.2.0
}

View File

@ -1,61 +0,0 @@
package app
import (
"fmt"
)
const (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorGreen = "\033[32m"
colorYellow = "\033[33m"
colorBlue = "\033[34m"
)
type ConsoleOutput struct {
Color bool
}
func (co ConsoleOutput) ErrorfMsg(format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...)
if co.Color {
msg = fmt.Sprintf("%vError:%v %v", colorRed, colorReset, msg)
} else {
msg = fmt.Sprintf("Error: %v", msg)
}
fmt.Println(msg)
}
func (co ConsoleOutput) InfoFMsg(format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...)
if co.Color {
msg = fmt.Sprintf("%vInfo:%v %v", colorBlue, colorReset, msg)
} else {
msg = fmt.Sprintf("Info: %v", msg)
}
fmt.Println(msg)
}
func (co ConsoleOutput) UnchangedStatusF(format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...)
if co.Color {
msg = fmt.Sprintf("%v%v%v", colorGreen, msg, colorReset)
}
fmt.Println(msg)
}
func (co ConsoleOutput) ChangedStatusF(format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...)
if co.Color {
msg = fmt.Sprintf("%v%v%v", colorYellow, msg, colorReset)
}
fmt.Println(msg)
}
func (co ConsoleOutput) ErrorStatusF(format string, a ...interface{}) {
msg := fmt.Sprintf(format, a...)
if co.Color {
msg = fmt.Sprintf("%v%v%v", colorRed, msg, colorReset)
}
fmt.Println(msg)
}

View File

@ -1,25 +0,0 @@
package app
import (
"errors"
"fmt"
"io/ioutil"
"strings"
)
func getFileContent(pathToFile string) ([]byte, error) {
return ioutil.ReadFile(pathToFile)
}
func getFileExcension(pathToFile string) (string, error) {
splittedFileName := strings.Split(pathToFile, ".")
if len(splittedFileName) == 1 {
msg := fmt.Sprintf("excension for file \"%v\", not found", splittedFileName)
return "", errors.New(msg)
}
fileExcension := splittedFileName[len(splittedFileName)-1]
return fileExcension, nil
}

View File

@ -1,36 +0,0 @@
package app
import "testing"
func TestGetFileExtension(t *testing.T) {
toTest := map[string]string{
"myYamlFile.yaml": "yaml",
"myTxtFile.txt": "txt",
"myJsonFile.json": "json",
}
for key, value := range toTest {
result, err := getFileExcension(key)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if result != value {
t.Errorf("Expected to get %v, instead of this got %v", value, result)
}
}
}
func TestErrorInGetExcensionFile(t *testing.T) {
result, err := getFileExcension("test")
if err == nil {
t.Errorf("Expected to get error, instead of this got result %v", result)
}
}

18
cmd/main.go Normal file
View File

@ -0,0 +1,18 @@
package main
import (
"gitlab.com/revalus/grm/internal/grm"
"os"
)
func main() {
app := grm.GitRepositoryManager{}
err := app.Parse(os.Args)
if err != nil {
os.Exit(2)
}
exitCode := app.Run(os.Stdout)
os.Exit(exitCode)
}

View File

@ -1,138 +0,0 @@
package commands
import (
"fmt"
"os"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
gitcfg "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/storage/filesystem"
"gitlab.com/revalus/grm/config"
)
type TestSetup struct {
rootFS billy.Filesystem
baseRepository struct {
fileSystem billy.Filesystem
repo *git.Repository
}
}
func checkErrorDuringPreparation(err error) {
if err != nil {
fmt.Printf("Cannot prepare a temporary directory for testing! %v ", err.Error())
os.Exit(2)
}
}
func createTmpDir() string {
baseForTMPDir := fmt.Sprintf("%v/grmTest", os.TempDir())
if _, ok := os.Stat(baseForTMPDir); ok != nil {
err := os.Mkdir(baseForTMPDir, 0777)
checkErrorDuringPreparation(err)
}
tempDir, err := os.MkdirTemp(baseForTMPDir, "*")
checkErrorDuringPreparation(err)
return tempDir
}
func getTestSetup() TestSetup {
tmpDir := createTmpDir()
baseFileSystem := osfs.New(tmpDir)
initRepositoryFileSystem, err := baseFileSystem.Chroot("worktree")
checkErrorDuringPreparation(err)
directoryForGitMetadata, err := initRepositoryFileSystem.Chroot(".git")
checkErrorDuringPreparation(err)
repository, err := git.Init(filesystem.NewStorage(directoryForGitMetadata, cache.NewObjectLRUDefault()), initRepositoryFileSystem)
checkErrorDuringPreparation(err)
fileForFirstCommit, err := initRepositoryFileSystem.Create("TestFile.txt")
checkErrorDuringPreparation(err)
_, err = fileForFirstCommit.Write([]byte("foo-conent"))
checkErrorDuringPreparation(err)
repositoryWorkTree, err := repository.Worktree()
checkErrorDuringPreparation(err)
repositoryWorkTree.Add(fileForFirstCommit.Name())
_, err = repositoryWorkTree.Commit("First commit", &git.CommitOptions{})
checkErrorDuringPreparation(err)
return TestSetup{
baseRepository: struct {
fileSystem billy.Filesystem
repo *git.Repository
}{
fileSystem: initRepositoryFileSystem,
repo: repository,
},
rootFS: baseFileSystem,
}
}
func makeCommit(wk *git.Worktree, commitMessage string) {
_, err := wk.Commit(commitMessage, &git.CommitOptions{})
checkErrorDuringPreparation(err)
}
func getFSForLocalRepo(dirName string, baseFileSystem billy.Filesystem) (billy.Filesystem, *filesystem.Storage) {
fsForLocalRepo, err := baseFileSystem.Chroot(dirName)
checkErrorDuringPreparation(err)
fsForMetadata, err := fsForLocalRepo.Chroot(".git")
checkErrorDuringPreparation(err)
storageForTestRepo := filesystem.NewStorage(fsForMetadata, cache.NewObjectLRUDefault())
return fsForLocalRepo, storageForTestRepo
}
func getBaseForTestingSyncCommand() (StatusChecker, *git.Repository, config.RepositoryConfig, TestSetup) {
tmpDirWithInitialRepository := getTestSetup()
dirNameForLocalRepository := "testRepo"
fsForLocalRepo, storageForTestRepo := getFSForLocalRepo(dirNameForLocalRepository, tmpDirWithInitialRepository.rootFS)
fakeLocalRepository, err := git.Clone(storageForTestRepo, fsForLocalRepo, &git.CloneOptions{
URL: tmpDirWithInitialRepository.baseRepository.fileSystem.Root(),
})
checkErrorDuringPreparation(err)
sc := StatusChecker{
workspace: tmpDirWithInitialRepository.rootFS.Root(),
}
repoCfg := config.RepositoryConfig{
Name: "test",
Src: tmpDirWithInitialRepository.baseRepository.fileSystem.Root(),
Dest: dirNameForLocalRepository,
}
return sc, fakeLocalRepository, repoCfg, tmpDirWithInitialRepository
}
func getBaseForTestingSyncMultipleRemote() (StatusChecker, *git.Repository, config.RepositoryConfig) {
sc, fakeLocalRepository, repoCfg, tmpDirWithInitialRepository := getBaseForTestingSyncCommand()
fakeLocalRepository.CreateRemote(&gitcfg.RemoteConfig{
Name: "subremote",
URLs: []string{tmpDirWithInitialRepository.baseRepository.fileSystem.Root()},
})
fakeLocalRepository.Fetch(&git.FetchOptions{
RemoteName: "subremote",
})
return sc, fakeLocalRepository, repoCfg
}

41
go.mod
View File

@ -1,30 +1,33 @@
module gitlab.com/revalus/grm
go 1.17
go 1.19
require (
github.com/akamensky/argparse v1.3.1
github.com/go-git/go-billy/v5 v5.3.1
github.com/go-git/go-git/v5 v5.4.2
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
github.com/akamensky/argparse v1.4.0
github.com/go-git/go-billy/v5 v5.5.0
github.com/go-git/go-git/v5 v5.11.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20210920160938-87db9fbc61c7 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/imdario/mergo v0.3.12 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.1.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/xanzy/ssh-agent v0.3.1 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/net v0.0.0-20211101193420-4a448f8816b3 // indirect
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c // indirect
github.com/skeema/knownhosts v1.2.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/tools v0.13.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

180
go.sum
View File

@ -1,124 +1,132 @@
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/ProtonMail/go-crypto v0.0.0-20210920160938-87db9fbc61c7 h1:DSqTh6nEes/uO8BlNcGk8PzZsxY2sN9ZL//veWBdTRI=
github.com/ProtonMail/go-crypto v0.0.0-20210920160938-87db9fbc61c7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/akamensky/argparse v1.3.1 h1:kP6+OyvR0fuBH6UhbE6yh/nskrDEIQgEA1SUXDPjx4g=
github.com/akamensky/argparse v1.3.1/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc=
github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34=
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8=
github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kevinburke/ssh_config v1.1.0 h1:pH/t1WS9NzT8go394IqZeJTMHVm6Cr6ZJ6AQ+mdNo/o=
github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo=
github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/net v0.0.0-20211101193420-4a448f8816b3 h1:VrJZAjbekhoRn7n5FBujY31gboH+iB3pdLxn3gE9FjU=
golang.org/x/net v0.0.0-20211101193420-4a448f8816b3/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c h1:QOfDMdrf/UwlVR0UBq2Mpr58UzNtvgJRXA4BgPfFACs=
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,9 +1,11 @@
package commands
import "gitlab.com/revalus/grm/config"
import (
"gitlab.com/revalus/grm/internal/config"
)
type Command interface {
Command(repoCfg config.RepositoryConfig, cmdStatus chan CommandStatus)
Command(repoCfg config.RepositoryConfig) CommandStatus
}
type CommandStatus struct {
Name string

View File

@ -0,0 +1,118 @@
package commands
import (
"fmt"
"os"
"path"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
gitcfg "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/storage/filesystem"
)
type TestSetupPaths struct {
baseTestDirectory string
baseTestRepository string
}
func checkErrorDuringPreparation(err error) {
if err != nil {
fmt.Printf("Cannot prepare a temporary directory for testing! %v ", err.Error())
os.Exit(2)
}
}
func createRepositoryForTest() string {
systemTMPDirectoryWithTestPath := fmt.Sprintf("%v/grmTest", os.TempDir())
if _, ok := os.Stat(systemTMPDirectoryWithTestPath); ok != nil {
err := os.Mkdir(systemTMPDirectoryWithTestPath, 0777)
checkErrorDuringPreparation(err)
}
temporaryDirPath, err := os.MkdirTemp(systemTMPDirectoryWithTestPath, "*")
checkErrorDuringPreparation(err)
return temporaryDirPath
}
// prepareRepositoryDirectories - prepare directories for file (rootRepositoryDirectory) and git metadata (gitMetadataDirectory)
func prepareRepositoryDirectories(dirName string) (billy.Filesystem, billy.Filesystem) {
rootRepositoryDirectory := osfs.New(dirName)
gitMetadataDirectory, err := rootRepositoryDirectory.Chroot(".git")
checkErrorDuringPreparation(err)
return rootRepositoryDirectory, gitMetadataDirectory
}
func prepareBasicRepository() TestSetupPaths {
temporaryDirPath := createRepositoryForTest()
// Create an interface of abstraction over filesystem to provide tests over multiple systems
// baseTestsDirectory - provides to main directory where new directories might be created
baseTestsDirectory := osfs.New(temporaryDirPath)
rootRepositoryDirectory, gitMetadataDirectory := prepareRepositoryDirectories(baseTestsDirectory.Root() + "/base_repository")
repository, err := git.Init(filesystem.NewStorage(
gitMetadataDirectory,
cache.NewObjectLRUDefault()),
rootRepositoryDirectory,
)
checkErrorDuringPreparation(err)
testFile, err := rootRepositoryDirectory.Create("TestFile.txt")
checkErrorDuringPreparation(err)
_, err = testFile.Write([]byte("foo-conent"))
checkErrorDuringPreparation(err)
repositoryWorkTree, err := repository.Worktree()
checkErrorDuringPreparation(err)
_, err = repositoryWorkTree.Add(testFile.Name())
checkErrorDuringPreparation(err)
_, err = repositoryWorkTree.Commit("First commit", &git.CommitOptions{})
checkErrorDuringPreparation(err)
return TestSetupPaths{
baseTestDirectory: baseTestsDirectory.Root(),
baseTestRepository: rootRepositoryDirectory.Root(),
}
}
func makeCommit(wk *git.Worktree, commitMessage string) {
_, err := wk.Commit(commitMessage, &git.CommitOptions{})
checkErrorDuringPreparation(err)
}
// createAndCloneRepository - create sub-repository with cloned base repository required to verify sync command
func createAndCloneRepository(repositoryName string, paths TestSetupPaths) *git.Repository {
baseGitRepository, gitMetadataDirectory := prepareRepositoryDirectories(
path.Join(paths.baseTestDirectory, repositoryName),
)
storageForSubRepository := filesystem.NewStorage(gitMetadataDirectory, cache.NewObjectLRUDefault())
fakeLocalRepository, err := git.Clone(storageForSubRepository, baseGitRepository, &git.CloneOptions{
URL: paths.baseTestRepository,
})
checkErrorDuringPreparation(err)
return fakeLocalRepository
}
func addLocalRepositoryAsAFakeRemoteRepository(repository *git.Repository, baseTestRepositoryPath string) error {
_, err := repository.CreateRemote(&gitcfg.RemoteConfig{
Name: "subremote",
URLs: []string{baseTestRepositoryPath},
})
if err != nil {
return err
}
return repository.Fetch(&git.FetchOptions{
RemoteName: "subremote",
})
}

View File

@ -2,11 +2,12 @@ package commands
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"gitlab.com/revalus/grm/config"
"gitlab.com/revalus/grm/internal/config"
"path"
"sort"
)
type StatusChecker struct {
@ -20,7 +21,7 @@ func NewStatusChecker(workspace string) StatusChecker {
}
func findNumberOfCommitDiffs(srcCommit *object.Commit, dstCommit *object.Commit) int {
// This function is a helper function to get only five latest items, based on given commit
getFiveElementsFromHashes := func(commit *object.Commit, hashedSlice *[]string) *object.Commit {
var err error
@ -36,6 +37,7 @@ func findNumberOfCommitDiffs(srcCommit *object.Commit, dstCommit *object.Commit)
return commit
}
// Compare diff between sources by hash list (the same hash list must be present to assume the end of changes)
getRangeDiff := func(listFist, listSecond []string) (int, bool) {
diffRange := 0
@ -52,8 +54,10 @@ func findNumberOfCommitDiffs(srcCommit *object.Commit, dstCommit *object.Commit)
return diffRange, false
}
baseCommitHashes := []string{}
destCommitHashes := []string{}
var baseCommitHashes []string
var destCommitHashes []string
// Try to find all differences, limit only to five last changes to avoid reading whole repository at once
for {
if srcCommit != nil {
@ -71,7 +75,7 @@ func findNumberOfCommitDiffs(srcCommit *object.Commit, dstCommit *object.Commit)
}
}
func (sc StatusChecker) Command(repoCfg config.RepositoryConfig, status chan CommandStatus) {
func (sc StatusChecker) Command(repoCfg config.RepositoryConfig) CommandStatus {
cmdStatus := CommandStatus{
Name: repoCfg.Name,
@ -80,38 +84,34 @@ func (sc StatusChecker) Command(repoCfg config.RepositoryConfig, status chan Com
Error: false,
}
destPath := fmt.Sprintf("%v/%v", sc.workspace, repoCfg.Dest)
repo, err := git.PlainOpen(destPath)
repositoryPath := path.Join(sc.workspace, repoCfg.Dest)
repo, err := git.PlainOpen(repositoryPath)
if err != nil {
cmdStatus.Error = true
cmdStatus.Message = err.Error()
status <- cmdStatus
return
return cmdStatus
}
headReference, err := repo.Head()
if err != nil {
cmdStatus.Error = true
cmdStatus.Message = err.Error()
status <- cmdStatus
return
return cmdStatus
}
remotes, err := repo.Remotes()
if err != nil || len(remotes) == 0 {
cmdStatus.Error = true
cmdStatus.Message = "cannot find remote branches"
status <- cmdStatus
return
return cmdStatus
}
currentBranchCommit, err := repo.CommitObject(headReference.Hash())
if err != nil {
cmdStatus.Error = true
cmdStatus.Message = err.Error()
status <- cmdStatus
return
return cmdStatus
}
type remoteStatus struct {
@ -120,14 +120,14 @@ func (sc StatusChecker) Command(repoCfg config.RepositoryConfig, status chan Com
err error
}
remotesStatus := make(map[string]remoteStatus)
var remoteNames []string
remoteStatues := make(map[string]remoteStatus)
for _, remote := range remotes {
remoteName := remote.Config().Name
remoteRevision, err := repo.ResolveRevision(plumbing.Revision(fmt.Sprintf("%v/%v", remoteName, headReference.Name().Short())))
if err != nil {
remotesStatus[remoteName] = remoteStatus{
remoteStatues[remoteName] = remoteStatus{
err: err,
}
continue
@ -135,7 +135,7 @@ func (sc StatusChecker) Command(repoCfg config.RepositoryConfig, status chan Com
remoteBranchCommit, err := repo.CommitObject(*remoteRevision)
if err != nil {
remotesStatus[remoteName] = remoteStatus{
remoteStatues[remoteName] = remoteStatus{
err: err,
}
continue
@ -148,11 +148,14 @@ func (sc StatusChecker) Command(repoCfg config.RepositoryConfig, status chan Com
if status.ahead > 0 || status.behind > 0 {
cmdStatus.Changed = true
}
remotesStatus[remoteName] = status
remoteNames = append(remoteNames, remoteName)
remoteStatues[remoteName] = status
}
sort.Strings(remoteNames)
cmdStatus.Message = fmt.Sprintf("branch %v", headReference.Name().Short())
for remoteName, status := range remotesStatus {
for _, remoteName := range remoteNames {
status := remoteStatues[remoteName]
if status.err != nil {
cmdStatus.Message = fmt.Sprintf("%v - ( | %v | problem: %v )", cmdStatus.Message, remoteName, status.err.Error())
continue
@ -160,5 +163,5 @@ func (sc StatusChecker) Command(repoCfg config.RepositoryConfig, status chan Com
cmdStatus.Message = fmt.Sprintf("%v - ( | %v | \u2191%v \u2193%v )", cmdStatus.Message, remoteName, status.ahead, status.behind)
}
status <- cmdStatus
return cmdStatus
}

View File

@ -2,20 +2,23 @@ package commands
import (
"fmt"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/storage/filesystem"
"gitlab.com/revalus/grm/internal/config"
"path"
"testing"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/storage/memory"
"gitlab.com/revalus/grm/config"
)
func TestIfBranchesAreEqual(t *testing.T) {
tmpDirWithInitialRepository := getTestSetup()
pathsToTest := prepareBasicRepository()
fakeLocalRepo, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
URL: tmpDirWithInitialRepository.baseRepository.fileSystem.Root(),
URL: pathsToTest.baseTestRepository,
})
checkErrorDuringPreparation(err)
@ -42,9 +45,9 @@ func TestIfBranchesAreEqual(t *testing.T) {
func TestIfCurrentBranchIsDifferent(t *testing.T) {
tmpDirWithInitialRepository := getTestSetup()
pathsToTest := prepareBasicRepository()
fakeLocalRepo, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
URL: tmpDirWithInitialRepository.baseRepository.fileSystem.Root(),
URL: pathsToTest.baseTestRepository,
})
checkErrorDuringPreparation(err)
@ -85,33 +88,24 @@ func TestIfCurrentBranchIsDifferent(t *testing.T) {
result = findNumberOfCommitDiffs(currentBranchCommit, remoteBranchCommit)
if result != 15 {
t.Errorf("Expected to get 5 changes, instead of this got %v", result)
t.Errorf("Expected to get 15 changes, instead of this got %v", result)
}
}
func TestCommandRepositoryDoesNotExists(t *testing.T) {
tmpDirWithInitialRepository := getTestSetup()
fsForLocalRepo, storageForTestRepo := getFSForLocalRepo("noMatterValue", tmpDirWithInitialRepository.rootFS)
_, err := git.Clone(storageForTestRepo, fsForLocalRepo, &git.CloneOptions{
URL: tmpDirWithInitialRepository.baseRepository.fileSystem.Root(),
})
checkErrorDuringPreparation(err)
pathsToTest := prepareBasicRepository()
sc := StatusChecker{
workspace: tmpDirWithInitialRepository.rootFS.Root(),
workspace: pathsToTest.baseTestDirectory,
}
repoCfg := config.RepositoryConfig{
Name: "test",
Src: tmpDirWithInitialRepository.baseRepository.fileSystem.Root(),
Dest: tmpDirWithInitialRepository.rootFS.Root(),
Src: pathsToTest.baseTestRepository,
Dest: pathsToTest.baseTestDirectory,
}
ch := make(chan CommandStatus)
go sc.Command(repoCfg, ch)
repoStatus := <-ch
repoStatus := sc.Command(repoCfg)
expectedMessage := "repository does not exist"
if !repoStatus.Error {
@ -127,12 +121,15 @@ func TestCommandRepositoryDoesNotExists(t *testing.T) {
func TestCommandRepositoryNoRemoteBranch(t *testing.T) {
tmpDirWithInitialRepository := getTestSetup()
dirNameForLocalRepository := "testRepo"
fsForLocalRepo, storageForTestRepo := getFSForLocalRepo(dirNameForLocalRepository, tmpDirWithInitialRepository.rootFS)
pathsToTest := prepareBasicRepository()
dirNameForLocalRepository := "sub-repository"
fsForLocalRepo, gitMetadataDirectory := prepareRepositoryDirectories(
path.Join(pathsToTest.baseTestDirectory, dirNameForLocalRepository),
)
storageForTestRepo := filesystem.NewStorage(gitMetadataDirectory, cache.NewObjectLRUDefault())
fakeLocalRepository, err := git.Clone(storageForTestRepo, fsForLocalRepo, &git.CloneOptions{
URL: tmpDirWithInitialRepository.baseRepository.fileSystem.Root(),
URL: pathsToTest.baseTestRepository,
})
checkErrorDuringPreparation(err)
@ -140,19 +137,16 @@ func TestCommandRepositoryNoRemoteBranch(t *testing.T) {
checkErrorDuringPreparation(err)
sc := StatusChecker{
workspace: tmpDirWithInitialRepository.rootFS.Root(),
workspace: pathsToTest.baseTestDirectory,
}
repoCfg := config.RepositoryConfig{
Name: "test",
Src: tmpDirWithInitialRepository.baseRepository.fileSystem.Root(),
Src: pathsToTest.baseTestRepository,
Dest: dirNameForLocalRepository,
}
ch := make(chan CommandStatus)
go sc.Command(repoCfg, ch)
repoStatus := <-ch
repoStatus := sc.Command(repoCfg)
expectedMessage := "cannot find remote branches"
if !repoStatus.Error {
@ -169,11 +163,23 @@ func TestCommandRepositoryNoRemoteBranch(t *testing.T) {
func TestCommandAllCorrectWithoutChanges(t *testing.T) {
sc, _, repoCfg, _ := getBaseForTestingSyncCommand()
pathsToTest := prepareBasicRepository()
subRepositoryDirectoryName := "sub-repository"
ch := make(chan CommandStatus)
go sc.Command(repoCfg, ch)
repoStatus := <-ch
// Get new empty repository to compare with base repository
createAndCloneRepository(subRepositoryDirectoryName, pathsToTest)
sc := StatusChecker{
workspace: pathsToTest.baseTestDirectory,
}
repoCfg := config.RepositoryConfig{
Name: "test",
Src: pathsToTest.baseTestRepository,
Dest: subRepositoryDirectoryName,
}
repoStatus := sc.Command(repoCfg)
expectedMessage := "branch master - ( | origin | \u21910 \u21930 )"
if repoStatus.Error {
@ -189,17 +195,26 @@ func TestCommandAllCorrectWithoutChanges(t *testing.T) {
}
func TestCommandAllCorrectWithOneChange(t *testing.T) {
sc, fakeLocalRepository, repoCfg, _ := getBaseForTestingSyncCommand()
pathsToTest := prepareBasicRepository()
subRepositoryDirectoryName := "sub-repository"
fakeLocalRepository := createAndCloneRepository(subRepositoryDirectoryName, pathsToTest)
ch := make(chan CommandStatus)
sc := StatusChecker{
workspace: pathsToTest.baseTestDirectory,
}
repoCfg := config.RepositoryConfig{
Name: "test",
Src: pathsToTest.baseTestRepository,
Dest: subRepositoryDirectoryName,
}
fakeLocalWorkTree, err := fakeLocalRepository.Worktree()
checkErrorDuringPreparation(err)
makeCommit(fakeLocalWorkTree, "commit 1")
go sc.Command(repoCfg, ch)
repoStatus := <-ch
repoStatus := sc.Command(repoCfg)
expectedMessage := "branch master - ( | origin | \u21911 \u21930 )"
if repoStatus.Message != expectedMessage {
@ -217,12 +232,27 @@ func TestCommandAllCorrectWithOneChange(t *testing.T) {
}
}
func TestCommandMultiRemoteNoChanges(t *testing.T) {
func TestCommandMultiRemotesNoChanges(t *testing.T) {
pathsToTest := prepareBasicRepository()
subRepositoryDirectoryName := "sub-repository"
fakeLocalRepository := createAndCloneRepository(subRepositoryDirectoryName, pathsToTest)
sc, _, repoCfg := getBaseForTestingSyncMultipleRemote()
ch := make(chan CommandStatus)
go sc.Command(repoCfg, ch)
repoStatus := <-ch
sc := StatusChecker{
workspace: pathsToTest.baseTestDirectory,
}
repoCfg := config.RepositoryConfig{
Name: "test",
Src: pathsToTest.baseTestRepository,
Dest: subRepositoryDirectoryName,
}
err := addLocalRepositoryAsAFakeRemoteRepository(fakeLocalRepository, pathsToTest.baseTestRepository)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
repoStatus := sc.Command(repoCfg)
expectedMessage := "branch master - ( | origin | \u21910 \u21930 ) - ( | subremote | \u21910 \u21930 )"
if repoStatus.Error {
@ -237,8 +267,25 @@ func TestCommandMultiRemoteNoChanges(t *testing.T) {
}
}
func TestCommandMultiRemoteWithOneChange(t *testing.T) {
sc, fakeLocalRepository, repoCfg := getBaseForTestingSyncMultipleRemote()
func TestCommandMultiRemotesWithOneChange(t *testing.T) {
pathsToTest := prepareBasicRepository()
subRepositoryDirectoryName := "sub-repository"
fakeLocalRepository := createAndCloneRepository(subRepositoryDirectoryName, pathsToTest)
sc := StatusChecker{
workspace: pathsToTest.baseTestDirectory,
}
repoCfg := config.RepositoryConfig{
Name: "test",
Src: pathsToTest.baseTestRepository,
Dest: subRepositoryDirectoryName,
}
err := addLocalRepositoryAsAFakeRemoteRepository(fakeLocalRepository, pathsToTest.baseTestRepository)
if err != nil {
t.Errorf("Unexpected error %v", err)
}
fakeLocalWorkTree, err := fakeLocalRepository.Worktree()
checkErrorDuringPreparation(err)
@ -246,9 +293,7 @@ func TestCommandMultiRemoteWithOneChange(t *testing.T) {
makeCommit(fakeLocalWorkTree, "commit 1")
checkErrorDuringPreparation(err)
ch := make(chan CommandStatus)
go sc.Command(repoCfg, ch)
repoStatus := <-ch
repoStatus := sc.Command(repoCfg)
expectedMessage := "branch master - ( | origin | \u21911 \u21930 ) - ( | subremote | \u21911 \u21930 )"
if repoStatus.Error {

View File

@ -1,10 +1,11 @@
package commands
import (
"errors"
"fmt"
"gitlab.com/revalus/grm/internal/config"
"github.com/go-git/go-git/v5"
"gitlab.com/revalus/grm/config"
)
type Synchronizer struct {
@ -26,15 +27,14 @@ const (
func fetchRepository(repo *git.Repository) (bool, error) {
err := repo.Fetch(&git.FetchOptions{})
if err == git.NoErrAlreadyUpToDate {
switch {
case errors.Is(err, git.NoErrAlreadyUpToDate):
return false, nil
case errors.Is(err, git.NoErrAlreadyUpToDate):
return false, nil
default:
return true, nil
}
if err != nil && err != git.NoErrAlreadyUpToDate {
return false, err
}
return true, nil
}
func cloneRepository(destPath string, repoCfg *config.RepositoryConfig) (bool, error) {
@ -49,7 +49,7 @@ func cloneRepository(destPath string, repoCfg *config.RepositoryConfig) (bool, e
return true, nil
}
func (s Synchronizer) Command(repoCfg config.RepositoryConfig, status chan CommandStatus) {
func (s Synchronizer) Command(repoCfg config.RepositoryConfig) CommandStatus {
var err error
cmdStatus := CommandStatus{
@ -62,20 +62,22 @@ func (s Synchronizer) Command(repoCfg config.RepositoryConfig, status chan Comma
destPath := fmt.Sprintf("%v/%v", s.workspace, repoCfg.Dest)
repo, err := git.PlainOpen(destPath)
if err != nil && err == git.ErrRepositoryNotExists {
cmdStatus.Changed, err = cloneRepository(destPath, &repoCfg)
cmdStatus.Message = syncCloned
} else if err == nil {
cmdStatus.Changed, err = fetchRepository(repo)
if cmdStatus.Changed {
cmdStatus.Message = syncFetched
if err != nil {
if errors.Is(err, git.ErrRepositoryNotExists) {
cmdStatus.Changed, err = cloneRepository(destPath, &repoCfg)
cmdStatus.Message = syncCloned
} else {
cmdStatus.Message = syncUpToDate
cmdStatus.Error = true
cmdStatus.Message = err.Error()
}
return cmdStatus
}
cmdStatus.Changed, err = fetchRepository(repo)
if cmdStatus.Changed {
cmdStatus.Message = syncFetched
} else {
cmdStatus.Error = true
cmdStatus.Message = err.Error()
status <- cmdStatus
cmdStatus.Message = syncUpToDate
}
if err != nil {
@ -83,6 +85,6 @@ func (s Synchronizer) Command(repoCfg config.RepositoryConfig, status chan Comma
cmdStatus.Message = err.Error()
}
status <- cmdStatus
return cmdStatus
}

View File

@ -2,10 +2,9 @@ package commands
import (
"fmt"
"gitlab.com/revalus/grm/internal/config"
"os"
"testing"
"gitlab.com/revalus/grm/config"
)
func TestSyncInit(t *testing.T) {
@ -17,31 +16,27 @@ func TestSyncInit(t *testing.T) {
func TestSyncCommand(t *testing.T) {
testSetup := getTestSetup()
pathsToTest := prepareBasicRepository()
sync := Synchronizer{
workspace: testSetup.rootFS.Root(),
workspace: pathsToTest.baseTestDirectory,
}
cfg := config.RepositoryConfig{
Src: fmt.Sprintf("file://%v", testSetup.baseRepository.fileSystem.Root()),
Src: fmt.Sprintf("file://%v", pathsToTest.baseTestRepository),
Dest: "awesome-go",
}
ch := make(chan CommandStatus)
// Pull part
go sync.Command(cfg, ch)
cloneStatus := <-ch
cloneStatus := sync.Command(cfg)
if cloneStatus.Error {
t.Errorf("Unexpected error: %v", cloneStatus.Message)
}
info, err := os.Stat(fmt.Sprintf("%v/awesome-go/.git", testSetup.rootFS.Root()))
info, err := os.Stat(fmt.Sprintf("%v/awesome-go/.git", pathsToTest.baseTestDirectory))
if err != nil {
t.Errorf("Unexpected error: %v", err.Error())
}
if !info.IsDir() {
t.Errorf("Expected that the selected path is dir")
}
@ -54,11 +49,7 @@ func TestSyncCommand(t *testing.T) {
t.Errorf("Expected to get %v, instead of this got %v", syncCloned, cloneStatus.Message)
}
// Fetch part
go sync.Command(cfg, ch)
fetchStatus := <-ch
fetchStatus := sync.Command(cfg)
if fetchStatus.Error {
t.Errorf("Unexpected error: %v", err.Error())
}

View File

@ -40,12 +40,29 @@ func ParseCliArguments(name, description string, arguments []string) (CliArgumen
Help: "Turn off color printing",
})
limitName := parser.String("n", "name", &argparse.Options{
Help: "Limit action to the specified repository name",
})
limitTags := parser.StringList("t", "tag", &argparse.Options{
Help: "Limit actions to repositories that contain specific tags",
})
limitRoutines := parser.Int("", "max-concurrent-process", &argparse.Options{
Default: 10,
Help: "Determine how many tasks can run simultaneously",
})
ignoreSkipped := parser.Flag("", "ignore-skip-flag", &argparse.Options{
Help: "Run selected command for all repositories with ignoring the skip flag",
})
if err := parser.Parse(arguments); err != nil {
return CliArguments{}, errors.New(parser.Usage("Please follow this help"))
}
if !syncCMD.Happened() && !(*version) && !statusCMD.Happened() {
return CliArguments{}, errors.New(errNoCommand)
if *limitName != "" && len(*limitTags) != 0 {
return CliArguments{}, errors.New(errNameAndTagsTogether)
}
return CliArguments{
@ -54,5 +71,9 @@ func ParseCliArguments(name, description string, arguments []string) (CliArgumen
Status: statusCMD.Happened(),
Version: *version,
Color: !(*color),
LimitToName: *limitName,
LimitToTags: *limitTags,
Routines: *limitRoutines,
IgnoreSkipped: *ignoreSkipped,
}, nil
}

View File

@ -3,6 +3,7 @@ package config
import (
"fmt"
"os"
"strings"
"testing"
)
@ -39,6 +40,22 @@ func TestParsingDefaultArguments(t *testing.T) {
func TestParsingWithoutCommand(t *testing.T) {
// First item in os.Args is appPath, this have to be mocked
fakeOSArgs := []string{"appName", "--name", "test"}
results, err := ParseCliArguments("", "", fakeOSArgs)
if err == nil {
t.Errorf("Expected error, not results: %v", results)
}
if !strings.Contains(err.Error(), "Please follow this help") {
t.Errorf("Expected the error contains \"Please follow this help\", but as a result received: \"%v\"", err.Error())
}
}
func TestParsingNothingProvided(t *testing.T) {
// First item in os.Args is appPath, this have to be mocked
fakeOSArgs := []string{"appName"}
@ -49,3 +66,19 @@ func TestParsingWithoutCommand(t *testing.T) {
}
}
func TestParsingNameAndTags(t *testing.T) {
// First item in os.Args is appPath, this have to be mocked
fakeOSArgs := []string{"appName", "status", "--tag", "example", "--name", "example"}
results, err := ParseCliArguments("", "", fakeOSArgs)
if err == nil {
t.Errorf("Expected error, not results: %v", results)
}
if err.Error() != errNameAndTagsTogether {
t.Errorf("Expected to get \"%v\", instead of this got \"%v\"", errNameAndTagsTogether, err.Error())
}
}

View File

@ -41,9 +41,9 @@ func GetRepositoryConfig(data []byte, fileExtension string) (Configuration, erro
return Configuration{}, errors.New(errorMessage)
}
if repo.Name == "" {
splittedGit := strings.Split(repo.Src, "/")
nameWithExcention := splittedGit[len(splittedGit)-1]
name := strings.Split(nameWithExcention, ".")[0]
splitGit := strings.Split(repo.Src, "/")
nameWithExtension := splitGit[len(splitGit)-1]
name := strings.Split(nameWithExtension, ".")[0]
config.Repositories[index].Name = name
}
if repo.Dest == "" {

View File

@ -1,6 +1,7 @@
package config
import (
"errors"
"fmt"
"os"
"reflect"
@ -27,6 +28,8 @@ repositories:
dest: "example/path"
name: "custom_example"
- src: https://github.com/example/example2.git
tags:
- "example"
`)
homedir, _ := os.UserHomeDir()
@ -43,6 +46,7 @@ repositories:
Name: "example2",
Src: "https://github.com/example/example2.git",
Dest: "example2",
Tags: []string{"example"},
},
},
}
@ -98,9 +102,9 @@ repositories:
`)
_, err := GetRepositoryConfig(exampleWrongYamlConfig, "yaml")
expectedError := fmt.Sprintf(errMissingSrcField, 0)
expectedError := errors.New(fmt.Sprintf(errMissingSrcField, 0))
if err.Error() != expectedError {
if errors.Is(err, expectedError) {
t.Errorf("Expected to get error with value %v, instead of this got: %v", expectedError, err.Error())
}
}
@ -124,7 +128,7 @@ repositories:
}
expectedError := getDuplicateFieldError("name", "example2", []int{1, 2})
if err.Error() != expectedError.Error() {
if errors.Is(err, expectedError) {
t.Errorf("Expected to get error with value %v, instead of this got: %v", expectedError.Error(), err.Error())
}
}
@ -148,7 +152,7 @@ repositories:
expectedError := getDuplicateFieldError("dest", "example", []int{1, 2})
if err.Error() != expectedError.Error() {
if errors.Is(err, expectedError) {
t.Errorf("Expected to get error with value \"%v\", instead of this got: \"%v\"", expectedError, err)
}
}

View File

@ -1,17 +1,16 @@
package config
import (
"errors"
"fmt"
"strconv"
"strings"
)
const (
errNoCommand = "at least one command must be specified, use help to get commands"
errNotSupportedType = "not supported configuration type"
errMissingWorkspaceField = "missing required \"workspace\" field"
errMissingSrcField = "missing required field the \"src\" in row %v"
errNameAndTagsTogether = "name and tags arguments connot be used together"
)
func getDuplicateFieldError(field string, name string, rows []int) error {
@ -21,7 +20,5 @@ func getDuplicateFieldError(field string, name string, rows []int) error {
rowsInString = append(rowsInString, strconv.Itoa(row))
}
errorMessage := fmt.Sprintf("The %v \"%v\" is duplicated in rows: %v", field, name, strings.Join(rowsInString, ","))
return errors.New(errorMessage)
return fmt.Errorf("the %v \"%v\" is duplicated in rows: %v", field, name, strings.Join(rowsInString, ","))
}

View File

@ -7,8 +7,10 @@ type Configuration struct {
type RepositoryConfig struct {
Name string `yaml:",omitempty"`
Src string `yaml:",omitempty"`
Dest string `yaml:",omitempty"`
Src string
Dest string `yaml:",omitempty"`
Tags []string `yaml:",omitempty"`
Skip bool
}
type CliArguments struct {
@ -17,4 +19,8 @@ type CliArguments struct {
Status bool
Version bool
Color bool
LimitToName string
LimitToTags []string
Routines int
IgnoreSkipped bool
}

72
internal/echo/echo.go Normal file
View File

@ -0,0 +1,72 @@
package echo
import (
"fmt"
"io"
"os"
)
const (
ColorReset = "\033[0m"
ColorRed = "\033[31m"
ColorGreen = "\033[32m"
ColorYellow = "\033[33m"
ColorBlue = "\033[34m"
)
var (
useColor bool = false
output io.Writer = os.Stdout
)
func Color(enabled bool) {
useColor = enabled
}
func Output(writer io.Writer) {
output = writer
}
func ErrorfMsg(format string, a ...interface{}) error {
msg := fmt.Sprintf(format, a...)
if useColor {
msg = fmt.Sprintf("%vError:%v %v", ColorRed, ColorReset, msg)
} else {
msg = fmt.Sprintf("Error: %v", msg)
}
return write(msg)
}
func InfoFMsg(format string, a ...interface{}) error {
msg := fmt.Sprintf(format, a...)
if useColor {
msg = fmt.Sprintf("%vInfo:%v %v", ColorBlue, ColorReset, msg)
} else {
msg = fmt.Sprintf("Info: %v", msg)
}
return write(msg)
}
func GreenMessageF(format string, a ...interface{}) error {
return writeWithColor(fmt.Sprintf(format, a...), ColorGreen)
}
func YellowMessageF(format string, a ...interface{}) error {
return writeWithColor(fmt.Sprintf(format, a...), ColorYellow)
}
func RedMessageF(format string, a ...interface{}) error {
return writeWithColor(fmt.Sprintf(format, a...), ColorRed)
}
func writeWithColor(msg string, color string) error {
if useColor {
return write(fmt.Sprintf("%v%v%v", color, msg, ColorReset))
}
return write(msg)
}
func write(msg string) error {
_, err := fmt.Fprintln(output, msg)
return err
}

151
internal/echo/echo_test.go Normal file
View File

@ -0,0 +1,151 @@
package echo
import (
"fmt"
"os"
"testing"
)
type ExpectedMessageTester struct {
expectedMessage string
}
func (emt ExpectedMessageTester) Write(p []byte) (n int, err error) {
msg := string(p)
if msg != emt.expectedMessage {
return 0, fmt.Errorf("expected to get \"%v\", instead of this got \"%v\"", msg, emt.expectedMessage)
}
return 0, nil
}
func TestOverwriteColor(t *testing.T) {
Color(false)
if useColor {
t.Error("Expected that \"useColor\" will be false")
}
Color(true)
if !useColor {
t.Error("Expected that \"useColor\" will be true")
}
}
func TestOverwriteWriter(t *testing.T) {
Output(os.Stderr)
if output != os.Stderr {
t.Error("Expected to receive addresses on os.Stderr")
}
Output(os.Stdout)
if output != os.Stdout {
t.Error("Expected to receive addresses on os.Stdout")
}
}
func TestErrorfMsgWithoutColor(t *testing.T) {
useColor = false
output = ExpectedMessageTester{
expectedMessage: "Error: test message\n",
}
err := ErrorfMsg("test message")
if err != nil {
t.Error(err)
}
}
func TestErrorfMsgWithColor(t *testing.T) {
useColor = true
output = ExpectedMessageTester{
expectedMessage: "\033[31mError:\033[0m test message\n",
}
err := ErrorfMsg("test message")
if err != nil {
t.Error(err)
}
}
func TestInfoMsgFWithoutColor(t *testing.T) {
useColor = false
output = ExpectedMessageTester{
expectedMessage: "Info: test message\n",
}
err := InfoFMsg("test message")
if err != nil {
t.Error(err)
}
}
func TestInfoMsgFWithColor(t *testing.T) {
useColor = true
output = ExpectedMessageTester{
expectedMessage: "\033[34mInfo:\033[0m test message\n",
}
err := InfoFMsg("test message")
if err != nil {
t.Error(err)
}
}
func TestGreenMessageWithoutColor(t *testing.T) {
useColor = false
output = ExpectedMessageTester{
expectedMessage: "test message\n",
}
err := GreenMessageF("test message")
if err != nil {
t.Error(err)
}
}
func TestGreenMessageWithColor(t *testing.T) {
useColor = true
output = ExpectedMessageTester{
expectedMessage: "\033[32mtest message\033[0m\n",
}
err := GreenMessageF("test message")
if err != nil {
t.Error(err)
}
}
func TestYellowMessageWithout(t *testing.T) {
useColor = false
output = ExpectedMessageTester{
expectedMessage: "test message\n",
}
err := YellowMessageF("test message")
if err != nil {
t.Error(err)
}
}
func TestYellowMessageWithColor(t *testing.T) {
useColor = true
output = ExpectedMessageTester{
expectedMessage: "\033[33mtest message\033[0m\n",
}
err := YellowMessageF("test message")
if err != nil {
t.Error(err)
}
}
func TestRedMessageWithout(t *testing.T) {
useColor = false
output = ExpectedMessageTester{
expectedMessage: "test message\n",
}
err := RedMessageF("test message")
if err != nil {
t.Error(err)
}
}
func TestRedMessageWithColor(t *testing.T) {
useColor = true
output = ExpectedMessageTester{
expectedMessage: "\033[31mtest message\033[0m\n",
}
err := RedMessageF("test message")
if err != nil {
t.Error(err)
}
}

157
internal/grm/app.go Normal file
View File

@ -0,0 +1,157 @@
package grm
import (
"errors"
"fmt"
"gitlab.com/revalus/grm/internal/commands"
"gitlab.com/revalus/grm/internal/config"
"gitlab.com/revalus/grm/internal/echo"
"io"
"sync"
)
const (
AppName = "Git repository manager"
AppDescription = "Manage your repository with simple grm"
VERSION = "0.3.2"
errNotFoundTags = "no repository was found with the specified tags"
errNotFoundName = "no repository was found with the specified name"
)
type GitRepositoryManager struct {
cliArguments config.CliArguments
configuration config.Configuration
}
func (g *GitRepositoryManager) Parse(args []string) error {
arguments, err := config.ParseCliArguments(AppName, AppDescription, args)
if err != nil {
fmt.Printf("Error: %v", err.Error())
return err
}
configFileContent, err := getFileContent(arguments.ConfigurationFile)
if err != nil {
fmt.Printf("Error: %v", err.Error())
return err
}
fileExtension, err := getFileExtension(arguments.ConfigurationFile)
if err != nil {
fmt.Printf("Error: %v", err.Error())
return err
}
configuration, err := config.GetRepositoryConfig(configFileContent, fileExtension)
if err != nil {
fmt.Printf("Error: %v", err.Error())
return err
}
g.cliArguments = arguments
g.configuration = configuration
return nil
}
func (g *GitRepositoryManager) Run(w io.Writer) int {
echo.Color(g.cliArguments.Color)
echo.Output(w)
exitCode := 0
if len(g.cliArguments.LimitToTags) != 0 {
err := g.limitRepositoriesToTags()
if err != nil {
echo.ErrorfMsg(err.Error())
return 1
}
}
if g.cliArguments.LimitToName != "" {
err := g.limitRepositoryToName()
if err != nil {
echo.ErrorfMsg(err.Error())
return 1
}
}
if g.cliArguments.Sync && exitCode == 0 {
echo.InfoFMsg("Synchronizing repositories")
sync := commands.NewSynchronizer(g.configuration.Workspace)
g.runCommand(sync)
echo.InfoFMsg("All repositories are synced")
}
if g.cliArguments.Status && exitCode == 0 {
echo.InfoFMsg("Current status of repositories")
status := commands.NewStatusChecker(g.configuration.Workspace)
g.runCommand(status)
}
if g.cliArguments.Version {
echo.InfoFMsg("Current version: %v", VERSION)
}
return exitCode
}
func describeStatus(status commands.CommandStatus) {
if status.Error {
echo.RedMessageF("Repository \"%v\": an error occurred: %v", status.Name, status.Message)
return
}
if status.Changed {
echo.YellowMessageF("Repository \"%v\": %v", status.Name, status.Message)
} else {
echo.GreenMessageF("Repository \"%v\": %v", status.Name, status.Message)
}
}
func (g *GitRepositoryManager) limitRepositoriesToTags() error {
limitedTagsTmp := []config.RepositoryConfig{}
for _, item := range g.configuration.Repositories {
if checkAnyOfItemInSlice(item.Tags, g.cliArguments.LimitToTags) {
limitedTagsTmp = append(limitedTagsTmp, item)
}
}
if len(limitedTagsTmp) == 0 {
return errors.New(errNotFoundTags)
}
g.configuration.Repositories = reverseRepositoryConfigs(limitedTagsTmp)
return nil
}
func (g *GitRepositoryManager) limitRepositoryToName() error {
for _, item := range g.configuration.Repositories {
if g.cliArguments.LimitToName == item.Name {
g.configuration.Repositories = []config.RepositoryConfig{item}
return nil
}
}
return errors.New(errNotFoundName)
}
func (g *GitRepositoryManager) runCommand(cmd commands.Command) {
routines := make(chan struct{}, g.cliArguments.Routines)
var wg sync.WaitGroup
for _, repo := range g.configuration.Repositories {
if repo.Skip && !g.cliArguments.IgnoreSkipped {
continue
}
wg.Add(1)
go func(r config.RepositoryConfig) {
defer wg.Done()
routines <- struct{}{}
describeStatus(cmd.Command(r))
<-routines
}(repo)
}
wg.Wait()
}

432
internal/grm/app_test.go Normal file
View File

@ -0,0 +1,432 @@
package grm
import (
"fmt"
"gitlab.com/revalus/grm/internal/commands"
"gitlab.com/revalus/grm/internal/config"
"gitlab.com/revalus/grm/internal/echo"
"os"
"reflect"
"testing"
)
type FakeCommandToTest struct {
triggerError bool
triggerChanged bool
}
type ExpectedMessageTester struct {
expectedMessages []string
}
func (emt ExpectedMessageTester) Write(p []byte) (n int, err error) {
msg := string(p)
if !checkIsItemInSlice(msg, emt.expectedMessages) {
panic(fmt.Sprintf("the message \"%v\"does not match any of the given patterns: %#v", msg, emt.expectedMessages))
} else {
fmt.Println(msg)
}
return 0, nil
}
func (fk FakeCommandToTest) Command(repoCfg config.RepositoryConfig) commands.CommandStatus {
status := commands.CommandStatus{
Name: repoCfg.Name,
Changed: false,
Message: "response from fake command",
Error: false,
}
if fk.triggerError {
status.Error = true
}
if fk.triggerChanged {
status.Changed = true
}
return status
}
func prepareConfigContent() (string, string) {
checkErrorDuringPreparation := func(err error) {
if err != nil {
fmt.Printf("Cannot prepare a temporary directory for testing! %v ", err.Error())
os.Exit(2)
}
}
baseTmp := fmt.Sprintf("%v/grmTest", os.TempDir())
if _, ok := os.Stat(baseTmp); ok != nil {
err := os.Mkdir(baseTmp, 0777)
checkErrorDuringPreparation(err)
}
tempDir, err := os.MkdirTemp(baseTmp, "*")
checkErrorDuringPreparation(err)
configFilePath := fmt.Sprintf("%v/config-file.yaml", tempDir)
file, err := os.Create(configFilePath)
checkErrorDuringPreparation(err)
defer file.Close()
yamlConfig := fmt.Sprintf(`
workspace: %v
repositories:
- src: "https://github.com/golang/example.git"
tags: ['example']
`, tempDir)
_, err = file.WriteString(yamlConfig)
checkErrorDuringPreparation(err)
return tempDir, configFilePath
}
func TestParseApplication(t *testing.T) {
workdir, configFile := prepareConfigContent()
t.Cleanup(func() {
os.Remove(workdir)
})
args := []string{"custom-grm", "sync", "-c", configFile}
grm := GitRepositoryManager{}
grm.Parse(args)
if workdir != grm.configuration.Workspace {
t.Errorf("Expected to get %v, instead of this got %v", workdir, grm.configuration.Repositories)
}
if !grm.cliArguments.Sync {
t.Error("The value of \"sync\" is expected to be true")
}
expectedRepo := config.RepositoryConfig{
Name: "example",
Src: "https://github.com/golang/example.git",
Dest: "example",
Tags: []string{"example"},
}
if !reflect.DeepEqual(expectedRepo, grm.configuration.Repositories[0]) {
t.Errorf("Expected to get %v, instead of this got %v", expectedRepo, grm.configuration.Repositories[0])
}
}
func TestOutputFromSync(t *testing.T) {
grm := GitRepositoryManager{
configuration: config.Configuration{
Workspace: "/tmp",
},
cliArguments: config.CliArguments{
Sync: true,
Version: true,
Color: false,
Routines: 10,
},
}
emt := ExpectedMessageTester{
expectedMessages: []string{
"Info: Synchronizing repositories\n",
"Info: All repositories are synced\n",
fmt.Sprintf("Info: Current version: %v\n", VERSION),
},
}
grm.Run(emt)
}
func TestLimitTags(t *testing.T) {
grm := GitRepositoryManager{
cliArguments: config.CliArguments{
LimitToTags: []string{"example"},
Routines: 10,
},
configuration: config.Configuration{
Repositories: []config.RepositoryConfig{
{Name: "example1", Tags: []string{"example"}},
{Name: "example2", Tags: []string{"example"}},
{Name: "notExample"},
},
},
}
fakeCommand := FakeCommandToTest{
triggerError: false,
triggerChanged: false,
}
emt := ExpectedMessageTester{
expectedMessages: []string{
"Repository \"example1\": response from fake command\n",
"Repository \"example2\": response from fake command\n",
},
}
echo.Color(false)
echo.Output(emt)
grm.limitRepositoriesToTags()
grm.runCommand(fakeCommand)
}
func TestLimitName(t *testing.T) {
grm := GitRepositoryManager{
cliArguments: config.CliArguments{
LimitToName: "notExample",
Routines: 10,
},
configuration: config.Configuration{
Repositories: []config.RepositoryConfig{
{Name: "example1", Tags: []string{"example"}},
{Name: "example2", Tags: []string{"example"}},
{Name: "notExample"},
},
},
}
fakeCommand := FakeCommandToTest{
triggerError: false,
triggerChanged: false,
}
emt := ExpectedMessageTester{
expectedMessages: []string{
"Repository \"notExample\": response from fake command\n",
},
}
echo.Color(false)
echo.Output(emt)
grm.limitRepositoryToName()
grm.runCommand(fakeCommand)
}
func TestRunWithNotExistingNameInLimit(t *testing.T) {
grm := GitRepositoryManager{
cliArguments: config.CliArguments{
LimitToName: "not-existing-name",
Routines: 10,
},
configuration: config.Configuration{
Repositories: []config.RepositoryConfig{
{Name: "example1", Tags: []string{"example"}},
{Name: "example2", Tags: []string{"example"}},
{Name: "notExample"},
},
},
}
emt := ExpectedMessageTester{
expectedMessages: []string{
"Error: no repository was found with the specified name\n",
},
}
echo.Color(false)
status := grm.Run(emt)
if status != 1 {
t.Errorf("Expected to get status %v, instead o this got %v", 1, status)
}
}
func TestRunWithNotExistingTagsInLimit(t *testing.T) {
grm := GitRepositoryManager{
cliArguments: config.CliArguments{
LimitToTags: []string{"not-existing-tag"},
Routines: 10,
},
configuration: config.Configuration{
Repositories: []config.RepositoryConfig{
{Name: "example1", Tags: []string{"example"}},
{Name: "example2", Tags: []string{"example"}},
{Name: "notExample"},
},
},
}
emt := ExpectedMessageTester{
expectedMessages: []string{
"Error: no repository was found with the specified tags\n",
},
}
echo.Color(false)
status := grm.Run(emt)
if status != 1 {
t.Errorf("Expected to get status %v, instead o this got %v", 1, status)
}
}
func TestGetStatusOutput(t *testing.T) {
grm := GitRepositoryManager{
configuration: config.Configuration{
Workspace: "/tmp",
},
cliArguments: config.CliArguments{
Status: true,
Routines: 10,
},
}
emt := ExpectedMessageTester{
expectedMessages: []string{
"Info: Current status of repositories\n",
},
}
echo.Color(false)
status := grm.Run(emt)
if status != 0 {
t.Errorf("Expected to get status %v, instead o this got %v", 1, status)
}
}
func TestDescribeStatusErrorNoColor(t *testing.T) {
emt := ExpectedMessageTester{
expectedMessages: []string{
"Repository \"Test\": an error occurred: test\n",
},
}
echo.Color(false)
echo.Output(emt)
status := commands.CommandStatus{
Name: "Test",
Message: "test",
Error: true,
}
describeStatus(status)
}
func TestDescribeStatusErrorColor(t *testing.T) {
emt := ExpectedMessageTester{
expectedMessages: []string{
fmt.Sprintf("%vRepository \"Test\": an error occurred: test%v\n", echo.ColorRed, echo.ColorReset),
},
}
echo.Color(true)
echo.Output(emt)
status := commands.CommandStatus{
Name: "Test",
Message: "test",
Error: true,
}
describeStatus(status)
}
func TestDescribeStatusChangedNoColor(t *testing.T) {
emt := ExpectedMessageTester{
expectedMessages: []string{
"Repository \"Test\": test\n",
},
}
echo.Color(false)
echo.Output(emt)
status := commands.CommandStatus{
Name: "Test",
Message: "test",
Changed: true,
}
describeStatus(status)
}
func TestDescribeStatusChangedColor(t *testing.T) {
emt := ExpectedMessageTester{
expectedMessages: []string{
fmt.Sprintf("%vRepository \"Test\": test%v\n", echo.ColorYellow, echo.ColorReset),
},
}
echo.Color(true)
echo.Output(emt)
status := commands.CommandStatus{
Name: "Test",
Message: "test",
Changed: true,
}
describeStatus(status)
}
func TestDescribeStatusNoChangeNoColor(t *testing.T) {
emt := ExpectedMessageTester{
expectedMessages: []string{
"Repository \"Test\": test\n",
},
}
echo.Color(false)
echo.Output(emt)
status := commands.CommandStatus{
Name: "Test",
Message: "test",
Changed: false,
}
describeStatus(status)
}
func TestDescribeStatusNoChangeColor(t *testing.T) {
emt := ExpectedMessageTester{
expectedMessages: []string{
fmt.Sprintf("%vRepository \"Test\": test%v\n", echo.ColorGreen, echo.ColorReset),
},
}
echo.Color(true)
echo.Output(emt)
status := commands.CommandStatus{
Name: "Test",
Message: "test",
Changed: false,
}
describeStatus(status)
}
func TestSkipRepository(t *testing.T) {
grm := GitRepositoryManager{
cliArguments: config.CliArguments{
LimitToTags: []string{"example"},
Routines: 10,
},
configuration: config.Configuration{
Repositories: []config.RepositoryConfig{
{Name: "example1"},
{Name: "example2", Skip: true},
{Name: "example3"},
},
},
}
fakeCommand := FakeCommandToTest{
triggerError: false,
triggerChanged: false,
}
emt := ExpectedMessageTester{
expectedMessages: []string{
"Repository \"example1\": response from fake command\n",
"Repository \"example3\": response from fake command\n",
},
}
echo.Color(false)
echo.Output(emt)
grm.runCommand(fakeCommand)
}
func TestSkipRepositoryWithIgnore(t *testing.T) {
grm := GitRepositoryManager{
cliArguments: config.CliArguments{
LimitToTags: []string{"example"},
Routines: 10,
IgnoreSkipped: true,
},
configuration: config.Configuration{
Repositories: []config.RepositoryConfig{
{Name: "example1"},
{Name: "example2", Skip: true},
{Name: "example3"},
},
},
}
fakeCommand := FakeCommandToTest{
triggerError: false,
triggerChanged: false,
}
emt := ExpectedMessageTester{
expectedMessages: []string{
"Repository \"example1\": response from fake command\n",
"Repository \"example2\": response from fake command\n",
"Repository \"example3\": response from fake command\n",
},
}
echo.Color(false)
echo.Output(emt)
grm.runCommand(fakeCommand)
}

54
internal/grm/utils.go Normal file
View File

@ -0,0 +1,54 @@
package grm
import (
"errors"
"fmt"
"gitlab.com/revalus/grm/internal/config"
"os"
"strings"
)
func getFileContent(pathToFile string) ([]byte, error) {
return os.ReadFile(pathToFile)
}
func getFileExtension(pathToFile string) (string, error) {
splitFileName := strings.Split(pathToFile, ".")
if len(splitFileName) == 1 {
msg := fmt.Sprintf("excension for file \"%v\", not found", splitFileName)
return "", errors.New(msg)
}
fileExtension := splitFileName[len(splitFileName)-1]
return fileExtension, nil
}
func checkIsItemInSlice(check string, sliceToCheck []string) bool {
for _, item := range sliceToCheck {
if item == check {
return true
}
}
return false
}
func checkAnyOfItemInSlice(check []string, sliceToCheck []string) bool {
for _, item := range check {
if checkIsItemInSlice(item, sliceToCheck) {
return true
}
}
return false
}
func reverseRepositoryConfigs(repositories []config.RepositoryConfig) []config.RepositoryConfig {
for i, j := 0, len(repositories)-1; i < j; i, j = i+1, j-1 {
repositories[i], repositories[j] = repositories[j], repositories[i]
}
return repositories
}

View File

@ -0,0 +1,84 @@
package grm
import (
"gitlab.com/revalus/grm/internal/config"
"reflect"
"testing"
)
func TestGetFileExtension(t *testing.T) {
toTest := map[string]string{
"myYamlFile.yaml": "yaml",
"myTxtFile.txt": "txt",
"myJsonFile.json": "json",
}
for key, value := range toTest {
result, err := getFileExtension(key)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if result != value {
t.Errorf("Expected to get %v, instead of this got %v", value, result)
}
}
}
func TestErrorInGetExtensionFile(t *testing.T) {
result, err := getFileExtension("test")
if err == nil {
t.Errorf("Expected to get error, instead of this got result %v", result)
}
}
func TestIsItemInSlice(t *testing.T) {
testedSlice := []string{"1", "2", "3", "4", "5"}
result := checkIsItemInSlice("0", testedSlice)
if result {
t.Error("Expected to get false as result")
}
result = checkIsItemInSlice("1", testedSlice)
if !result {
t.Error("Expected to get true as result")
}
}
func TestIsAnyInSlice(t *testing.T) {
testedSlice := []string{"1", "2", "3", "4", "5"}
result := checkAnyOfItemInSlice([]string{"0", "10"}, testedSlice)
if result {
t.Error("Expected to get false as result")
}
result = checkAnyOfItemInSlice([]string{"0", "5"}, testedSlice)
if !result {
t.Error("Expected to get true as result")
}
}
func TestReverseStringsSlice(t *testing.T) {
testedSlice := []config.RepositoryConfig{
{Name: "test1"},
{Name: "test2"},
{Name: "test3"},
}
expectedResult := []config.RepositoryConfig{
{Name: "test3"},
{Name: "test2"},
{Name: "test1"},
}
result := reverseRepositoryConfigs(testedSlice)
if !reflect.DeepEqual(result, expectedResult) {
t.Errorf("Expected to get \"%#v\", instead of this got \"%#v\"", expectedResult, result)
}
}

16
main.go
View File

@ -1,16 +0,0 @@
package main
import (
"os"
"gitlab.com/revalus/grm/app"
)
const ()
func main() {
app := app.GitRepositoryManager{}
app.Parse(os.Args)
app.Run()
}