MM-21722 - Repository synchronization tool (#86)
This commit is contained in:
parent
4e7a2d3734
commit
0688e8df4c
18 changed files with 1589 additions and 0 deletions
214
build/sync/plan/actions.go
Normal file
214
build/sync/plan/actions.go
Normal file
|
@ -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
|
||||
}
|
107
build/sync/plan/actions_test.go
Normal file
107
build/sync/plan/actions_test.go
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
147
build/sync/plan/checks.go
Normal file
147
build/sync/plan/checks.go
Normal file
|
@ -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)
|
||||
}
|
122
build/sync/plan/checks_test.go
Normal file
122
build/sync/plan/checks_test.go
Normal file
|
@ -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)))
|
||||
}
|
106
build/sync/plan/git/file_history.go
Normal file
106
build/sync/plan/git/file_history.go
Normal file
|
@ -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
|
||||
}
|
27
build/sync/plan/git/file_history_test.go
Normal file
27
build/sync/plan/git/file_history_test.go
Normal file
|
@ -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)
|
||||
}
|
1
build/sync/plan/git/testdata/testfile.txt
vendored
Normal file
1
build/sync/plan/git/testdata/testfile.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
This file is used to test file history tracking.
|
245
build/sync/plan/plan.go
Normal file
245
build/sync/plan/plan.go
Normal file
|
@ -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"
|
||||
)
|
253
build/sync/plan/plan_test.go
Normal file
253
build/sync/plan/plan_test.go
Normal file
|
@ -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.
|
||||
}
|
80
build/sync/plan/setup.go
Normal file
80
build/sync/plan/setup.go
Normal file
|
@ -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
|
||||
}
|
1
build/sync/plan/testdata/a
vendored
Normal file
1
build/sync/plan/testdata/a
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
a
|
1
build/sync/plan/testdata/b/c
vendored
Normal file
1
build/sync/plan/testdata/b/c
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
c
|
Reference in a new issue