commit 5e6a5e23fd32aa68a19daf657eeea4db2d517654 Author: Mikołaj Pęczkowski Date: Tue Nov 2 19:19:31 2021 +0100 Add sync command diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d3bfeb --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Git repositories manager + +Sync your repositories with single click + +## Usage + +Create a yaml file with the given structure + +```yaml +# Place where your repositories should be saved +workspace: ${HOME}/workspace +# List with repositories, that you want to manage with GRM +repositories: +- src: "git@github.com:Revalus/GitRepositoryManager.git" + 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 +``` + +### Note + +By default, the config file is searched for in `[HOME_DIR]./config/grm/config.yaml`. + +## Commands and arguments + +### 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 | + +### Commands + +| command | Description | +|---------|-------------| +| sync | Fetches changes from repositories or pulls a repository if one does not exist. + +## Changelog + +0.1 Add sync command - allow to fetch and clone repositories diff --git a/app/app.go b/app/app.go new file mode 100644 index 0000000..67c78a4 --- /dev/null +++ b/app/app.go @@ -0,0 +1,89 @@ +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.1" +) + +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.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) + } +} diff --git a/app/app_test.go b/app/app_test.go new file mode 100644 index 0000000..56b9267 --- /dev/null +++ b/app/app_test.go @@ -0,0 +1,94 @@ +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.1 +} diff --git a/app/console_output.go b/app/console_output.go new file mode 100644 index 0000000..641ebda --- /dev/null +++ b/app/console_output.go @@ -0,0 +1,61 @@ +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", colorGreen, msg) + } + fmt.Println(msg) +} + +func (co ConsoleOutput) ChangedStatusF(format string, a ...interface{}) { + msg := fmt.Sprintf(format, a...) + if co.Color { + msg = fmt.Sprintf("%v%v", colorYellow, msg) + } + fmt.Println(msg) +} + +func (co ConsoleOutput) ErrorStatusF(format string, a ...interface{}) { + msg := fmt.Sprintf(format, a...) + if co.Color { + msg = fmt.Sprintf("%v%v", colorRed, msg) + } + fmt.Println(msg) +} diff --git a/app/utils.go b/app/utils.go new file mode 100644 index 0000000..f2d8ecb --- /dev/null +++ b/app/utils.go @@ -0,0 +1,25 @@ +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 +} diff --git a/app/utils_test.go b/app/utils_test.go new file mode 100644 index 0000000..8f3901c --- /dev/null +++ b/app/utils_test.go @@ -0,0 +1,36 @@ +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) + } + +} diff --git a/commands/command.go b/commands/command.go new file mode 100644 index 0000000..73b1333 --- /dev/null +++ b/commands/command.go @@ -0,0 +1,13 @@ +package commands + +import "gitlab.com/revalus/grm/config" + +type Command interface { + Command(repoCfg config.RepositoryConfig, cmdStatus chan CommandStatus) +} +type CommandStatus struct { + Name string + Changed bool + Message string + Error bool +} diff --git a/commands/sync_cmd.go b/commands/sync_cmd.go new file mode 100644 index 0000000..d38c4c6 --- /dev/null +++ b/commands/sync_cmd.go @@ -0,0 +1,88 @@ +package commands + +import ( + "fmt" + + "github.com/go-git/go-git/v5" + "gitlab.com/revalus/grm/config" +) + +type Synchronizer struct { + workspace string +} + +func NewSynchronizer(workspace string) Synchronizer { + return Synchronizer{ + workspace: workspace, + } +} + +const ( + syncUpToDate = "up to date" + syncFetched = "has been fetched" // Why fetched, instead of updated? To be consistent with git commands :D + syncCloned = "has been cloned" +) + +func fetchRepository(repo *git.Repository) (bool, error) { + err := repo.Fetch(&git.FetchOptions{}) + + if err == git.NoErrAlreadyUpToDate { + return false, nil + } + + if err != nil && err != git.NoErrAlreadyUpToDate { + return false, err + } + + return true, nil +} + +func cloneRepository(destPath string, repoCfg *config.RepositoryConfig) (bool, error) { + _, err := git.PlainClone(destPath, false, &git.CloneOptions{ + URL: repoCfg.Src, + }) + + if err != nil { + return false, err + } + + return true, nil +} + +func (s Synchronizer) Command(repoCfg config.RepositoryConfig, status chan CommandStatus) { + var err error + + cmdStatus := CommandStatus{ + Name: repoCfg.Name, + Changed: false, + Message: "", + Error: false, + } + + 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 + } else { + cmdStatus.Message = syncUpToDate + } + } else { + cmdStatus.Error = true + cmdStatus.Message = err.Error() + status <- cmdStatus + } + + if err != nil { + cmdStatus.Error = true + cmdStatus.Message = err.Error() + } + + status <- cmdStatus + +} diff --git a/commands/sync_cmd_test.go b/commands/sync_cmd_test.go new file mode 100644 index 0000000..6d2f280 --- /dev/null +++ b/commands/sync_cmd_test.go @@ -0,0 +1,95 @@ +package commands + +import ( + "fmt" + "os" + "testing" + + "gitlab.com/revalus/grm/config" +) + +func createTempDir() 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) + + return tempDir +} + +func TestSyncInit(t *testing.T) { + sync := NewSynchronizer("test") + if sync.workspace != "test" { + t.Errorf("Expected to get \"test\", instead of this got %v", sync.workspace) + } +} + +func TestSyncCommand(t *testing.T) { + + workdir := createTempDir() + defer func() { + os.RemoveAll(workdir) + }() + + sync := Synchronizer{ + workspace: workdir, + } + + cfg := config.RepositoryConfig{ + Src: "https://github.com/avelino/awesome-go", + Dest: "awesome-go", + } + + ch := make(chan CommandStatus) + + // Pull part + go sync.Command(cfg, ch) + + cloneStatus := <-ch + if cloneStatus.Error { + t.Errorf("Unexpected error: %v", cloneStatus.Message) + } + + info, err := os.Stat(fmt.Sprintf("%v/awesome-go/.git", workdir)) + if err != nil { + t.Errorf("Unexpected error: %v", err.Error()) + } + if !info.IsDir() { + t.Errorf("Expected that the selected path is dir") + } + + if cloneStatus.Changed != true { + t.Errorf("Expected that the status is changed") + } + + if cloneStatus.Message != syncCloned { + t.Errorf("Expected to get %v, instead of this got %v", syncCloned, cloneStatus.Message) + } + + // Fetch part + go sync.Command(cfg, ch) + + fetchStatus := <-ch + + if fetchStatus.Error { + t.Errorf("Unexpected error: %v", err.Error()) + } + + if fetchStatus.Changed != false { + t.Errorf("Expected that the status is not changed") + } + + if fetchStatus.Message != syncUpToDate { + t.Errorf("Expected to get %v, instead of this got %v", syncUpToDate, cloneStatus.Message) + } +} diff --git a/config/cmd.go b/config/cmd.go new file mode 100644 index 0000000..00f50f5 --- /dev/null +++ b/config/cmd.go @@ -0,0 +1,54 @@ +package config + +import ( + "errors" + "fmt" + "os" + + "github.com/akamensky/argparse" +) + +const ( + defaultConfigPath = ".config/grm/config.yaml" +) + +func getDefaultConfigDir() string { + // Only systems like Unix, Linux, and Windows systems are supported + userHomeDir, _ := os.UserHomeDir() + return fmt.Sprintf("%v/%v", userHomeDir, defaultConfigPath) +} + +func ParseCliArguments(name, description string, arguments []string) (CliArguments, error) { + + parser := argparse.NewParser(name, description) + + syncCMD := parser.NewCommand("sync", "Synchronize repositories with remote branches, if the repository does not exist, clone it. (If pulling is not possible, the repository will be fetched)") + + configFile := parser.String("c", "config-file", &argparse.Options{ + Default: getDefaultConfigDir(), + Help: "Path to the configuration file", + }) + + version := parser.Flag("v", "version", &argparse.Options{ + Default: false, + Help: "Print version", + }) + + color := parser.Flag("", "no-color", &argparse.Options{ + Default: false, + Help: "Turn off color printing", + }) + if err := parser.Parse(arguments); err != nil { + return CliArguments{}, err + } + if !syncCMD.Happened() && !(*version) { + return CliArguments{}, errors.New(errNoCommand) + } + + return CliArguments{ + ConfigurationFile: *configFile, + Sync: syncCMD.Happened(), + Version: *version, + Color: !(*color), + }, nil +} diff --git a/config/cmd_test.go b/config/cmd_test.go new file mode 100644 index 0000000..457b510 --- /dev/null +++ b/config/cmd_test.go @@ -0,0 +1,51 @@ +package config + +import ( + "fmt" + "os" + "testing" +) + +func TestGetDefaultConfigDir(t *testing.T) { + ud, _ := os.UserHomeDir() + + desiredPath := fmt.Sprintf("%v/%v", ud, defaultConfigPath) + + if getDefaultConfigDir() != desiredPath { + t.Errorf("Expected to get %v, instead of this got %v", desiredPath, getDefaultConfigDir()) + } + +} + +func TestParsingDefaultArguments(t *testing.T) { + + // First item in os.Args is appPath, this have to be mocked + fakeOSArgs := []string{"appName", "sync"} + + parseResult, err := ParseCliArguments("", "", fakeOSArgs) + + if err != nil { + t.Errorf("Unexpected error %v", err.Error()) + } + + if parseResult.ConfigurationFile != getDefaultConfigDir() { + t.Errorf("Default value for configurationFile should be %v, instead of this got %v", getDefaultConfigDir(), parseResult.ConfigurationFile) + } + + if parseResult.Sync != true { + t.Errorf("Default value for configurationFile should be %v, instead of this got %v", true, parseResult.Sync) + } +} + +func TestParsingWithoutCommand(t *testing.T) { + + // First item in os.Args is appPath, this have to be mocked + fakeOSArgs := []string{"appName"} + + results, err := ParseCliArguments("", "", fakeOSArgs) + + if err == nil { + t.Errorf("Expected error, not results: %v", results) + } + +} diff --git a/config/config_file_parse.go b/config/config_file_parse.go new file mode 100644 index 0000000..270b49c --- /dev/null +++ b/config/config_file_parse.go @@ -0,0 +1,66 @@ +package config + +import ( + "errors" + "fmt" + "strings" + + "gopkg.in/yaml.v3" +) + +func GetRepositoryConfig(data []byte, fileExtension string) (Configuration, error) { + + var config Configuration + var err error + + switch fileExtension { + case "yaml": + err = yaml.Unmarshal(data, &config) + default: + return Configuration{}, errors.New(errNotSupportedType) + } + + if err != nil { + return Configuration{}, err + } + + if config.Workspace == "" { + + return Configuration{}, errors.New(errMissingWorkspaceField) + } + // Get counters to check if name or dest values are not duplicated + nameFieldCounter := make(map[string][]int) + destFieldCounter := make(map[string][]int) + + for index, repo := range config.Repositories { + if repo.Src == "" { + errorMessage := fmt.Sprintf(errMissingSrcField, index) + return Configuration{}, errors.New(errorMessage) + } + if repo.Name == "" { + splittedGit := strings.Split(repo.Src, "/") + nameWithExcention := splittedGit[len(splittedGit)-1] + name := strings.Split(nameWithExcention, ".")[0] + config.Repositories[index].Name = name + } + if repo.Dest == "" { + config.Repositories[index].Dest = config.Repositories[index].Name + } + nameFieldCounter[config.Repositories[index].Name] = append(nameFieldCounter[config.Repositories[index].Name], index) + destFieldCounter[config.Repositories[index].Dest] = append(destFieldCounter[config.Repositories[index].Dest], index) + } + + for rowId, items := range nameFieldCounter { + if len(items) != 1 { + return Configuration{}, getDuplicateFieldError("name", rowId, items) + } + } + + for rowId, items := range destFieldCounter { + if len(items) != 1 { + return Configuration{}, getDuplicateFieldError("dest", rowId, items) + } + } + + return config, err +} diff --git a/config/config_file_parse_test.go b/config/config_file_parse_test.go new file mode 100644 index 0000000..b334c4c --- /dev/null +++ b/config/config_file_parse_test.go @@ -0,0 +1,150 @@ +package config + +import ( + "fmt" + "reflect" + "testing" +) + +var exampleYamlConfig = []byte(` +workspace: /tmp +repositories: + - src: "https://github.com/example/example.git" + dest: "example/path" + name: "custom_example" + - src: https://github.com/example/example2.git +`) + +var destinationConfiguration = Configuration{ + Workspace: "/tmp", + Repositories: []RepositoryConfig{ + { + Name: "custom_example", + Dest: "example/path", + Src: "https://github.com/example/example.git", + }, + { + Name: "example2", + Src: "https://github.com/example/example2.git", + Dest: "example2", + }, + }, +} + +func TestNotSupportedFileExcension(t *testing.T) { + _, err := GetRepositoryConfig(exampleYamlConfig, "custom") + if err == nil { + t.Error("Expected to get error") + } + if err.Error() != errNotSupportedType { + t.Errorf("Expected to get %v, instead of this got %v", errNotSupportedType, err.Error()) + } +} + +func TestGetRepositoryConfigFromYaml(t *testing.T) { + + result, err := GetRepositoryConfig(exampleYamlConfig, "yaml") + + if err != nil { + t.Errorf("Unexpected error %v", err.Error()) + } + + if !reflect.DeepEqual(result.Repositories, destinationConfiguration.Repositories) { + t.Errorf("Default value for configurationFile should be:\n %v \ninstead of this got:\n %v", result, destinationConfiguration) + } +} + +func TestWrongYamlFormat(t *testing.T) { + exampleWrongYamlConfig := []byte(`--- + workspace: "/test" +repositories: + - src: "https://github.com/example/example.git" + dest: "example/path" + name: "custom_example" +`) + + _, err := GetRepositoryConfig(exampleWrongYamlConfig, "yaml") + expectedError := "yaml: line 2: found character that cannot start any token" + if err.Error() != expectedError { + t.Errorf("Expected to get error with value %v, instead of this got: %v", expectedError, err.Error()) + } +} + +func TestMissingWorkspaceRequiredField(t *testing.T) { + exampleWrongYamlConfig := []byte(`--- +repositories: + - src: "https://github.com/example/example.git" + dest: "example/path" + name: "custom_example" +`) + + _, err := GetRepositoryConfig(exampleWrongYamlConfig, "yaml") + + if err.Error() != errMissingWorkspaceField { + t.Errorf("Expected to get error with value %v, instead of this got: %v", errMissingWorkspaceField, err.Error()) + } +} + +func TestMissingSourceRequiredField(t *testing.T) { + exampleWrongYamlConfig := []byte(`--- +workspace: /tmp +repositories: + - dest: "example/path" + name: "custom_example" +`) + _, err := GetRepositoryConfig(exampleWrongYamlConfig, "yaml") + + expectedError := fmt.Sprintf(errMissingSrcField, 0) + + if err.Error() != expectedError { + t.Errorf("Expected to get error with value %v, instead of this got: %v", expectedError, err.Error()) + } +} + +func TestDuplicatedNameField(t *testing.T) { + exampleWrongYamlConfig := []byte(` +workspace: "/tmp" +repositories: + - src: "https://github.com/example/example1.git" + dest: "example/path" + name: "custom_example" + - src: "https://github.com/example/example2.git" + name: "example2" + - src: "https://github.com/example/example2.git" +`) + result, err := GetRepositoryConfig(exampleWrongYamlConfig, "yaml") + + if err == nil { + t.Errorf("Unexpected result: %v", result) + + } + expectedError := getDuplicateFieldError("name", "example2", []int{1, 2}) + + if err.Error() != expectedError.Error() { + t.Errorf("Expected to get error with value %v, instead of this got: %v", expectedError.Error(), err.Error()) + } +} + +func TestDuplicatedDestField(t *testing.T) { + exampleWrongYamlConfig := []byte(` +workspace: "/tmp" +repositories: + - src: "https://github.com/example/example1.git" + dest: "example/path" + - src: "https://github.com/example/example2.git" + dest: "example" + - src: "https://github.com/example/example3.git" + dest: "example" +`) + result, err := GetRepositoryConfig(exampleWrongYamlConfig, "yaml") + + if err == nil { + t.Errorf("Unexpected result: %v", result) + } + + expectedError := getDuplicateFieldError("dest", "example", []int{1, 2}) + + if err.Error() != expectedError.Error() { + t.Errorf("Expected to get error with value \"%v\", instead of this got: \"%v\"", expectedError, err) + } +} diff --git a/config/errors.go b/config/errors.go new file mode 100644 index 0000000..df68675 --- /dev/null +++ b/config/errors.go @@ -0,0 +1,27 @@ +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" +) + +func getDuplicateFieldError(field string, name string, rows []int) error { + + var rowsInString []string + for _, row := range rows { + 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) +} diff --git a/config/structures.go b/config/structures.go new file mode 100644 index 0000000..92f710e --- /dev/null +++ b/config/structures.go @@ -0,0 +1,19 @@ +package config + +type Configuration struct { + Workspace string + Repositories []RepositoryConfig +} + +type RepositoryConfig struct { + Name string `yaml:",omitempty"` + Src string `yaml:",omitempty"` + Dest string `yaml:",omitempty"` +} + +type CliArguments struct { + ConfigurationFile string + Sync bool + Version bool + Color bool +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1c858ea --- /dev/null +++ b/go.mod @@ -0,0 +1,30 @@ +module gitlab.com/revalus/grm + +go 1.17 + +require ( + github.com/akamensky/argparse v1.3.1 + github.com/go-git/go-git/v5 v5.4.2 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b +) + +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/go-git/go-billy/v5 v5.3.1 // indirect + github.com/google/go-cmp v0.5.6 // indirect + github.com/imdario/mergo v0.3.12 // 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/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 + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..91471eb --- /dev/null +++ b/go.sum @@ -0,0 +1,124 @@ +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= +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/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/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/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/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/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/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/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= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +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/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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/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= +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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..80af623 --- /dev/null +++ b/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "os" + + "gitlab.com/revalus/grm/app" +) + +const () + +func main() { + + app := app.GitRepositoryManager{} + app.Parse(os.Args) + app.Run() +}