From 23e4547e52683af9f303fb2c009803953ab60746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20P=C4=99czkowski?= Date: Sun, 7 Nov 2021 14:05:45 +0100 Subject: [PATCH] Add possiblity to limit actions to tagged or named repository --- README.md | 14 ++- app/app.go | 59 +++++++++++-- app/app_test.go | 147 ++++++++++++++++++++++++++++++- app/utils.go | 30 +++++++ app/utils_test.go | 53 ++++++++++- config/cmd.go | 14 +++ config/cmd_test.go | 32 +++++++ config/config_file_parse_test.go | 3 + config/errors.go | 6 +- config/structures.go | 9 +- main.go | 2 +- 11 files changed, 345 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 9c2da79..fb254f7 100644 --- a/README.md +++ b/README.md @@ -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,9 +25,13 @@ 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)| ### Commands @@ -37,6 +42,7 @@ By default, the config file is searched for in `[HOME_DIR]./config/grm/config.ya ## Changelog +- 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 diff --git a/app/app.go b/app/app.go index 86be04c..9ebd403 100644 --- a/app/app.go +++ b/app/app.go @@ -1,6 +1,7 @@ package app import ( + "errors" "os" "gitlab.com/revalus/grm/commands" @@ -10,7 +11,9 @@ import ( const ( APP_NAME = "Git repository manager" APP_DESCRIPTION = "Manage your repository with simple app" - VERSION = "0.2.0" + VERSION = "0.3.0" + errNotFoundTags = "no repository was found with the specified tags" + errNotFoundName = "no repository was found with the specified name" ) type GitRepositoryManager struct { @@ -47,17 +50,35 @@ func (g *GitRepositoryManager) Parse(args []string) { g.configuration = configuration } -func (g *GitRepositoryManager) Run() { - if g.cliArguments.Sync { +func (g *GitRepositoryManager) Run() int { + + exitCode := 0 + + if len(g.cliArguments.LimitTags) != 0 { + err := g.limitTags() + if err != nil { + g.console.ErrorfMsg(err.Error()) + exitCode = 1 + } + } + + if g.cliArguments.LimitName != "" { + err := g.limitName() + if err != nil { + g.console.ErrorfMsg(err.Error()) + exitCode = 1 + } + } + + if g.cliArguments.Sync && exitCode == 0 { 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 { + if g.cliArguments.Status && exitCode == 0 { + g.console.InfoFMsg("Current status of repositories") status := commands.NewStatusChecker(g.configuration.Workspace) g.runCommand(status) } @@ -65,6 +86,7 @@ func (g *GitRepositoryManager) Run() { if g.cliArguments.Version { g.console.InfoFMsg("Current version: %v", VERSION) } + return exitCode } func (g GitRepositoryManager) describeStatus(status commands.CommandStatus) { @@ -80,6 +102,31 @@ func (g GitRepositoryManager) describeStatus(status commands.CommandStatus) { } } +func (g *GitRepositoryManager) limitTags() error { + limitedTagsTmp := []config.RepositoryConfig{} + + for _, item := range g.configuration.Repositories { + if checkAnyOfItemInSlice(item.Tags, g.cliArguments.LimitTags) { + limitedTagsTmp = append(limitedTagsTmp, item) + } + } + if len(limitedTagsTmp) == 0 { + return errors.New(errNotFoundTags) + } + g.configuration.Repositories = reverseRepositoryConfigs(limitedTagsTmp) + return nil +} + +func (g *GitRepositoryManager) limitName() error { + for _, item := range g.configuration.Repositories { + if g.cliArguments.LimitName == item.Name { + g.configuration.Repositories = []config.RepositoryConfig{item} + return nil + } + } + return errors.New(errNotFoundName) +} + func (g *GitRepositoryManager) runCommand(cmd commands.Command) { statusChan := make(chan commands.CommandStatus) diff --git a/app/app_test.go b/app/app_test.go index cb30a11..c1471ad 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -3,11 +3,36 @@ package app import ( "fmt" "os" + "reflect" "testing" + "gitlab.com/revalus/grm/commands" "gitlab.com/revalus/grm/config" ) +type FakeCommandToTest struct { + triggerError bool + triggerChanged bool +} + +func (fk FakeCommandToTest) Command(repoCfg config.RepositoryConfig, cmdStatus chan 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 + } + + cmdStatus <- status +} + func prepareConfigContent() (string, string) { checkErrorDuringPreparation := func(err error) { if err != nil { @@ -35,7 +60,9 @@ func prepareConfigContent() (string, string) { yamlConfig := fmt.Sprintf(` workspace: %v repositories: - - src: "https://github.com/golang/example.git"`, tempDir) + - src: "https://github.com/golang/example.git" + tags: ['example'] +`, tempDir) _, err = file.WriteString(yamlConfig) @@ -65,12 +92,12 @@ func TestParseApplication(t *testing.T) { Name: "example", Src: "https://github.com/golang/example.git", Dest: "example", + Tags: []string{"example"}, } - if expectedRepo != grm.configuration.Repositories[0] { + 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 Example_test_sync_output() { @@ -90,5 +117,117 @@ func Example_test_sync_output() { // Output: // Info: Synchronizing repositories // Info: All repositories are synced - // Info: Current version: 0.2.0 + // Info: Current version: 0.3.0 +} + +func Example_limit_test_tags() { + grm := GitRepositoryManager{ + cliArguments: config.CliArguments{ + LimitTags: []string{"example"}, + }, + configuration: config.Configuration{ + Repositories: []config.RepositoryConfig{ + {Name: "example1", Tags: []string{"example"}}, + {Name: "example2", Tags: []string{"example"}}, + {Name: "notExample"}, + }, + }, + console: ConsoleOutput{ + Color: false, + }, + } + fakeCommand := FakeCommandToTest{ + triggerError: false, + triggerChanged: false, + } + grm.limitTags() + grm.runCommand(fakeCommand) + // Output: + // Repository "example1": response from fake command + // Repository "example2": response from fake command +} + +func Example_limit_name() { + grm := GitRepositoryManager{ + cliArguments: config.CliArguments{ + LimitName: "notExample", + }, + configuration: config.Configuration{ + Repositories: []config.RepositoryConfig{ + {Name: "example1", Tags: []string{"example"}}, + {Name: "example2", Tags: []string{"example"}}, + {Name: "notExample"}, + }, + }, + console: ConsoleOutput{ + Color: false, + }, + } + fakeCommand := FakeCommandToTest{ + triggerError: false, + triggerChanged: false, + } + grm.limitName() + grm.runCommand(fakeCommand) + // Output: + // Repository "notExample": response from fake command +} + +func Example_run_with_limit_not_existing_name() { + grm := GitRepositoryManager{ + cliArguments: config.CliArguments{ + LimitName: "not-existing-name", + }, + configuration: config.Configuration{ + Repositories: []config.RepositoryConfig{ + {Name: "example1", Tags: []string{"example"}}, + {Name: "example2", Tags: []string{"example"}}, + {Name: "notExample"}, + }, + }, + console: ConsoleOutput{ + Color: false, + }, + } + grm.Run() + // Output: + // Error: no repository was found with the specified name +} + +func Example_run_with_limit_not_existing_tags() { + grm := GitRepositoryManager{ + cliArguments: config.CliArguments{ + LimitTags: []string{"not-existing-tag"}, + }, + configuration: config.Configuration{ + Repositories: []config.RepositoryConfig{ + {Name: "example1", Tags: []string{"example"}}, + {Name: "example2", Tags: []string{"example"}}, + {Name: "notExample"}, + }, + }, + console: ConsoleOutput{ + Color: false, + }, + } + grm.Run() + // Output: + // Error: no repository was found with the specified tags +} + +func Example_test_status_output() { + grm := GitRepositoryManager{ + configuration: config.Configuration{ + Workspace: "/tmp", + }, + cliArguments: config.CliArguments{ + Status: true, + }, + console: ConsoleOutput{ + Color: false, + }, + } + grm.Run() + // Output: + // Info: Current status of repositories } diff --git a/app/utils.go b/app/utils.go index f2d8ecb..bdbdfdc 100644 --- a/app/utils.go +++ b/app/utils.go @@ -5,6 +5,8 @@ import ( "fmt" "io/ioutil" "strings" + + "gitlab.com/revalus/grm/config" ) func getFileContent(pathToFile string) ([]byte, error) { @@ -23,3 +25,31 @@ func getFileExcension(pathToFile string) (string, error) { return fileExcension, 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 +} diff --git a/app/utils_test.go b/app/utils_test.go index 8f3901c..ed4df90 100644 --- a/app/utils_test.go +++ b/app/utils_test.go @@ -1,6 +1,11 @@ package app -import "testing" +import ( + "reflect" + "testing" + + "gitlab.com/revalus/grm/config" +) func TestGetFileExtension(t *testing.T) { @@ -32,5 +37,49 @@ func TestErrorInGetExcensionFile(t *testing.T) { 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) + } } diff --git a/config/cmd.go b/config/cmd.go index bec1841..6d0f9e9 100644 --- a/config/cmd.go +++ b/config/cmd.go @@ -40,6 +40,14 @@ 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", + }) + if err := parser.Parse(arguments); err != nil { return CliArguments{}, errors.New(parser.Usage("Please follow this help")) } @@ -48,11 +56,17 @@ func ParseCliArguments(name, description string, arguments []string) (CliArgumen return CliArguments{}, errors.New(errNoCommand) } + if *limitName != "" && len(*limitTags) != 0 { + return CliArguments{}, errors.New(errNameAndTagsTogether) + } + return CliArguments{ ConfigurationFile: *configFile, Sync: syncCMD.Happened(), Status: statusCMD.Happened(), Version: *version, Color: !(*color), + LimitName: *limitName, + LimitTags: *limitTags, }, nil } diff --git a/config/cmd_test.go b/config/cmd_test.go index 457b510..1d90adf 100644 --- a/config/cmd_test.go +++ b/config/cmd_test.go @@ -39,6 +39,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 err.Error() != errNoCommand { + t.Errorf("Expected to get \"%v\", instead of this got \"%v\"", errNoCommand, err.Error()) + } + +} + +func TestParsingNothingProvided(t *testing.T) { + // First item in os.Args is appPath, this have to be mocked fakeOSArgs := []string{"appName"} @@ -49,3 +65,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()) + } + +} diff --git a/config/config_file_parse_test.go b/config/config_file_parse_test.go index ed3306f..e5fe458 100644 --- a/config/config_file_parse_test.go +++ b/config/config_file_parse_test.go @@ -27,6 +27,8 @@ repositories: dest: "example/path" name: "custom_example" - src: https://github.com/example/example2.git + tags: + - "example" `) homedir, _ := os.UserHomeDir() @@ -43,6 +45,7 @@ repositories: Name: "example2", Src: "https://github.com/example/example2.git", Dest: "example2", + Tags: []string{"example"}, }, }, } diff --git a/config/errors.go b/config/errors.go index df68675..4a9dcac 100644 --- a/config/errors.go +++ b/config/errors.go @@ -1,7 +1,6 @@ package config import ( - "errors" "fmt" "strconv" "strings" @@ -12,6 +11,7 @@ const ( 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 +21,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, ",")) } diff --git a/config/structures.go b/config/structures.go index f6a8bca..fb26c6c 100644 --- a/config/structures.go +++ b/config/structures.go @@ -6,9 +6,10 @@ type Configuration struct { } type RepositoryConfig struct { - Name string `yaml:",omitempty"` - Src string `yaml:",omitempty"` - Dest string `yaml:",omitempty"` + Name string `yaml:",omitempty"` + Src string `yaml:",omitempty"` + Dest string `yaml:",omitempty"` + Tags []string `yaml:",omitempty"` } type CliArguments struct { @@ -17,4 +18,6 @@ type CliArguments struct { Status bool Version bool Color bool + LimitName string + LimitTags []string } diff --git a/main.go b/main.go index 80af623..af24735 100644 --- a/main.go +++ b/main.go @@ -12,5 +12,5 @@ func main() { app := app.GitRepositoryManager{} app.Parse(os.Args) - app.Run() + os.Exit(app.Run()) }