diff --git a/Makefile b/Makefile index 1dfdc73e..0f4933b6 100644 --- a/Makefile +++ b/Makefile @@ -189,6 +189,7 @@ endif ifneq ($(HAS_WEBAPP),) cd webapp && $(NPM) run test; endif + cd ./build/sync && $(GO) test -v $(GO_TEST_FLAGS) ./... ## Creates a coverage report for the server code. .PHONY: coverage diff --git a/build/sync/README.md b/build/sync/README.md new file mode 100644 index 00000000..34c834f2 --- /dev/null +++ b/build/sync/README.md @@ -0,0 +1,113 @@ +sync +==== + +The sync tool is a proof-of-concept implementation of a tool for synchronizing mattermost plugin +repositories with the mattermost-plugin-starter-template repo. + +Overview +-------- + +At its core the tool is just a collection of checks and actions that are executed according to a +synchronization plan (see [./build/sync/plan.yml](https://github.com/mattermost/mattermost-plugin-starter-template/blob/sync/build/sync/plan.yml) +for an example). The plan defines a set of files +and/or directories that need to be kept in sync between the plugin repository and the template (this +repo). + +For each set of paths, a set of actions to be performed is outlined. No more than one action of that set +will be executed - the first one whose checks pass. Other actions are meant to act as fallbacks. +The idea is to be able to e.g. overwrite a file if it has no local changes or apply a format-specific +merge algorithm otherwise. + +Before running each action, the tool will check if any checks are defined for that action. If there +are any, they will be executed and their results examined. If all checks pass, the action will be executed. +If there is a check failure, the tool will locate the next applicable action according to the plan and +start over with it. + +The synchronization plan can also run checks before running any actions, e.g. to check if the source and +target worktrees are clean. + +Running +------- + +The tool can be executed from the root of this repository with a command: +``` +$ go run ./build/sync/main.go ./build/sync/plan.yml ../mattermost-plugin-github +``` + +(assuming `mattermost-plugin-github` is the target repository we want to synchronize with the source). + +plan.yml +--------- + +The `plan.yml` file (located in `build/sync/plan.yml`) consists of two parts: + - checks + - actions + +The `checks` section defines tests to run before executing the plan itself. Currently the only available such check is `repo_is_clean` defined as: +``` +type: repo_is_clean +params: + repo: source +``` +The `repo` parameter takes one of two values: +- `source` - the `mattermost-plugin-starter-template` repository +- `target` - the repository of the plugin being updated. + +The `actions` section defines actions to be run as part of the synchronization. +Each entry in this section has the form: +``` +paths: + - path1 + - path2 +actions: + - type: action_type + params: + action_parameter: value + conditions: + - type: check_type + params: + check_parameter: value +``` + +`paths` is a list of file or directory paths (relative to the root of the repository) +synchronization should be performed on. + +Each action in the `actions` section is defined by its type. Currently supported action types are: + - `overwrite_file` - overwrite the specified file in the `target` repository with the file in the `source` repository. + - `overwrite_directory` - overwrite a directory. + +Both actions accept a parameter called `create` which determines if the file or directory should be created if it does not exist in the target repository. + +The `conditions` part of an action definition defines tests that need to pass for the +action to be run. Available checks are: + - `exists` + - `file_unaltered` + +The `exists` check takes a single parameter - `repo` (referencing either the source or target repository) and it passes only if the file or directory the action is about to be run on exists. If the repo parameter is not specified, it will default to `target`. + +The `file_unaltered` check is only applicable to file paths. It passes if the file +has not been altered - i.e. it is identical to some version of that same file in the reference repository (usually `source`). This check takes two parameters: + - `in` - repository to check the file in, default `target` + - `compared-to` - repository to check the file against, default `source`. + +When multiple actions are specified for a set of paths, the `sync` tool will only +execute a single action for each path. The first action in the list, whose conditions +are all satisfied will be executed. + +If an acton fails due to an error, the synchronization run will be aborted. + +Caveat emptor +------------- + +This is a very basic proof-of-concept and there are many things that should be improved/implemented: +(in no specific order) + + 1. Format-specific merge actions for `go.mod`, `go.sum`, `webapp/package.json` and other files should + be implemented. + 2. Better logging should be implemented. + 3. Handling action dependencies should be investigated. + e.g. if the `build` directory is overwritten, that will in some cases mean that the go.mod file also needs + to be updated. + 4. Storing the tree-hash of the template repository that the plugin was synchronized with would allow + improving the performance of the tool by restricting the search space when examining if a file + has been altered in the plugin repository. diff --git a/build/sync/main.go b/build/sync/main.go new file mode 100644 index 00000000..f03f9a8c --- /dev/null +++ b/build/sync/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "sigs.k8s.io/yaml" + + "github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan" +) + +func main() { + verbose := flag.Bool("verbose", false, "enable verbose output") + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Update a pluging directory with /mattermost-plugin-starter-template/.\n") + fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0]) + fmt.Fprintf(flag.CommandLine.Output(), "%s \n", os.Args[0]) + flag.PrintDefaults() + } + + flag.Parse() + // TODO: implement proper command line parameter parsing. + if len(os.Args) != 3 { + fmt.Fprintf(os.Stderr, "running: \n $ sync [plan.yaml] [plugin path]\n") + os.Exit(1) + } + + syncPlan, err := readPlan(os.Args[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "coud not load plan: %s\n", err) + os.Exit(1) + } + + srcDir, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to get current directory: %s\n", err) + os.Exit(1) + } + + trgDir, err := filepath.Abs(os.Args[2]) + if err != nil { + fmt.Fprintf(os.Stderr, "could not determine target directory: %s\n", err) + os.Exit(1) + } + + srcRepo, err := plan.GetRepoSetup(srcDir) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + trgRepo, err := plan.GetRepoSetup(trgDir) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + + planSetup := plan.Setup{ + Source: srcRepo, + Target: trgRepo, + VerboseLogging: *verbose, + } + err = syncPlan.Execute(planSetup) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } +} + +func readPlan(path string) (*plan.Plan, error) { + raw, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read plan file %q: %v", path, err) + } + + var p plan.Plan + err = yaml.Unmarshal(raw, &p) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal plan yaml: %v", err) + } + + return &p, err +} diff --git a/build/sync/plan.yml b/build/sync/plan.yml new file mode 100644 index 00000000..ea33a365 --- /dev/null +++ b/build/sync/plan.yml @@ -0,0 +1,44 @@ +checks: + - type: repo_is_clean + params: + repo: source + - type: repo_is_clean + params: + repo: target +actions: + - paths: + - build + actions: + - type: overwrite_directory + params: + create: true + - paths: + - .editorconfig + actions: + - type: overwrite_file + params: + create: true + conditions: + - type: file_unaltered + - paths: + - Makefile + actions: + - type: overwrite_file + params: + create: true + - paths: + - .circleci/config.yml + - server/configuration.go + - server/plugin.go + - server/plugin_test.go + - webapp/.eslintrc.json + - webapp/babel.config.js + - webapp/package.json + - webapp/src/index.js + - webapp/tsconfig.json + - webapp/webpack.config.js + actions: + - type: overwrite_file + conditions: + - type: exists + - type: file_unaltered diff --git a/build/sync/plan/actions.go b/build/sync/plan/actions.go new file mode 100644 index 00000000..0f08c73d --- /dev/null +++ b/build/sync/plan/actions.go @@ -0,0 +1,214 @@ +package plan + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +// ActionConditions adds condition support to actions. +type ActionConditions struct { + // Conditions are checkers run before executing the + // action. If any one fails (returns an error), the action + // itself is not executed. + Conditions []Check +} + +// Check runs the conditions associated with the action and returns +// the first error (if any). +func (c ActionConditions) Check(path string, setup Setup) error { + if len(c.Conditions) > 0 { + setup.Logf("checking action conditions") + } + for _, condition := range c.Conditions { + err := condition.Check(path, setup) + if err != nil { + return err + } + } + return nil +} + +// OverwriteFileAction is used to overwrite a file. +type OverwriteFileAction struct { + ActionConditions + Params struct { + // Create determines whether the target directory + // will be created if it does not exist. + Create bool `json:"create"` + } +} + +// Run implements plan.Action.Run. +func (a OverwriteFileAction) Run(path string, setup Setup) error { + setup.Logf("overwriting file %q", path) + src := setup.PathInRepo(SourceRepo, path) + dst := setup.PathInRepo(TargetRepo, path) + + dstInfo, err := os.Stat(dst) + switch { + case os.IsNotExist(err): + if !a.Params.Create { + return fmt.Errorf("path %q does not exist, not creating", dst) + } + case err != nil: + return fmt.Errorf("failed to check path %q: %v", dst, err) + case dstInfo.IsDir(): + return fmt.Errorf("path %q is a directory", dst) + } + + srcInfo, err := os.Stat(src) + if os.IsNotExist(err) { + return fmt.Errorf("file %q does not exist", src) + } else if err != nil { + return fmt.Errorf("failed to check path %q: %v", src, err) + } + if srcInfo.IsDir() { + return fmt.Errorf("path %q is a directory", src) + } + + srcF, err := os.Open(src) + if err != nil { + return fmt.Errorf("failed to open %q: %v", src, err) + } + defer srcF.Close() + dstF, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, srcInfo.Mode()) + if err != nil { + return fmt.Errorf("failed to open %q: %v", src, err) + } + defer dstF.Close() + _, err = io.Copy(dstF, srcF) + if err != nil { + return fmt.Errorf("failed to copy file %q: %v", path, err) + } + return nil +} + +// OverwriteDirectoryAction is used to completely overwrite directories. +// If the target directory exists, it will be removed first. +type OverwriteDirectoryAction struct { + ActionConditions + Params struct { + // Create determines whether the target directory + // will be created if it does not exist. + Create bool `json:"create"` + } +} + +// Run implements plan.Action.Run. +func (a OverwriteDirectoryAction) Run(path string, setup Setup) error { + setup.Logf("overwriting directory %q", path) + src := setup.PathInRepo(SourceRepo, path) + dst := setup.PathInRepo(TargetRepo, path) + + dstInfo, err := os.Stat(dst) + switch { + case os.IsNotExist(err): + if !a.Params.Create { + return fmt.Errorf("path %q does not exist, not creating", dst) + } + case err != nil: + return fmt.Errorf("failed to check path %q: %v", dst, err) + default: + if !dstInfo.IsDir() { + return fmt.Errorf("path %q is not a directory", dst) + } + err = os.RemoveAll(dst) + if err != nil { + return fmt.Errorf("failed to remove directory %q: %v", dst, err) + } + } + + srcInfo, err := os.Stat(src) + if os.IsNotExist(err) { + return fmt.Errorf("directory %q does not exist", src) + } else if err != nil { + return fmt.Errorf("failed to check path %q: %v", src, err) + } + if !srcInfo.IsDir() { + return fmt.Errorf("path %q is not a directory", src) + } + + err = CopyDirectory(src, dst) + if err != nil { + return fmt.Errorf("failed to copy path %q: %v", path, err) + } + return nil +} + +// CopyDirectory copies the directory src to dst so that after +// a successful operation the contents of src and dst are equal. +func CopyDirectory(src, dst string) error { + copier := dirCopier{dst: dst, src: src} + return filepath.Walk(src, copier.Copy) +} + +type dirCopier struct { + dst string + src string +} + +// Convert a path in the source directory to a path in the destination +// directory. +func (d dirCopier) srcToDst(path string) (string, error) { + suff := strings.TrimPrefix(path, d.src) + if suff == path { + return "", fmt.Errorf("path %q is not in %q", path, d.src) + } + return filepath.Join(d.dst, suff), nil +} + +// Copy is an implementation of filepatch.WalkFunc that copies the +// source directory to target with all subdirectories. +func (d dirCopier) Copy(srcPath string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("failed to copy directory: %v", err) + } + trgPath, err := d.srcToDst(srcPath) + if err != nil { + return err + } + if info.IsDir() { + err = os.MkdirAll(trgPath, info.Mode()) + if err != nil { + return fmt.Errorf("failed to create directory %q: %v", trgPath, err) + } + err = os.Chtimes(trgPath, info.ModTime(), info.ModTime()) + if err != nil { + return fmt.Errorf("failed to create directory %q: %v", trgPath, err) + } + return nil + } + err = copyFile(srcPath, trgPath, info) + if err != nil { + return fmt.Errorf("failed to copy file %q: %v", srcPath, err) + } + return nil +} + +func copyFile(src, dst string, info os.FileInfo) error { + srcF, err := os.Open(src) + if err != nil { + return fmt.Errorf("failed to open source file %q: %v", src, err) + } + defer srcF.Close() + dstF, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, info.Mode()) + if err != nil { + return fmt.Errorf("failed to open destination file %q: %v", dst, err) + } + _, err = io.Copy(dstF, srcF) + if err != nil { + dstF.Close() + return fmt.Errorf("failed to copy file %q: %v", src, err) + } + if err = dstF.Close(); err != nil { + return fmt.Errorf("failed to close file %q: %v", dst, err) + } + err = os.Chtimes(dst, info.ModTime(), info.ModTime()) + if err != nil { + return fmt.Errorf("failed to adjust file modification time for %q: %v", dst, err) + } + return nil +} diff --git a/build/sync/plan/actions_test.go b/build/sync/plan/actions_test.go new file mode 100644 index 00000000..94915a26 --- /dev/null +++ b/build/sync/plan/actions_test.go @@ -0,0 +1,107 @@ +package plan_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan" +) + +func TestCopyDirectory(t *testing.T) { + assert := assert.New(t) + + // Create a temporary directory to copy to. + dir, err := ioutil.TempDir("", "test") + assert.Nil(err) + defer os.RemoveAll(dir) + + wd, err := os.Getwd() + assert.Nil(err) + + srcDir := filepath.Join(wd, "testdata") + err = plan.CopyDirectory(srcDir, dir) + assert.Nil(err) + + compareDirectories(assert, dir, srcDir) +} + +func TestOverwriteFileAction(t *testing.T) { + assert := assert.New(t) + + // Create a temporary directory to copy to. + dir, err := ioutil.TempDir("", "test") + assert.Nil(err) + defer os.RemoveAll(dir) + + wd, err := os.Getwd() + assert.Nil(err) + + setup := plan.Setup{ + Source: plan.RepoSetup{ + Git: nil, + Path: filepath.Join(wd, "testdata", "b"), + }, + Target: plan.RepoSetup{ + Git: nil, + Path: dir, + }, + } + action := plan.OverwriteFileAction{} + action.Params.Create = true + err = action.Run("c", setup) + assert.Nil(err) + + compareDirectories(assert, dir, filepath.Join(wd, "testdata", "b")) +} + +func TestOverwriteDirectoryAction(t *testing.T) { + assert := assert.New(t) + + // Create a temporary directory to copy to. + dir, err := ioutil.TempDir("", "test") + assert.Nil(err) + defer os.RemoveAll(dir) + + wd, err := os.Getwd() + assert.Nil(err) + + setup := plan.Setup{ + Source: plan.RepoSetup{ + Git: nil, + Path: wd, + }, + Target: plan.RepoSetup{ + Git: nil, + Path: dir, + }, + } + action := plan.OverwriteDirectoryAction{} + action.Params.Create = true + err = action.Run("testdata", setup) + assert.Nil(err) + + destDir := filepath.Join(dir, "testdata") + srcDir := filepath.Join(wd, "testdata") + compareDirectories(assert, destDir, srcDir) +} + +func compareDirectories(assert *assert.Assertions, pathA, pathB string) { + aContents, err := ioutil.ReadDir(pathA) + assert.Nil(err) + bContents, err := ioutil.ReadDir(pathB) + assert.Nil(err) + assert.Len(aContents, len(bContents)) + + // Check the directory contents are equal. + for i, aFInfo := range aContents { + bFInfo := bContents[i] + assert.Equal(aFInfo.Name(), bFInfo.Name()) + assert.Equal(aFInfo.Size(), bFInfo.Size()) + assert.Equal(aFInfo.Mode(), bFInfo.Mode()) + assert.Equal(aFInfo.IsDir(), bFInfo.IsDir()) + } +} diff --git a/build/sync/plan/checks.go b/build/sync/plan/checks.go new file mode 100644 index 00000000..ea8325f9 --- /dev/null +++ b/build/sync/plan/checks.go @@ -0,0 +1,147 @@ +package plan + +import ( + "fmt" + "os" + "sort" + + "github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan/git" +) + +// CheckFail is a custom error type used to indicate a +// check that did not pass (but did not fail due to external +// causes. +// Use `IsCheckFail` to check if an error is a check failure. +type CheckFail string + +func (e CheckFail) Error() string { + return string(e) +} + +// CheckFailf creates an error with the specified message string. +// The error will pass the IsCheckFail filter. +func CheckFailf(msg string, args ...interface{}) CheckFail { + if len(args) > 0 { + msg = fmt.Sprintf(msg, args...) + } + return CheckFail(msg) +} + +// IsCheckFail determines if an error is a check fail error. +func IsCheckFail(err error) bool { + if err == nil { + return false + } + _, ok := err.(CheckFail) + return ok +} + +// RepoIsCleanChecker checks whether the git repository is clean. +type RepoIsCleanChecker struct { + Params struct { + Repo RepoID + } +} + +// Check implements the Checker interface. +// The path parameter is ignored because this checker checks the state of a repository. +func (r RepoIsCleanChecker) Check(_ string, ctx Setup) error { + ctx.Logf("checking if repository %q is clean", r.Params.Repo) + rc := ctx.GetRepo(r.Params.Repo) + repo := rc.Git + worktree, err := repo.Worktree() + if err != nil { + return fmt.Errorf("failed to get worktree: %v", err) + } + status, err := worktree.Status() + if err != nil { + return fmt.Errorf("failed to get worktree status: %v", err) + } + if !status.IsClean() { + return CheckFailf("%q repository is not clean", r.Params.Repo) + } + return nil +} + +// PathExistsChecker checks whether the fle or directory with the +// path exists. If it does not, an error is returned. +type PathExistsChecker struct { + Params struct { + Repo RepoID + } +} + +// Check implements the Checker interface. +func (r PathExistsChecker) Check(path string, ctx Setup) error { + repo := r.Params.Repo + if repo == "" { + repo = TargetRepo + } + ctx.Logf("checking if path %q exists in repo %q", path, repo) + absPath := ctx.PathInRepo(repo, path) + _, err := os.Stat(absPath) + if os.IsNotExist(err) { + return CheckFailf("path %q does not exist", path) + } else if err != nil { + return fmt.Errorf("failed to stat path %q: %v", absPath, err) + } + return nil +} + +// FileUnalteredChecker checks whether the file in Repo is +// an unaltered version of that same file in ReferenceRepo. +// +// Its purpose is to check that a file has not been changed after forking a repository. +// It could be an old unaltered version, so the git history of the file is traversed +// until a matching version is found. +// +// If the repositories in the parameters are not specified, +// reference will default to the source repository and repo - to the target. +type FileUnalteredChecker struct { + Params struct { + ReferenceRepo RepoID `json:"compared-to"` + Repo RepoID `json:"in"` + } +} + +// Check implements the Checker interface. +func (f FileUnalteredChecker) Check(path string, setup Setup) error { + setup.Logf("checking if file %q has not been altered", path) + repo := f.Params.Repo + if repo == "" { + repo = TargetRepo + } + reference := f.Params.ReferenceRepo + if reference == "" { + reference = SourceRepo + } + absPath := setup.PathInRepo(repo, path) + + info, err := os.Stat(absPath) + if os.IsNotExist(err) { + return CheckFailf("file %q has been deleted", absPath) + } + if err != nil { + return fmt.Errorf("failed to get stat for %q: %v", absPath, err) + } + if info.IsDir() { + return fmt.Errorf("%q is a directory", absPath) + } + + fileHashes, err := git.FileHistory(path, setup.GetRepo(reference).Git) + if err != nil { + return err + } + + currentHash, err := git.GetFileHash(absPath) + if err != nil { + return err + } + + sort.Strings(fileHashes) + idx := sort.SearchStrings(fileHashes, currentHash) + if idx < len(fileHashes) && fileHashes[idx] == currentHash { + return nil + } + return CheckFailf("file %q has been altered", absPath) +} diff --git a/build/sync/plan/checks_test.go b/build/sync/plan/checks_test.go new file mode 100644 index 00000000..3c27de14 --- /dev/null +++ b/build/sync/plan/checks_test.go @@ -0,0 +1,122 @@ +package plan_test + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + git "gopkg.in/src-d/go-git.v4" + + "github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan" +) + +// Tests for the RepoIsClean checker. +func TestRepoIsCleanChecker(t *testing.T) { + assert := assert.New(t) + + // Create a git repository in a temporary dir. + dir, err := ioutil.TempDir("", "test") + assert.Nil(err) + defer os.RemoveAll(dir) + repo, err := git.PlainInit(dir, false) + assert.Nil(err) + + // Repo should be clean. + checker := plan.RepoIsCleanChecker{} + checker.Params.Repo = plan.TargetRepo + + ctx := plan.Setup{ + Target: plan.RepoSetup{ + Path: dir, + Git: repo, + }, + } + assert.Nil(checker.Check("", ctx)) + + // Create a file in the repository. + err = ioutil.WriteFile(path.Join(dir, "data.txt"), []byte("lorem ipsum"), 0666) + assert.Nil(err) + err = checker.Check("", ctx) + assert.EqualError(err, "\"target\" repository is not clean") + assert.True(plan.IsCheckFail(err)) +} + +func TestPathExistsChecker(t *testing.T) { + assert := assert.New(t) + + wd, err := os.Getwd() + assert.Nil(err) + + checker := plan.PathExistsChecker{} + checker.Params.Repo = plan.SourceRepo + + ctx := plan.Setup{ + Source: plan.RepoSetup{ + Path: wd, + }, + } + + // Check with existing directory. + assert.Nil(checker.Check("testdata", ctx)) + + // Check with existing file. + assert.Nil(checker.Check("testdata/a", ctx)) + + err = checker.Check("nosuchpath", ctx) + assert.NotNil(err) + assert.True(plan.IsCheckFail(err)) +} + +func TestUnalteredChecker(t *testing.T) { + assert := assert.New(t) + + // Path to the root of the repo. + wd, err := filepath.Abs("../../../") + assert.Nil(err) + + gitRepo, err := git.PlainOpen(wd) + assert.Nil(err) + + ctx := plan.Setup{ + Source: plan.RepoSetup{ + Path: wd, + Git: gitRepo, + }, + Target: plan.RepoSetup{ + Path: wd, + }, + } + + checker := plan.FileUnalteredChecker{} + checker.Params.ReferenceRepo = plan.SourceRepo + checker.Params.Repo = plan.TargetRepo + + // Check with the same file - check should succeed + hashPath := "build/sync/plan/testdata/a" + err = checker.Check(hashPath, ctx) + assert.Nil(err) + + // Create a file with the same suffix path, but different contents. + tmpDir, err := ioutil.TempDir("", "test") + assert.Nil(err) + //defer os.RemoveAll(tmpDir) + fullPath := filepath.Join(tmpDir, "build/sync/plan/testdata") + err = os.MkdirAll(fullPath, 0777) + assert.Nil(err) + file, err := os.OpenFile(filepath.Join(fullPath, "a"), os.O_CREATE|os.O_WRONLY, 0755) + assert.Nil(err) + _, err = file.WriteString("this file has different contents") + assert.Nil(err) + assert.Nil(file.Close()) + + // Set the plugin path to the temporary directory. + ctx.Target.Path = tmpDir + + err = checker.Check(hashPath, ctx) + assert.True(plan.IsCheckFail(err)) + assert.EqualError(err, fmt.Sprintf("file %q has been altered", filepath.Join(tmpDir, hashPath))) +} diff --git a/build/sync/plan/git/file_history.go b/build/sync/plan/git/file_history.go new file mode 100644 index 00000000..929159d6 --- /dev/null +++ b/build/sync/plan/git/file_history.go @@ -0,0 +1,106 @@ +package git + +import ( + "crypto/sha1" //nolint + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + + git "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing/object" +) + +// ErrNotFound signifies the file was not found. +var ErrNotFound = fmt.Errorf("not found") + +// FileHistory will trace all the versions of a file in the git repository +// and return a list of sha1 hashes of that file. +func FileHistory(path string, repo *git.Repository) ([]string, error) { + logOpts := git.LogOptions{ + FileName: &path, + } + commits, err := repo.Log(&logOpts) + if err != nil { + return nil, fmt.Errorf("failed to get commits for path %q: %v", path, err) + } + defer commits.Close() + + hashHistory := []string{} + cerr := commits.ForEach(func(c *object.Commit) error { + root, err := repo.TreeObject(c.TreeHash) + if err != nil { + return fmt.Errorf("failed to get commit tree: %v", err) + } + f, err := traverseTree(root, path) + if err == object.ErrFileNotFound || err == object.ErrDirectoryNotFound { + // Ignoring file not found errors. + return nil + } else if err != nil { + return err + } + sum, err := getReaderHash(f) + f.Close() + if err != nil { + return err + } + hashHistory = append(hashHistory, sum) + return nil + }) + if cerr != nil && cerr != io.EOF { + return nil, cerr + } + if len(hashHistory) == 0 { + return nil, ErrNotFound + } + return hashHistory, nil +} + +func traverseTree(root *object.Tree, path string) (io.ReadCloser, error) { + dirName, fileName := filepath.Split(path) + var err error + t := root + if dirName != "" { + t, err = root.Tree(filepath.Clean(dirName)) + if err == object.ErrDirectoryNotFound { + return nil, err + } else if err != nil { + return nil, fmt.Errorf("failed to traverse tree to %q: %v", dirName, err) + } + } + f, err := t.File(fileName) + if err == object.ErrFileNotFound { + return nil, err + } else if err != nil { + return nil, fmt.Errorf("failed to lookup file %q: %v", fileName, err) + } + reader, err := f.Reader() + if err != nil { + return nil, fmt.Errorf("failed to open %q: %v", path, err) + } + return reader, nil +} + +func getReaderHash(r io.Reader) (string, error) { + h := sha1.New() // nolint + _, err := io.Copy(h, r) + if err != nil { + return "", err + } + return hex.EncodeToString(h.Sum(nil)), nil +} + +// GetFileHash calculates the sha1 hash sum of the file. +func GetFileHash(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + sum, err := getReaderHash(f) + if err != nil { + return "", err + } + return sum, nil +} diff --git a/build/sync/plan/git/file_history_test.go b/build/sync/plan/git/file_history_test.go new file mode 100644 index 00000000..b8922e30 --- /dev/null +++ b/build/sync/plan/git/file_history_test.go @@ -0,0 +1,27 @@ +package git_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + git "gopkg.in/src-d/go-git.v4" + + gitutil "github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan/git" +) + +func TestFileHistory(t *testing.T) { + assert := assert.New(t) + + repo, err := git.PlainOpenWithOptions("./", &git.PlainOpenOptions{ + DetectDotGit: true, + }) + assert.Nil(err) + sums, err := gitutil.FileHistory("build/sync/plan/git/testdata/testfile.txt", repo) + assert.Nil(err) + assert.Equal([]string{"ba7192052d7cf77c55d3b7bf40b350b8431b208b"}, sums) + + // Calling with a non-existent file returns error. + sums, err = gitutil.FileHistory("build/sync/plan/git/testdata/nosuch_testfile.txt", repo) + assert.Equal(gitutil.ErrNotFound, err) + assert.Nil(sums) +} diff --git a/build/sync/plan/git/testdata/testfile.txt b/build/sync/plan/git/testdata/testfile.txt new file mode 100644 index 00000000..039983dd --- /dev/null +++ b/build/sync/plan/git/testdata/testfile.txt @@ -0,0 +1 @@ +This file is used to test file history tracking. \ No newline at end of file diff --git a/build/sync/plan/plan.go b/build/sync/plan/plan.go new file mode 100644 index 00000000..794c775e --- /dev/null +++ b/build/sync/plan/plan.go @@ -0,0 +1,245 @@ +// Package plan handles the synchronization plan. +// +// Each synchronization plan is a set of checks and actions to perform on specified paths +// that will result in the "plugin" repository being updated. +package plan + +import ( + "encoding/json" + "fmt" + "os" + "sort" +) + +// Plan defines the plan for synchronizing a target and a source directory. +type Plan struct { + Checks []Check `json:"checks"` + // Each set of paths has multiple actions associated, each a fallback for the one + // previous to it. + Actions []ActionSet +} + +// UnmarshalJSON implements the `json.Unmarshaler` interface. +func (p *Plan) UnmarshalJSON(raw []byte) error { + var t jsonPlan + if err := json.Unmarshal(raw, &t); err != nil { + return err + } + p.Checks = make([]Check, len(t.Checks)) + for i, check := range t.Checks { + c, err := parseCheck(check.Type, check.Params) + if err != nil { + return fmt.Errorf("failed to parse check %q: %v", check.Type, err) + } + p.Checks[i] = c + } + + if len(t.Actions) > 0 { + p.Actions = make([]ActionSet, len(t.Actions)) + } + for i, actionSet := range t.Actions { + var err error + pathActions := make([]Action, len(actionSet.Actions)) + for i, action := range actionSet.Actions { + var actionConditions []Check + if len(action.Conditions) > 0 { + actionConditions = make([]Check, len(action.Conditions)) + } + for j, check := range action.Conditions { + actionConditions[j], err = parseCheck(check.Type, check.Params) + if err != nil { + return err + } + } + pathActions[i], err = parseAction(action.Type, action.Params, actionConditions) + if err != nil { + return err + } + } + p.Actions[i] = ActionSet{ + Paths: actionSet.Paths, + Actions: pathActions, + } + } + return nil +} + +// Execute executes the synchronization plan. +func (p *Plan) Execute(c Setup) error { + c.Logf("running pre-checks") + for _, check := range p.Checks { + err := check.Check("", c) // For pre-sync checks, the path is ignored. + if err != nil { + return fmt.Errorf("failed check: %v", err) + } + } + result := []pathResult{} + c.Logf("running actions") + for _, actions := range p.Actions { + PATHS_LOOP: + for _, path := range actions.Paths { + c.Logf("syncing path %q", path) + ACTIONS_LOOP: + for i, action := range actions.Actions { + c.Logf("running action for path %q", path) + err := action.Check(path, c) + if IsCheckFail(err) { + c.Logf("check failed, not running action: %v", err) + // If a check for an action fails, we switch to + // the next action associated with the path. + if i == len(actions.Actions)-1 { // no actions to fallback to. + c.Logf("path %q not handled - no more fallbacks", path) + result = append(result, + pathResult{ + Path: path, + Status: statusFailed, + Message: fmt.Sprintf("check failed, %s", err.Error()), + }) + } + continue ACTIONS_LOOP + } else if err != nil { + c.LogErrorf("unexpected error when running check: %v", err) + return fmt.Errorf("failed to run checks for action: %v", err) + } + err = action.Run(path, c) + if err != nil { + c.LogErrorf("action failed: %v", err) + return fmt.Errorf("action failed: %v", err) + } + c.Logf("path %q sync'ed successfully", path) + result = append(result, + pathResult{ + Path: path, + Status: statusUpdated, + }) + + continue PATHS_LOOP + } + } + } + + // Print execution result. + sort.SliceStable(result, func(i, j int) bool { return result[i].Path < result[j].Path }) + for _, res := range result { + if res.Message != "" { + fmt.Fprintf(os.Stdout, "%s\t%s: %s\n", res.Status, res.Path, res.Message) + } else { + fmt.Fprintf(os.Stdout, "%s\t%s\n", res.Status, res.Path) + } + } + return nil +} + +// Check returns an error if the condition fails. +type Check interface { + Check(string, Setup) error +} + +// ActionSet is a set of actions along with a set of paths to +// perform those actions on. +type ActionSet struct { + Paths []string + Actions []Action +} + +// Action runs the defined action. +type Action interface { + // Run performs the action on the specified path. + Run(string, Setup) error + // Check runs checks associated with the action + // before running it. + Check(string, Setup) error +} + +// jsonPlan is used to unmarshal Plan structures. +type jsonPlan struct { + Checks []struct { + Type string `json:"type"` + Params json.RawMessage `json:"params,omitempty"` + } + Actions []struct { + Paths []string `json:"paths"` + Actions []struct { + Type string `json:"type"` + Params json.RawMessage `json:"params,omitempty"` + Conditions []struct { + Type string `json:"type"` + Params json.RawMessage `json:"params"` + } + } + } +} + +func parseCheck(checkType string, rawParams json.RawMessage) (Check, error) { + var c Check + + var params interface{} + + switch checkType { + case "repo_is_clean": + tc := RepoIsCleanChecker{} + params = &tc.Params + c = &tc + case "exists": + tc := PathExistsChecker{} + params = &tc.Params + c = &tc + case "file_unaltered": + tc := FileUnalteredChecker{} + params = &tc.Params + c = &tc + default: + return nil, fmt.Errorf("unknown checker type %q", checkType) + } + + if len(rawParams) > 0 { + err := json.Unmarshal(rawParams, params) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal params for %s: %v", checkType, err) + } + } + return c, nil +} + +func parseAction(actionType string, rawParams json.RawMessage, checks []Check) (Action, error) { + var a Action + + var params interface{} + + switch actionType { + case "overwrite_file": + ta := OverwriteFileAction{} + ta.Conditions = checks + params = &ta.Params + a = &ta + case "overwrite_directory": + ta := OverwriteDirectoryAction{} + ta.Conditions = checks + params = &ta.Params + a = &ta + default: + return nil, fmt.Errorf("unknown action type %q", actionType) + } + + if len(rawParams) > 0 { + err := json.Unmarshal(rawParams, params) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal params for %s: %v", actionType, err) + } + } + return a, nil +} + +// pathResult contains the result of synchronizing a path. +type pathResult struct { + Path string + Status status + Message string +} + +type status string + +const ( + statusUpdated status = "UPDATED" + statusFailed status = "FAILED" +) diff --git a/build/sync/plan/plan_test.go b/build/sync/plan/plan_test.go new file mode 100644 index 00000000..711e1509 --- /dev/null +++ b/build/sync/plan/plan_test.go @@ -0,0 +1,253 @@ +package plan_test + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mattermost/mattermost-plugin-starter-template/build/sync/plan" +) + +func TestUnmarshalPlan(t *testing.T) { + assert := assert.New(t) + rawJSON := []byte(` +{ + "checks": [ + {"type": "repo_is_clean", "params": {"repo": "template"}} + ], + "actions": [ + { + "paths": ["abc"], + "actions": [{ + "type": "overwrite_file", + "params": {"create": true}, + "conditions": [{ + "type": "exists", + "params": {"repo": "plugin"} + }] + }] + } + ] +}`) + var p plan.Plan + err := json.Unmarshal(rawJSON, &p) + assert.Nil(err) + expectedCheck := plan.RepoIsCleanChecker{} + expectedCheck.Params.Repo = "template" + + expectedAction := plan.OverwriteFileAction{} + expectedAction.Params.Create = true + expectedActionCheck := plan.PathExistsChecker{} + expectedActionCheck.Params.Repo = "plugin" + expectedAction.Conditions = []plan.Check{&expectedActionCheck} + expected := plan.Plan{ + Checks: []plan.Check{&expectedCheck}, + Actions: []plan.ActionSet{{ + Paths: []string{"abc"}, + Actions: []plan.Action{ + &expectedAction, + }, + }}, + } + assert.Equal(expected, p) +} + +type mockCheck struct { + returnErr error + calledWith string // Path parameter the check was called with. +} + +// Check implements the plan.Check interface. +func (m *mockCheck) Check(path string, c plan.Setup) error { + m.calledWith = path + return m.returnErr +} + +type mockAction struct { + runErr error // Error to be returned by Run. + checkErr error // Error to be returned by Check. + calledWith string +} + +// Check implements plan.Action interface. +func (m *mockAction) Check(path string, c plan.Setup) error { + return m.checkErr +} + +// Run implements plan.Action interface. +func (m *mockAction) Run(path string, c plan.Setup) error { + m.calledWith = path + return m.runErr +} + +// TestRunPlanSuccessfully tests a successful execution of a sync plan. +func TestRunPlanSuccessfully(t *testing.T) { + assert := assert.New(t) + + setup := plan.Setup{} // mocked actions and checks won't be accessing the setup. + + preCheck := &mockCheck{} + action1 := &mockAction{} + action2 := &mockAction{} + + p := &plan.Plan{ + Checks: []plan.Check{preCheck}, + Actions: []plan.ActionSet{{ + Paths: []string{"somepath"}, + Actions: []plan.Action{ + action1, + action2, + }, + }}, + } + err := p.Execute(setup) + assert.Nil(err) + + assert.Equal("", preCheck.calledWith) + assert.Equal("somepath", action1.calledWith) + assert.Equal("", action2.calledWith) // second action was not called. +} + +// TestRunPlanPreCheckFail checks the scenario where a sync plan precheck +// fails, aborting the whole operation. +func TestRunPlanPreCheckFail(t *testing.T) { + assert := assert.New(t) + + setup := plan.Setup{} // mocked actions and checks won't be accessing the setup. + + preCheck := &mockCheck{returnErr: plan.CheckFailf("check failed")} + action1 := &mockAction{} + action2 := &mockAction{} + + p := &plan.Plan{ + Checks: []plan.Check{preCheck}, + Actions: []plan.ActionSet{{ + Paths: []string{"somepath"}, + Actions: []plan.Action{ + action1, + action2, + }, + }}, + } + err := p.Execute(setup) + assert.EqualError(err, "failed check: check failed") + + assert.Equal("", preCheck.calledWith) + // None of the actions were executed. + assert.Equal("", action1.calledWith) + assert.Equal("", action2.calledWith) +} + +// TestRunPlanActionCheckFails tests the situation where an action's +// check returns a recoverable error, forcing the plan to execute the fallback action. +func TestRunPlanActionCheckFails(t *testing.T) { + assert := assert.New(t) + + setup := plan.Setup{} // mocked actions and checks won't be accessing the setup. + + action1 := &mockAction{checkErr: plan.CheckFailf("action check failed")} + action2 := &mockAction{} + + p := &plan.Plan{ + Actions: []plan.ActionSet{{ + Paths: []string{"somepath"}, + Actions: []plan.Action{ + action1, + action2, + }, + }}, + } + err := p.Execute(setup) + assert.Nil(err) + + assert.Equal("", action1.calledWith) // First action was not run. + assert.Equal("somepath", action2.calledWith) // Second action was run. +} + +// TestRunPlanNoFallbacks tests the case where an action's check fails, +// but there are not more fallback actions for that path. +func TestRunPlanNoFallbacks(t *testing.T) { + assert := assert.New(t) + + setup := plan.Setup{} // mocked actions and checks won't be accessing the setup. + + action1 := &mockAction{checkErr: plan.CheckFailf("fail")} + action2 := &mockAction{checkErr: plan.CheckFailf("fail")} + + p := &plan.Plan{ + Actions: []plan.ActionSet{{ + Paths: []string{"somepath"}, + Actions: []plan.Action{ + action1, + action2, + }, + }}, + } + err := p.Execute(setup) + assert.Nil(err) + + // both actions were not executed. + assert.Equal("", action1.calledWith) + assert.Equal("", action2.calledWith) +} + +// TestRunPlanCheckError tests the scenario where a plan check fails with +// an unexpected error. Plan execution is aborted. +func TestRunPlanCheckError(t *testing.T) { + assert := assert.New(t) + + setup := plan.Setup{} // mocked actions and checks won't be accessing the setup. + + preCheck := &mockCheck{returnErr: fmt.Errorf("fail")} + action1 := &mockAction{} + action2 := &mockAction{} + + p := &plan.Plan{ + Checks: []plan.Check{preCheck}, + Actions: []plan.ActionSet{{ + Paths: []string{"somepath"}, + Actions: []plan.Action{ + action1, + action2, + }, + }}, + } + err := p.Execute(setup) + assert.EqualError(err, "failed check: fail") + + assert.Equal("", preCheck.calledWith) + // Actions were not run. + assert.Equal("", action1.calledWith) + assert.Equal("", action2.calledWith) +} + +// TestRunPlanActionError tests the scenario where an action fails, +// aborting the whole sync process. +func TestRunPlanActionError(t *testing.T) { + assert := assert.New(t) + + setup := plan.Setup{} // mocked actions and checks won't be accessing the setup. + + preCheck := &mockCheck{} + action1 := &mockAction{runErr: fmt.Errorf("fail")} + action2 := &mockAction{} + + p := &plan.Plan{ + Checks: []plan.Check{preCheck}, + Actions: []plan.ActionSet{{ + Paths: []string{"somepath"}, + Actions: []plan.Action{ + action1, + action2, + }, + }}, + } + err := p.Execute(setup) + assert.EqualError(err, "action failed: fail") + + assert.Equal("", preCheck.calledWith) + assert.Equal("somepath", action1.calledWith) + assert.Equal("", action2.calledWith) // second action was not called. +} diff --git a/build/sync/plan/setup.go b/build/sync/plan/setup.go new file mode 100644 index 00000000..f4cee0e0 --- /dev/null +++ b/build/sync/plan/setup.go @@ -0,0 +1,80 @@ +package plan + +import ( + "fmt" + "os" + "path/filepath" + + git "gopkg.in/src-d/go-git.v4" +) + +// RepoID identifies a repository - either plugin or template. +type RepoID string + +const ( + // SourceRepo is the id of the template repository (source). + SourceRepo RepoID = "source" + // TargetRepo is the id of the plugin repository (target). + TargetRepo RepoID = "target" +) + +// Setup contains information about both parties +// in the sync: the plugin repository being updated +// and the source of the update - the template repo. +type Setup struct { + Source RepoSetup + Target RepoSetup + VerboseLogging bool +} + +// Logf logs the provided message. +// If verbose output is not enabled, the message will not be printed. +func (c Setup) Logf(tpl string, args ...interface{}) { + if c.VerboseLogging { + fmt.Fprintf(os.Stderr, tpl+"\n", args...) + } +} + +// LogErrorf logs the provided error message. +func (c Setup) LogErrorf(tpl string, args ...interface{}) { + fmt.Fprintf(os.Stderr, tpl+"\n", args...) +} + +// GetRepo is a helper to get the required repo setup. +// If the target parameter is not one of "plugin" or "template", +// the function panics. +func (c Setup) GetRepo(r RepoID) RepoSetup { + switch r { + case TargetRepo: + return c.Target + case SourceRepo: + return c.Source + default: + panic(fmt.Sprintf("cannot get repository setup %q", r)) + } +} + +// PathInRepo returns the full path of a file in the specified repository. +func (c Setup) PathInRepo(repo RepoID, path string) string { + r := c.GetRepo(repo) + return filepath.Join(r.Path, path) +} + +// RepoSetup contains relevant information +// about a single repository (either source or target). +type RepoSetup struct { + Git *git.Repository + Path string +} + +// GetRepoSetup returns the repository setup for the specified path. +func GetRepoSetup(path string) (RepoSetup, error) { + repo, err := git.PlainOpen(path) + if err != nil { + return RepoSetup{}, fmt.Errorf("failed to access git repository at %q: %v", path, err) + } + return RepoSetup{ + Git: repo, + Path: path, + }, nil +} diff --git a/build/sync/plan/testdata/a b/build/sync/plan/testdata/a new file mode 100644 index 00000000..2e65efe2 --- /dev/null +++ b/build/sync/plan/testdata/a @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/build/sync/plan/testdata/b/c b/build/sync/plan/testdata/b/c new file mode 100644 index 00000000..3410062b --- /dev/null +++ b/build/sync/plan/testdata/b/c @@ -0,0 +1 @@ +c \ No newline at end of file diff --git a/go.mod b/go.mod index 956bea1b..3687e995 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,6 @@ require ( github.com/mattermost/mattermost-server/v5 v5.3.2-0.20200723144633-ed34468996e6 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.6.1 + gopkg.in/src-d/go-git.v4 v4.13.1 + sigs.k8s.io/yaml v1.2.0 ) diff --git a/go.sum b/go.sum index a5fcbcc6..c897e27c 100644 --- a/go.sum +++ b/go.sum @@ -34,16 +34,21 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/a8m/expect v1.0.0/go.mod h1:4IwSCMumY49ScypDnjNbYEjgVeqy1/U2cEs3Lat96eA= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +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/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.3/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +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/avct/uasurfer v0.0.0-20191028135549-26b5daa857f1/go.mod h1:noBAuukeYOXa0aXGqxr24tADqkwDO2KRD15FsuaZ5a8= github.com/aws/aws-sdk-go v1.19.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= @@ -91,6 +96,7 @@ github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37g github.com/couchbase/vellum v1.0.1/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= @@ -113,6 +119,8 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -127,6 +135,7 @@ github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= @@ -142,6 +151,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= @@ -274,7 +285,10 @@ github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62 github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= github.com/jamiealquiza/envy v1.1.0/go.mod h1:MP36BriGCLwEHhi1OU8E9569JNZrjWfCvzG7RsPnHus= github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +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/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -295,6 +309,8 @@ github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4 github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U= github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= @@ -311,6 +327,7 @@ github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= @@ -368,6 +385,7 @@ github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77Z github.com/minio/minio-go/v6 v6.0.57/go.mod h1:5+R/nM9Pwrh0vqF+HbYYDQ84wdUFPyXHkrdT4AIkifM= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +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/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= @@ -417,6 +435,7 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= @@ -479,6 +498,7 @@ github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFo github.com/satori/go.uuid v0.0.0-20180103174451-36e9d2ebbde5/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= @@ -528,6 +548,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7Z4dM9/Y= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -576,6 +598,8 @@ github.com/wiggin77/merror v1.0.2/go.mod h1:uQTcIU0Z6jRK4OwqganPYerzQxSFJ4GSHM3a github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8= github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= @@ -615,6 +639,7 @@ golang.org/x/build v0.0.0-20190314133821-5284462c4bec/go.mod h1:atTaCNAy0f16Ah5a golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -664,6 +689,7 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -694,11 +720,13 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -739,6 +767,7 @@ golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -826,14 +855,23 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXL gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/olivere/elastic.v6 v6.2.33/go.mod h1:2cTT8Z+/LcArSWpCgvZqBgt3VOqXiy7v00w12Lz8bd4= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= +gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 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.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -848,6 +886,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= willnorris.com/go/gifresize v1.0.0/go.mod h1:eBM8gogBGCcaH603vxSpnfjwXIpq6nmnj/jauBDKtAk=