diff --git a/pipe/pipe.go b/pipe/pipe.go new file mode 100644 index 0000000..e55271d --- /dev/null +++ b/pipe/pipe.go @@ -0,0 +1,27 @@ +package pipe + +// Op is the common pipe operation. Can be composed into Ops and run as a single unit +type Op interface { + Do() error +} + +// OpFunc can easily wrap an anonymous function into an Op +type OpFunc func() error + +// Do implements the Op interface +func (o OpFunc) Do() error { + return o() +} + +// Ops can run a slice of Op's in series, stopping on the first error +type Ops []Op + +// Do implements the Op interface +func (ops Ops) Do() error { + for _, op := range ops { + if err := op.Do(); err != nil { + return err + } + } + return nil +} diff --git a/pipe/pipe_test.go b/pipe/pipe_test.go new file mode 100644 index 0000000..a1271fb --- /dev/null +++ b/pipe/pipe_test.go @@ -0,0 +1,54 @@ +package pipe + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOpFuncDo(t *testing.T) { + t.Run("no error", func(t *testing.T) { + op := OpFunc(func() error { + return nil + }) + assert.NoError(t, op.Do()) + }) + + t.Run("error", func(t *testing.T) { + someErr := errors.New("some error") + op := OpFunc(func() error { + return someErr + }) + assert.Equal(t, someErr, op.Do()) + }) +} + +func TestOpsDo(t *testing.T) { + nilOp := OpFunc(func() error { + return nil + }) + someErr := errors.New("some error") + errOp := OpFunc(func() error { + return someErr + }) + + detectOp := func(ran *bool) Op { + return OpFunc(func() error { + *ran = true + return nil + }) + } + + t.Run("no errors", func(t *testing.T) { + var ranLast bool + assert.NoError(t, Ops{nilOp, nilOp, nilOp, detectOp(&ranLast)}.Do()) + assert.True(t, ranLast) + }) + + t.Run("stops on first error", func(t *testing.T) { + var ranAfterError bool + assert.Equal(t, someErr, Ops{nilOp, errOp, detectOp(&ranAfterError), nilOp}.Do()) + assert.False(t, ranAfterError) + }) +} diff --git a/vcs/vcs.go b/vcs/vcs.go index 2f95b7f..c1ec14f 100644 --- a/vcs/vcs.go +++ b/vcs/vcs.go @@ -7,6 +7,7 @@ import ( "sync" "time" + "github.com/johnstarich/sage/pipe" "github.com/pkg/errors" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing" @@ -42,40 +43,46 @@ type syncRepo struct { } func initVCS(path string) (*git.Repository, error) { - repo, err := git.PlainInit(path, false) - if err != nil { - return nil, err - } - tree, err := repo.Worktree() - if err != nil { - return nil, err - } - - status, err := tree.Status() - if err != nil { - return nil, err - } - - added := false - for file, stat := range status { - // add any untracked files, excluding hidden and tmp files - if stat.Worktree == git.Untracked && !strings.HasPrefix(file, ".") && !strings.HasSuffix(file, ".tmp") { - _, err := tree.Add(file) - if err != nil { - return nil, err + var err error + var repo *git.Repository + var tree *git.Worktree + var status git.Status + return repo, pipe.Ops{ + pipe.OpFunc(func() error { + repo, err = git.PlainInit(path, false) + return err + }), + pipe.OpFunc(func() error { + tree, err = repo.Worktree() + return err + }), + pipe.OpFunc(func() error { + status, err = tree.Status() + return err + }), + pipe.OpFunc(func() error { + var ops pipe.Ops + added := false + for file, stat := range status { + // add any untracked files, excluding hidden and tmp files + if stat.Worktree == git.Untracked && !strings.HasPrefix(file, ".") && !strings.HasSuffix(file, ".tmp") { + fileCopy := file + ops = append(ops, pipe.OpFunc(func() error { + _, err := tree.Add(fileCopy) + return err + })) + added = true + } } - added = true - } - } - if added { - _, err := tree.Commit("Initial commit", &git.CommitOptions{ - Author: sageAuthor(), - }) - if err != nil { - return nil, err - } - } - return repo, nil + if added { + ops = append(ops, pipe.OpFunc(func() error { + _, err := tree.Commit("Initial commit", &git.CommitOptions{Author: sageAuthor()}) + return err + })) + } + return ops.Do() + }), + }.Do() } func sageAuthor() *object.Signature { diff --git a/vcs/vcs_test.go b/vcs/vcs_test.go index bbf67cb..d21dc94 100644 --- a/vcs/vcs_test.go +++ b/vcs/vcs_test.go @@ -3,6 +3,7 @@ package vcs import ( "io/ioutil" "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -11,18 +12,22 @@ import ( "gopkg.in/src-d/go-git.v4/plumbing/object" ) +const testDBPath = "./testdb" + +func cleanupTestDB(t *testing.T) { + t.Helper() + require.NoError(t, os.RemoveAll(testDBPath)) +} + func TestOpen(t *testing.T) { - cleanup := func() { - require.NoError(t, os.RemoveAll("./testdb")) - } - cleanup() - defer cleanup() - err := os.MkdirAll("./testdb", 0750) + cleanupTestDB(t) + defer cleanupTestDB(t) + err := os.MkdirAll(testDBPath, 0750) require.NoError(t, err) - err = ioutil.WriteFile("./testdb/bucket.json", []byte(`{}`), 0750) + err = ioutil.WriteFile(filepath.Join(testDBPath, "bucket.json"), []byte(`{}`), 0750) require.NoError(t, err) - repoInt, err := Open("./testdb") + repoInt, err := Open(testDBPath) require.NoError(t, err) require.IsType(t, &syncRepo{}, repoInt) @@ -49,21 +54,29 @@ func TestOpen(t *testing.T) { assert.Equal(t, 1, count) } +func TestOpenMkdirErr(t *testing.T) { + cleanupTestDB(t) + defer cleanupTestDB(t) + + err := ioutil.WriteFile(testDBPath, []byte(`I'm not a database!`), 0750) + require.NoError(t, err) + _, err = Open(testDBPath) + require.Error(t, err) + assert.Equal(t, "mkdir testdb: not a directory", err.Error()) +} + func TestCommitFiles(t *testing.T) { - cleanup := func() { - require.NoError(t, os.RemoveAll("./testdb")) - } - cleanup() - defer cleanup() + cleanupTestDB(t) + defer cleanupTestDB(t) - repoInt, err := Open("./testdb") + repoInt, err := Open(testDBPath) require.NoError(t, err) require.IsType(t, &syncRepo{}, repoInt) repo := repoInt.(*syncRepo) err = repo.CommitFiles(func() error { - return ioutil.WriteFile("./testdb/some file.txt", []byte("hello world"), 0750) - }, "add some file", "./testdb/some file.txt") + return ioutil.WriteFile(filepath.Join(testDBPath, "some file.txt"), []byte("hello world"), 0750) + }, "add some file", filepath.Join(testDBPath, "some file.txt")) require.NoError(t, err) getCount := func() int { @@ -81,27 +94,24 @@ func TestCommitFiles(t *testing.T) { assert.Equal(t, 1, getCount()) err = repo.CommitFiles(func() error { - return ioutil.WriteFile("./testdb/some other file.txt", []byte("hello world"), 0750) - }, "add some other file", "./testdb/some other file.txt") + return ioutil.WriteFile(filepath.Join(testDBPath, "some other file.txt"), []byte("hello world"), 0750) + }, "add some other file", filepath.Join(testDBPath, "some other file.txt")) require.NoError(t, err) assert.Equal(t, 2, getCount()) } func TestCommitNoChanges(t *testing.T) { - cleanup := func() { - require.NoError(t, os.RemoveAll("./testdb")) - } - cleanup() - defer cleanup() + cleanupTestDB(t) + defer cleanupTestDB(t) - repoInt, err := Open("./testdb") + repoInt, err := Open(testDBPath) require.NoError(t, err) require.IsType(t, &syncRepo{}, repoInt) repo := repoInt.(*syncRepo) err = repo.CommitFiles(func() error { - return ioutil.WriteFile("./testdb/some file.txt", []byte("hello world"), 0750) - }, "add some file", "./testdb/some file.txt") + return ioutil.WriteFile(filepath.Join(testDBPath, "some file.txt"), []byte("hello world"), 0750) + }, "add some file", filepath.Join(testDBPath, "some file.txt")) require.NoError(t, err) getCount := func() int { @@ -118,7 +128,7 @@ func TestCommitNoChanges(t *testing.T) { } assert.Equal(t, 1, getCount()) - err = repo.CommitFiles(func() error { return nil }, "add same file", "./testdb/some file.txt") + err = repo.CommitFiles(func() error { return nil }, "add same file", filepath.Join(testDBPath, "some file.txt")) require.NoError(t, err) assert.Equal(t, 1, getCount(), "no commit should be made for unchanged file") }