Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

commit subcommand #17

Merged
merged 2 commits into from
Apr 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions cmd/clone/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ func TestItLogsCheckoutErrorsButContinuesToTryAll(t *testing.T) {
{"work/org", "org/repo2"},
})
fakeGit.AssertCalledWith(t, [][]string{
{"work/org/repo1", testsupport.Pwd()},
{"work/org/repo2", testsupport.Pwd()},
{"checkout", "work/org/repo1", testsupport.Pwd()},
{"checkout", "work/org/repo2", testsupport.Pwd()},
})

}
Expand All @@ -96,8 +96,8 @@ func TestItClonesReposFoundInReposFile(t *testing.T) {
{"work/org", "org/repo2"},
})
fakeGit.AssertCalledWith(t, [][]string{
{"work/org/repo1", testsupport.Pwd()},
{"work/org/repo2", testsupport.Pwd()},
{"checkout", "work/org/repo1", testsupport.Pwd()},
{"checkout", "work/org/repo2", testsupport.Pwd()},
})
}

Expand All @@ -117,8 +117,8 @@ func TestItClonesReposInMultipleOrgs(t *testing.T) {
{"work/orgB", "orgB/repo2"},
})
fakeGit.AssertCalledWith(t, [][]string{
{"work/orgA/repo1", testsupport.Pwd()},
{"work/orgB/repo2", testsupport.Pwd()},
{"checkout", "work/orgA/repo1", testsupport.Pwd()},
{"checkout", "work/orgB/repo2", testsupport.Pwd()},
})
}

Expand All @@ -138,8 +138,8 @@ func TestItClonesReposFromOtherHosts(t *testing.T) {
{"work/orgB", "orgB/repo2"},
})
fakeGit.AssertCalledWith(t, [][]string{
{"work/orgA/repo1", testsupport.Pwd()},
{"work/orgB/repo2", testsupport.Pwd()},
{"checkout", "work/orgA/repo1", testsupport.Pwd()},
{"checkout", "work/orgB/repo2", testsupport.Pwd()},
})
}

Expand All @@ -160,7 +160,7 @@ func TestItSkipsCloningIfAWorkingCopyAlreadyExists(t *testing.T) {
{"work/org", "org/repo2"},
})
fakeGit.AssertCalledWith(t, [][]string{
{"work/org/repo2", testsupport.Pwd()},
{"checkout", "work/org/repo2", testsupport.Pwd()},
})
}

Expand Down
83 changes: 83 additions & 0 deletions cmd/commit/commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package commit

import (
"github.com/skyscanner/turbolift/internal/campaign"
"github.com/skyscanner/turbolift/internal/colors"
"github.com/skyscanner/turbolift/internal/git"
"github.com/spf13/cobra"
"os"
"path"
)

var g git.Git = git.NewRealGit()

var message string

func NewCommitCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "commit",
Short: "Applies git commit -a -m '...' to all working copies, if they have changes",
Run: run,
}

cmd.Flags().StringVarP(&message, "message", "m", "", "Commit message to apply")
err := cmd.MarkFlagRequired("message")
if err != nil {
panic(err)
}

return cmd
}

func run(c *cobra.Command, _ []string) {
dir, err := campaign.OpenCampaign()
if err != nil {
c.Printf(colors.Red("Error when reading campaign directory: %s\n"), err)
return
}

doneCount := 0
skippedCount := 0
errorCount := 0
for _, repo := range dir.Repos {
repoDirPath := path.Join("work", repo.OrgName, repo.RepoName) // i.e. work/org/repo

// skip if the working copy does not exist
if _, err = os.Stat(repoDirPath); os.IsNotExist(err) {
c.Printf(colors.Yellow("Not running against %s as the directory %s does not exist - has it been cloned?\n"), repo.FullRepoName, repoDirPath)
skippedCount++
continue
}

c.Println(repo.FullRepoName)

isChanged, err := g.IsRepoChanged(c.OutOrStdout(), repoDirPath)
if err != nil {
c.Printf(colors.Red("Error when checking for changes in %s: %s\n"), repo.FullRepoName, err)
errorCount++
continue
}

if !isChanged {
c.Printf(colors.Yellow("No changes in %s - skipping commit\n"), repo.FullRepoName)
skippedCount++
continue
}

c.Printf("Committing changes in %s\n", repo.FullRepoName)

err = g.Commit(c.OutOrStdout(), repoDirPath, message)
if err != nil {
c.Printf(colors.Red("Error when committing changes in %s: %s\n"), repo.FullRepoName, err)
sledigabel marked this conversation as resolved.
Show resolved Hide resolved
errorCount++
} else {
doneCount++
}
}

if errorCount == 0 {
c.Printf(colors.Green("✅ turbolift commit completed (%d OK, %d skipped)\n"), doneCount, skippedCount)
} else {
c.Printf(colors.Yellow("⚠️ turbolift commit completed with errors (%d OK, %d skipped, %d errored)\n"), doneCount, skippedCount, errorCount)
}
}
116 changes: 116 additions & 0 deletions cmd/commit/commit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package commit

import (
"bytes"
"errors"
"github.com/skyscanner/turbolift/internal/git"
"github.com/skyscanner/turbolift/internal/testsupport"
"github.com/stretchr/testify/assert"
"io"
"os"
"testing"
)

func TestItCommitsAllWithChanges(t *testing.T) {
fakeGit := git.NewAlwaysSucceedsFakeGit()
g = fakeGit

testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")

out, err := runCommand("some test message", []string{}...)
assert.NoError(t, err)
assert.Contains(t, out, "2 OK")

fakeGit.AssertCalledWith(t, [][]string{
{"isRepoChanged", "work/org/repo1"},
{"commit", "work/org/repo1", "some test message"},
{"isRepoChanged", "work/org/repo2"},
{"commit", "work/org/repo2", "some test message"},
})
}

func TestItSkipsReposWithoutChanges(t *testing.T) {
fakeGit := git.NewFakeGit(func(output io.Writer, call []string) (bool, error) {
if call[0] == "isRepoChanged" && call[1] == "work/org/repo1" {
return false, nil
} else {
return true, nil
}
})
g = fakeGit

testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")

out, err := runCommand("some test message", []string{}...)
assert.NoError(t, err)
assert.Contains(t, out, "No changes in org/repo1 - skipping commit")
assert.Contains(t, out, "1 OK, 1 skipped")

fakeGit.AssertCalledWith(t, [][]string{
{"isRepoChanged", "work/org/repo1"},
{"isRepoChanged", "work/org/repo2"},
{"commit", "work/org/repo2", "some test message"},
})
}

func TestItSkipsReposWhichErrorOnStatusChekc(t *testing.T) {
fakeGit := git.NewFakeGit(func(output io.Writer, call []string) (bool, error) {
if call[0] == "isRepoChanged" && call[1] == "work/org/repo1" {
return false, errors.New("synthetic error")
} else {
return true, nil
}
})
g = fakeGit

testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")

out, err := runCommand("some test message", []string{}...)
assert.NoError(t, err)
assert.Contains(t, out, "Error when checking for changes in org/repo1")
assert.Contains(t, out, "1 OK, 0 skipped, 1 errored")

fakeGit.AssertCalledWith(t, [][]string{
{"isRepoChanged", "work/org/repo1"},
{"isRepoChanged", "work/org/repo2"},
{"commit", "work/org/repo2", "some test message"},
})
}

func TestItSkipsMissingRepos(t *testing.T) {
fakeGit := git.NewAlwaysSucceedsFakeGit()
g = fakeGit

testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")
err := os.RemoveAll("work/org/repo1")
if err != nil {
panic(err)
}

out, err := runCommand("some test message", []string{}...)
assert.NoError(t, err)
assert.Contains(t, out, "1 OK, 1 skipped")

fakeGit.AssertCalledWith(t, [][]string{
{"isRepoChanged", "work/org/repo2"},
{"commit", "work/org/repo2", "some test message"},
})
}

func runCommand(m string, args ...string) (string, error) {
cmd := NewCommitCmd()
outBuffer := bytes.NewBufferString("")
cmd.SetOut(outBuffer)
cmd.SetArgs(args)
err := cmd.Flags().Set("message", m)
if err != nil {
panic(err)
}

err = cmd.Execute()

if err != nil {
return outBuffer.String(), err
}
return outBuffer.String(), nil
}
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
cloneCmd "github.com/skyscanner/turbolift/cmd/clone"
commitCmd "github.com/skyscanner/turbolift/cmd/commit"
foreachCmd "github.com/skyscanner/turbolift/cmd/foreach"
initCmd "github.com/skyscanner/turbolift/cmd/init"
"github.com/spf13/cobra"
Expand All @@ -15,6 +16,7 @@ var rootCmd = &cobra.Command{
}

func init() {
rootCmd.AddCommand(commitCmd.NewCommitCmd())
rootCmd.AddCommand(cloneCmd.NewCloneCmd())
rootCmd.AddCommand(initCmd.NewInitCmd())
rootCmd.AddCommand(foreachCmd.NewForeachCmd())
Expand Down
18 changes: 18 additions & 0 deletions internal/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

type Executor interface {
Execute(output io.Writer, workingDir string, name string, args ...string) error
ExecuteAndCapture(output io.Writer, workingDir string, name string, args ...string) (string, error)
sledigabel marked this conversation as resolved.
Show resolved Hide resolved
}

type RealExecutor struct {
Expand Down Expand Up @@ -38,6 +39,23 @@ func (e *RealExecutor) Execute(output io.Writer, workingDir string, name string,
return nil
}

func (e *RealExecutor) ExecuteAndCapture(output io.Writer, workingDir string, name string, args ...string) (string, error) {
command := exec.Command(name, args...)
command.Dir = workingDir

_, err := fmt.Fprintln(output, "Executing:", name, args)
if err != nil {
return "", err
}

commandOutput, err := command.Output()
if err != nil {
return string(commandOutput), err
}

return string(commandOutput), nil
}

func NewRealExecutor() *RealExecutor {
return &RealExecutor{}
}
Expand Down
22 changes: 17 additions & 5 deletions internal/executor/fake_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import (
)

type FakeExecutor struct {
Handler func(workingDir string, name string, args ...string) error
calls [][]string
Handler func(workingDir string, name string, args ...string) error
ReturningHandler func(workingDir string, name string, args ...string) (string, error)
calls [][]string
}

func (e *FakeExecutor) Execute(_ io.Writer, workingDir string, name string, args ...string) error {
Expand All @@ -18,25 +19,36 @@ func (e *FakeExecutor) Execute(_ io.Writer, workingDir string, name string, args
return e.Handler(workingDir, name, args...)
}

func (e *FakeExecutor) ExecuteAndCapture(_ io.Writer, workingDir string, name string, args ...string) (string, error) {
allArgs := append([]string{workingDir, name}, args...)
e.calls = append(e.calls, allArgs)
return e.ReturningHandler(workingDir, name, args...)
}

func (e *FakeExecutor) AssertCalledWith(t *testing.T, expected [][]string) {
assert.Equal(t, expected, e.calls)
}

func NewFakeExecutor(h func(string, string, ...string) error) *FakeExecutor {
func NewFakeExecutor(handler func(string, string, ...string) error, returningHandler func(string, string, ...string) (string, error)) *FakeExecutor {
return &FakeExecutor{
Handler: h,
calls: [][]string{},
Handler: handler,
ReturningHandler: returningHandler,
calls: [][]string{},
}
}

func NewAlwaysSucceedsFakeExecutor() *FakeExecutor {
return NewFakeExecutor(func(s string, s2 string, s3 ...string) error {
return nil
}, func(s string, s2 string, s3 ...string) (string, error) {
return "", nil
})
}

func NewAlwaysFailsFakeExecutor() *FakeExecutor {
return NewFakeExecutor(func(s string, s2 string, s3 ...string) error {
return errors.New("synthetic error")
}, func(s string, s2 string, s3 ...string) (string, error) {
return "", errors.New("synthetic error")
})
}
Loading