Add possiblity to limit actions to tagged or named repository

This commit is contained in:
Mikołaj Pęczkowski 2021-11-07 14:05:45 +01:00
parent 5c98ab6554
commit 23e4547e52
11 changed files with 345 additions and 24 deletions

View File

@ -11,9 +11,10 @@ Create a yaml file with the given structure
workspace: ${HOME}/workspace workspace: ${HOME}/workspace
# List with repositories, that you want to manage with GRM # List with repositories, that you want to manage with GRM
repositories: 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 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 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 ### Note
@ -24,9 +25,13 @@ By default, the config file is searched for in `[HOME_DIR]./config/grm/config.ya
### Global args ### Global args
| argument | type | default | Description | | argument | type | default | Description |
|-------------------|--------|--------------------------------------|----------------------------------------------------------------------| |---------------------------|----------|--------------------------------------|------------------------------------------------------------------------|
| -c, --config-file | string | `[HOME_DIR]./config/grm/config.yaml` | Path to configuration file, where the repositories must be specified | | **-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 ### Commands
@ -37,6 +42,7 @@ By default, the config file is searched for in `[HOME_DIR]./config/grm/config.ya
## Changelog ## 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.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.1 Allow to use env vars in config
- 0.1.0 Add sync command - allow to fetch and clone repositories - 0.1.0 Add sync command - allow to fetch and clone repositories

View File

@ -1,6 +1,7 @@
package app package app
import ( import (
"errors"
"os" "os"
"gitlab.com/revalus/grm/commands" "gitlab.com/revalus/grm/commands"
@ -10,7 +11,9 @@ import (
const ( const (
APP_NAME = "Git repository manager" APP_NAME = "Git repository manager"
APP_DESCRIPTION = "Manage your repository with simple app" 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 { type GitRepositoryManager struct {
@ -47,17 +50,35 @@ func (g *GitRepositoryManager) Parse(args []string) {
g.configuration = configuration g.configuration = configuration
} }
func (g *GitRepositoryManager) Run() { func (g *GitRepositoryManager) Run() int {
if g.cliArguments.Sync {
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") g.console.InfoFMsg("Synchronizing repositories")
println()
sync := commands.NewSynchronizer(g.configuration.Workspace) sync := commands.NewSynchronizer(g.configuration.Workspace)
g.runCommand(sync) g.runCommand(sync)
println()
g.console.InfoFMsg("All repositories are synced") 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) status := commands.NewStatusChecker(g.configuration.Workspace)
g.runCommand(status) g.runCommand(status)
} }
@ -65,6 +86,7 @@ func (g *GitRepositoryManager) Run() {
if g.cliArguments.Version { if g.cliArguments.Version {
g.console.InfoFMsg("Current version: %v", VERSION) g.console.InfoFMsg("Current version: %v", VERSION)
} }
return exitCode
} }
func (g GitRepositoryManager) describeStatus(status commands.CommandStatus) { 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) { func (g *GitRepositoryManager) runCommand(cmd commands.Command) {
statusChan := make(chan commands.CommandStatus) statusChan := make(chan commands.CommandStatus)

View File

@ -3,11 +3,36 @@ package app
import ( import (
"fmt" "fmt"
"os" "os"
"reflect"
"testing" "testing"
"gitlab.com/revalus/grm/commands"
"gitlab.com/revalus/grm/config" "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) { func prepareConfigContent() (string, string) {
checkErrorDuringPreparation := func(err error) { checkErrorDuringPreparation := func(err error) {
if err != nil { if err != nil {
@ -35,7 +60,9 @@ func prepareConfigContent() (string, string) {
yamlConfig := fmt.Sprintf(` yamlConfig := fmt.Sprintf(`
workspace: %v workspace: %v
repositories: repositories:
- src: "https://github.com/golang/example.git"`, tempDir) - src: "https://github.com/golang/example.git"
tags: ['example']
`, tempDir)
_, err = file.WriteString(yamlConfig) _, err = file.WriteString(yamlConfig)
@ -65,12 +92,12 @@ func TestParseApplication(t *testing.T) {
Name: "example", Name: "example",
Src: "https://github.com/golang/example.git", Src: "https://github.com/golang/example.git",
Dest: "example", 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]) t.Errorf("Expected to get %v, instead of this got %v", expectedRepo, grm.configuration.Repositories[0])
} }
} }
func Example_test_sync_output() { func Example_test_sync_output() {
@ -90,5 +117,117 @@ func Example_test_sync_output() {
// Output: // Output:
// Info: Synchronizing repositories // Info: Synchronizing repositories
// Info: All repositories are synced // 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
} }

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"strings" "strings"
"gitlab.com/revalus/grm/config"
) )
func getFileContent(pathToFile string) ([]byte, error) { func getFileContent(pathToFile string) ([]byte, error) {
@ -23,3 +25,31 @@ func getFileExcension(pathToFile string) (string, error) {
return fileExcension, nil 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
}

View File

@ -1,6 +1,11 @@
package app package app
import "testing" import (
"reflect"
"testing"
"gitlab.com/revalus/grm/config"
)
func TestGetFileExtension(t *testing.T) { func TestGetFileExtension(t *testing.T) {
@ -32,5 +37,49 @@ func TestErrorInGetExcensionFile(t *testing.T) {
if err == nil { if err == nil {
t.Errorf("Expected to get error, instead of this got result %v", result) 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)
}
} }

View File

@ -40,6 +40,14 @@ func ParseCliArguments(name, description string, arguments []string) (CliArgumen
Help: "Turn off color printing", 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 { if err := parser.Parse(arguments); err != nil {
return CliArguments{}, errors.New(parser.Usage("Please follow this help")) 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) return CliArguments{}, errors.New(errNoCommand)
} }
if *limitName != "" && len(*limitTags) != 0 {
return CliArguments{}, errors.New(errNameAndTagsTogether)
}
return CliArguments{ return CliArguments{
ConfigurationFile: *configFile, ConfigurationFile: *configFile,
Sync: syncCMD.Happened(), Sync: syncCMD.Happened(),
Status: statusCMD.Happened(), Status: statusCMD.Happened(),
Version: *version, Version: *version,
Color: !(*color), Color: !(*color),
LimitName: *limitName,
LimitTags: *limitTags,
}, nil }, nil
} }

View File

@ -39,6 +39,22 @@ func TestParsingDefaultArguments(t *testing.T) {
func TestParsingWithoutCommand(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 // First item in os.Args is appPath, this have to be mocked
fakeOSArgs := []string{"appName"} 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())
}
}

View File

@ -27,6 +27,8 @@ repositories:
dest: "example/path" dest: "example/path"
name: "custom_example" name: "custom_example"
- src: https://github.com/example/example2.git - src: https://github.com/example/example2.git
tags:
- "example"
`) `)
homedir, _ := os.UserHomeDir() homedir, _ := os.UserHomeDir()
@ -43,6 +45,7 @@ repositories:
Name: "example2", Name: "example2",
Src: "https://github.com/example/example2.git", Src: "https://github.com/example/example2.git",
Dest: "example2", Dest: "example2",
Tags: []string{"example"},
}, },
}, },
} }

View File

@ -1,7 +1,6 @@
package config package config
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -12,6 +11,7 @@ const (
errNotSupportedType = "not supported configuration type" errNotSupportedType = "not supported configuration type"
errMissingWorkspaceField = "missing required \"workspace\" field" errMissingWorkspaceField = "missing required \"workspace\" field"
errMissingSrcField = "missing required field the \"src\" in row %v" 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 { 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)) rowsInString = append(rowsInString, strconv.Itoa(row))
} }
errorMessage := fmt.Sprintf("The %v \"%v\" is duplicated in rows: %v", field, name, strings.Join(rowsInString, ",")) return fmt.Errorf("the %v \"%v\" is duplicated in rows: %v", field, name, strings.Join(rowsInString, ","))
return errors.New(errorMessage)
} }

View File

@ -6,9 +6,10 @@ type Configuration struct {
} }
type RepositoryConfig struct { type RepositoryConfig struct {
Name string `yaml:",omitempty"` Name string `yaml:",omitempty"`
Src string `yaml:",omitempty"` Src string `yaml:",omitempty"`
Dest string `yaml:",omitempty"` Dest string `yaml:",omitempty"`
Tags []string `yaml:",omitempty"`
} }
type CliArguments struct { type CliArguments struct {
@ -17,4 +18,6 @@ type CliArguments struct {
Status bool Status bool
Version bool Version bool
Color bool Color bool
LimitName string
LimitTags []string
} }

View File

@ -12,5 +12,5 @@ func main() {
app := app.GitRepositoryManager{} app := app.GitRepositoryManager{}
app.Parse(os.Args) app.Parse(os.Args)
app.Run() os.Exit(app.Run())
} }