Add sync command
This commit is contained in:
commit
5e6a5e23fd
39
README.md
Normal file
39
README.md
Normal file
@ -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
|
89
app/app.go
Normal file
89
app/app.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
94
app/app_test.go
Normal file
94
app/app_test.go
Normal file
@ -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
|
||||||
|
}
|
61
app/console_output.go
Normal file
61
app/console_output.go
Normal file
@ -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)
|
||||||
|
}
|
25
app/utils.go
Normal file
25
app/utils.go
Normal file
@ -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
|
||||||
|
}
|
36
app/utils_test.go
Normal file
36
app/utils_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
13
commands/command.go
Normal file
13
commands/command.go
Normal file
@ -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
|
||||||
|
}
|
88
commands/sync_cmd.go
Normal file
88
commands/sync_cmd.go
Normal file
@ -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
|
||||||
|
|
||||||
|
}
|
95
commands/sync_cmd_test.go
Normal file
95
commands/sync_cmd_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
54
config/cmd.go
Normal file
54
config/cmd.go
Normal file
@ -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
|
||||||
|
}
|
51
config/cmd_test.go
Normal file
51
config/cmd_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
66
config/config_file_parse.go
Normal file
66
config/config_file_parse.go
Normal file
@ -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
|
||||||
|
}
|
150
config/config_file_parse_test.go
Normal file
150
config/config_file_parse_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
27
config/errors.go
Normal file
27
config/errors.go
Normal file
@ -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)
|
||||||
|
}
|
19
config/structures.go
Normal file
19
config/structures.go
Normal file
@ -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
|
||||||
|
}
|
30
go.mod
Normal file
30
go.mod
Normal file
@ -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
|
||||||
|
)
|
124
go.sum
Normal file
124
go.sum
Normal file
@ -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=
|
Loading…
Reference in New Issue
Block a user