diff --git a/pkg/domain/gh_test.go b/pkg/domain/gh_test.go index b3e1f9f..70c2084 100644 --- a/pkg/domain/gh_test.go +++ b/pkg/domain/gh_test.go @@ -2,7 +2,6 @@ package domain_test import ( "bytes" - "strings" "testing" "github.com/plumming/dx/pkg/api" @@ -35,13 +34,13 @@ func TestGetDefaultBranch_Master(t *testing.T) { } func TestGetOrgAndRepo(t *testing.T) { - output := `origin https://github.com/clone/chilly (fetch) -origin https://github.com/clone/chilly (push) -upstream https://github.com/plumming/dx (fetch) -upstream https://github.com/plumming/dx (push)` - - org, repo, err := domain.ExtractOrgAndRepoFromGitRemotes(strings.NewReader(output)) + org, repo, err := domain.ExtractOrgAndRepoURL("https://github.com/clone/chilly") assert.NoError(t, err) assert.Equal(t, org, "clone") assert.Equal(t, repo, "chilly") + + org, repo, err = domain.ExtractOrgAndRepoURL("https://github.com/plumming/dx") + assert.NoError(t, err) + assert.Equal(t, org, "plumming") + assert.Equal(t, repo, "dx") } diff --git a/pkg/domain/git.go b/pkg/domain/git.go index ba076b2..b3be306 100644 --- a/pkg/domain/git.go +++ b/pkg/domain/git.go @@ -11,17 +11,12 @@ import ( "github.com/plumming/dx/pkg/util" ) -func GetOrgAndRepoFromCurrentDir() (string, string, error) { - c := util.Command{ - Name: "git", - Args: []string{"remote", "-v"}, - } - output, err := c.RunWithoutRetry() - if err != nil { - return "", "", err - } +var ( + Runner util.CommandRunner +) - return ExtractOrgAndRepoFromGitRemotes(strings.NewReader(output)) +func init() { + Runner = util.DefaultCommandRunner{} } func GetRemote(name string) (string, error) { @@ -29,7 +24,7 @@ func GetRemote(name string) (string, error) { Name: "git", Args: []string{"remote", "-v"}, } - output, err := c.RunWithoutRetry() + output, err := Runner.RunWithoutRetry(&c) if err != nil { return "", err } @@ -43,20 +38,7 @@ func CurrentBranchName(dir string) (string, error) { Args: []string{"branch", "--show-current"}, Dir: dir, } - output, err := c.RunWithoutRetry() - if err != nil { - return "", err - } - return output, nil -} - -func SwitchBranch(dir string, name string) (string, error) { - c := util.Command{ - Name: "git", - Args: []string{"checkout", name}, - Dir: dir, - } - output, err := c.RunWithoutRetry() + output, err := Runner.RunWithoutRetry(&c) if err != nil { return "", err } @@ -69,10 +51,12 @@ func Stash(dir string) (string, error) { Args: []string{"stash"}, Dir: dir, } - output, err := c.RunWithoutRetry() + + output, err := Runner.RunWithoutRetry(&c) if err != nil { return "", err } + return output, nil } @@ -82,7 +66,7 @@ func StashPop(dir string) (string, error) { Args: []string{"stash", "pop"}, Dir: dir, } - output, err := c.RunWithoutRetry() + output, err := Runner.RunWithoutRetry(&c) if err != nil { return "", err } @@ -95,7 +79,7 @@ func Add(dir string, name string) (string, error) { Args: []string{"add", name}, Dir: dir, } - output, err := c.RunWithoutRetry() + output, err := Runner.RunWithoutRetry(&c) if err != nil { return "", err } @@ -108,7 +92,7 @@ func Commit(dir string, message string) (string, error) { Args: []string{"commit", "-m", message}, Dir: dir, } - output, err := c.RunWithoutRetry() + output, err := Runner.RunWithoutRetry(&c) if err != nil { return "", err } @@ -121,7 +105,7 @@ func Status(dir string) (string, error) { Args: []string{"status"}, Dir: dir, } - output, err := c.RunWithoutRetry() + output, err := Runner.RunWithoutRetry(&c) if err != nil { return "", err } @@ -134,11 +118,22 @@ func LocalChanges(dir string) (bool, error) { Args: []string{"status", "--porcelain"}, Dir: dir, } - output, err := c.RunWithoutRetry() + output, err := Runner.RunWithoutRetry(&c) if err != nil { return false, err } - return output != "", nil + + split := strings.Split(strings.TrimSpace(output), "\n") + changed := []string{} + for _, s := range split { + if s != "" && !strings.HasPrefix(s, "??") { + changed = append(changed, s) + } + } + + log.Logger().Debugf("changed files %s, len=%d", changed, len(changed)) + + return len(changed) > 0, nil } func ConfigCommitterInformation(dir string, email string, name string) error { @@ -147,7 +142,7 @@ func ConfigCommitterInformation(dir string, email string, name string) error { Args: []string{"config", "user.email", email}, Dir: dir, } - _, err := c.RunWithoutRetry() + _, err := Runner.RunWithoutRetry(&c) if err != nil { return err } @@ -157,7 +152,7 @@ func ConfigCommitterInformation(dir string, email string, name string) error { Args: []string{"config", "user.name", name}, Dir: dir, } - _, err = c.RunWithoutRetry() + _, err = Runner.RunWithoutRetry(&c) if err != nil { return err } @@ -187,15 +182,10 @@ func ExtractURLFromRemote(reader io.Reader, name string) (string, error) { } } - return "", errors.New("unable to find remote named '" + name + "'") + return "", nil } -func ExtractOrgAndRepoFromGitRemotes(reader io.Reader) (string, string, error) { - urlString, err := ExtractURLFromRemote(reader, "origin") - if err != nil { - return "", "", errors.New("unable to find remote named 'origin'") - } - +func ExtractOrgAndRepoURL(urlString string) (string, string, error) { url, err := url2.Parse(urlString) if err != nil { return "", "", err diff --git a/pkg/domain/git_test.go b/pkg/domain/git_test.go index c9140a5..a99941a 100644 --- a/pkg/domain/git_test.go +++ b/pkg/domain/git_test.go @@ -8,12 +8,16 @@ import ( "strings" "testing" + "github.com/plumming/dx/pkg/util/mocks" + "github.com/plumming/dx/pkg/domain" "github.com/plumming/dx/pkg/util" "github.com/stretchr/testify/assert" ) func TestCanDetermineBranchName(t *testing.T) { + cr := util.DefaultCommandRunner{} + dir, err := ioutil.TempDir("", "domain_test__TestCanDetermineBranchName") assert.NoError(t, err) @@ -24,7 +28,8 @@ func TestCanDetermineBranchName(t *testing.T) { Args: []string{"init", "-b", "master"}, Dir: dir, } - output, err := c.RunWithoutRetry() + + output, err := cr.RunWithoutRetry(&c) assert.NoError(t, err) t.Log(output) @@ -35,6 +40,8 @@ func TestCanDetermineBranchName(t *testing.T) { } func TestCanStash(t *testing.T) { + cr := util.DefaultCommandRunner{} + dir, err := ioutil.TempDir("", "domain_test__TestCanStash") assert.NoError(t, err) @@ -45,7 +52,7 @@ func TestCanStash(t *testing.T) { Args: []string{"init", "-b", "master"}, Dir: dir, } - output, err := c.RunWithoutRetry() + output, err := cr.RunWithoutRetry(&c) assert.NoError(t, err) t.Log(output) @@ -95,6 +102,74 @@ func TestCanStash(t *testing.T) { localChanges, err = domain.LocalChanges(dir) assert.NoError(t, err) assert.True(t, localChanges) + + output, err = domain.Add(dir, "README.md") + assert.NoError(t, err) + t.Log(output) + + output, err = domain.Commit(dir, "Updated Commit") + assert.NoError(t, err) + t.Log(output) + + localChanges, err = domain.LocalChanges(dir) + assert.NoError(t, err) + assert.False(t, localChanges) + + d1 = []byte("hello\ngo\n") + err = ioutil.WriteFile(path.Join(dir, "OTHER.md"), d1, 0600) + assert.NoError(t, err) + + localChanges, err = domain.LocalChanges(dir) + assert.NoError(t, err) + assert.False(t, localChanges) +} + +func TestLocalChanges(t *testing.T) { + type test struct { + name string + raw string + expected bool + } + + tests := []test{ + { + name: "no changes", + raw: ``, + expected: false, + }, + { + name: "changes to existing files", + raw: ` M go.sum`, + expected: true, + }, + { + name: "new file", + raw: `?? ll`, + expected: false, + }, + { + name: "changes to both existing and new files", + raw: ` M go.sum +?? ll`, + expected: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + r := mocks.MockCommandRunner{} + domain.Runner = &r + mocks.GetRunWithoutRetryFunc = func(c *util.Command) (string, error) { + return tc.raw, nil + } + + b, err := domain.LocalChanges("") + assert.NoError(t, err) + assert.Equal(t, tc.expected, b) + + t.Logf("commands> %s", r.Commands) + }) + } } func TestCanDetermineRemoteNames(t *testing.T) { @@ -121,6 +196,12 @@ upstream https://github.com/plumming/dx (push)`, remote: "upstream", expectedURL: "https://github.com/plumming/dx", }, + { + raw: `origin https://github.com/garethjevans/chilly (fetch) +origin https://github.com/garethjevans/chilly (push)`, + remote: "upstream", + expectedURL: "", + }, } for _, tc := range tests { t.Run(fmt.Sprintf("TestCanDetermineRemoteNames-%s", tc.remote), func(t *testing.T) { diff --git a/pkg/domain/rebase.go b/pkg/domain/rebase.go index f036dc1..721847a 100644 --- a/pkg/domain/rebase.go +++ b/pkg/domain/rebase.go @@ -12,8 +12,10 @@ import ( type Rebase struct { cmd.CommonOptions - Org string - Repo string + OriginOrg string + OriginRepo string + UpstreamOrg string + UpstreamRepo string DefaultBranch string Config *api.Config } @@ -40,17 +42,29 @@ func (c *Rebase) Validate() error { return err } + if upstream == "" { + log.Logger().Warnf("No remote named 'upstream' found") + } + if origin == upstream { return errors.New("origin & upstream appear to be the same: " + origin) } - c.Org, c.Repo, err = GetOrgAndRepoFromCurrentDir() + c.OriginOrg, c.OriginRepo, err = ExtractOrgAndRepoURL(origin) if err != nil { return err } - log.Logger().Debugf("determined repo as %s/%s", c.Org, c.Repo) + log.Logger().Debugf("determined origin repo as %s/%s", c.OriginOrg, c.OriginRepo) - c.DefaultBranch, err = GetDefaultBranch(gh, c.Org, c.Repo) + if upstream != "" { + c.UpstreamOrg, c.UpstreamRepo, err = ExtractOrgAndRepoURL(upstream) + if err != nil { + return err + } + log.Logger().Debugf("determined upstream repo as %s/%s", c.UpstreamOrg, c.UpstreamRepo) + } + + c.DefaultBranch, err = GetDefaultBranch(gh, c.OriginOrg, c.OriginRepo) log.Logger().Debugf("determined default branch as %s", c.DefaultBranch) if err != nil { return err @@ -66,6 +80,7 @@ func (c *Rebase) Run() error { if err != nil { return err } + if localChanges { log.Logger().Error("There appear to be local changes, please stash and try again") return nil @@ -81,38 +96,51 @@ func (c *Rebase) Run() error { return nil } - // git fetch --tags upstream master - cmd := util.Command{ - Name: "git", - Args: []string{"fetch", "--tags", "upstream", c.DefaultBranch}, - } - output, err := cmd.RunWithoutRetry() - if err != nil { - return err - } - log.Logger().Info(output) - - // git rebase upstream/master - cmd = util.Command{ - Name: "git", - Args: []string{"rebase", fmt.Sprintf("upstream/%s", c.DefaultBranch)}, - } - output, err = cmd.RunWithoutRetry() - if err != nil { - return err - } - log.Logger().Info(output) - - // git push origin master - cmd = util.Command{ - Name: "git", - Args: []string{"push", "origin", c.DefaultBranch}, - } - output, err = cmd.RunWithoutRetry() - if err != nil { - return err + if c.UpstreamRepo == "" && c.UpstreamOrg == "" { + // git fetch --tags upstream master + cmd := util.Command{ + Name: "git", + Args: []string{"pull", "--tags", "origin", c.DefaultBranch}, + } + output, err := Runner.RunWithoutRetry(&cmd) + if err != nil { + return err + } + log.Logger().Info(output) + } else { + // git fetch --tags upstream master + cmd := util.Command{ + Name: "git", + Args: []string{"fetch", "--tags", "upstream", c.DefaultBranch}, + } + output, err := Runner.RunWithoutRetry(&cmd) + if err != nil { + return err + } + log.Logger().Info(output) + + // git rebase upstream/master + cmd = util.Command{ + Name: "git", + Args: []string{"rebase", fmt.Sprintf("upstream/%s", c.DefaultBranch)}, + } + output, err = Runner.RunWithoutRetry(&cmd) + if err != nil { + return err + } + log.Logger().Info(output) + + // git push origin master + cmd = util.Command{ + Name: "git", + Args: []string{"push", "origin", c.DefaultBranch}, + } + output, err = Runner.RunWithoutRetry(&cmd) + if err != nil { + return err + } + log.Logger().Info(output) } - log.Logger().Info(output) return nil } diff --git a/pkg/domain/rebase_test.go b/pkg/domain/rebase_test.go new file mode 100644 index 0000000..e4bb765 --- /dev/null +++ b/pkg/domain/rebase_test.go @@ -0,0 +1,107 @@ +package domain_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/plumming/dx/pkg/api" + + "github.com/plumming/dx/pkg/domain" + "github.com/plumming/dx/pkg/util" + "github.com/plumming/dx/pkg/util/mocks" + "github.com/stretchr/testify/assert" +) + +func TestCanRebase(t *testing.T) { + type test struct { + name string + remotes string + defaultBranch string + expected []string + } + + tests := []test{ + { + name: "simple rebase on master", + remotes: `origin https://github.com/origin/clone (fetch) +origin https://github.com/origin/clone (push) +upstream https://github.com/upstream/repo (fetch) +upstream https://github.com/upstream/repo (push)`, + defaultBranch: "master", + expected: []string{ + "git remote -v", + "git remote -v", + "git status --porcelain", + "git branch --show-current", + "git fetch --tags upstream master", + "git rebase upstream/master", + "git push origin master", + }, + }, + { + name: "simple rebase on main", + remotes: `origin https://github.com/origin/clone (fetch) +origin https://github.com/origin/clone (push) +upstream https://github.com/upstream/repo (fetch) +upstream https://github.com/upstream/repo (push)`, + defaultBranch: "main", + expected: []string{ + "git remote -v", + "git remote -v", + "git status --porcelain", + "git branch --show-current", + "git fetch --tags upstream main", + "git rebase upstream/main", + "git push origin main", + }, + }, + { + name: "simple rebase on main with no upstream", + remotes: `origin https://github.com/origin/clone (fetch) +origin https://github.com/origin/clone (push)`, + defaultBranch: "main", + expected: []string{ + "git remote -v", + "git remote -v", + "git status --porcelain", + "git branch --show-current", + "git pull --tags origin main", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + http := &api.FakeHTTP{} + client := api.NewClient(api.ReplaceTripper(http)) + + http.StubResponse(200, bytes.NewBufferString(fmt.Sprintf("{ \"default_branch\":\"%s\"}", tc.defaultBranch))) + + rb := domain.NewRebase() + rb.DefaultBranch = tc.defaultBranch + rb.SetGithubClient(client) + + r := mocks.MockCommandRunner{} + domain.Runner = &r + mocks.GetRunWithoutRetryFunc = func(c *util.Command) (string, error) { + if c.String() == "git branch --show-current" { + return tc.defaultBranch, nil + } + + if c.String() == "git remote -v" { + return tc.remotes, nil + } + + return "", nil + } + + err := rb.Validate() + assert.NoError(t, err) + + err = rb.Run() + assert.NoError(t, err) + assert.Equal(t, tc.expected, r.Commands) + }) + } +} diff --git a/pkg/util/command.go b/pkg/util/command.go index 4967a4c..13b8251 100644 --- a/pkg/util/command.go +++ b/pkg/util/command.go @@ -8,6 +8,11 @@ import ( "strings" ) +// CommandRunner interface that wraps the RunWithoutRetry function. +type CommandRunner interface { + RunWithoutRetry(c *Command) (string, error) +} + // Command is a struct containing the details of an external command to be executed. type Command struct { attempts int @@ -115,19 +120,6 @@ func (c *Command) Error() error { return nil } -// RunWithoutRetry Execute the command without retrying on failure and block waiting for return values. -func (c *Command) RunWithoutRetry() (string, error) { - var r string - var e error - - r, e = c.run() - c.attempts++ - if e != nil { - c.Errors = append(c.Errors, e) - } - return r, e -} - func (c *Command) String() string { var builder strings.Builder for k, v := range c.Env { @@ -144,7 +136,23 @@ func (c *Command) String() string { return builder.String() } -func (c *Command) run() (string, error) { +type DefaultCommandRunner struct { +} + +// RunWithoutRetry Execute the command without retrying on failure and block waiting for return values. +func (d DefaultCommandRunner) RunWithoutRetry(c *Command) (string, error) { + var r string + var e error + + r, e = d.run(c) + c.attempts++ + if e != nil { + c.Errors = append(c.Errors, e) + } + return r, e +} + +func (d *DefaultCommandRunner) run(c *Command) (string, error) { e := exec.Command(c.Name, c.Args...) // #nosec if c.Dir != "" { e.Dir = c.Dir diff --git a/pkg/util/mocks/command.go b/pkg/util/mocks/command.go new file mode 100644 index 0000000..8d8a557 --- /dev/null +++ b/pkg/util/mocks/command.go @@ -0,0 +1,22 @@ +package mocks + +import ( + "github.com/plumming/dx/pkg/util" +) + +// MockCommandRunner is the mock command. +type MockCommandRunner struct { + RunWithoutRetryFunc func(c *util.Command) (string, error) + Commands []string +} + +var ( + // GetRunWithoutRetryFunc fetches the mock command's `RunWithoutRetry` func. + GetRunWithoutRetryFunc func(c *util.Command) (string, error) +) + +// RunWithoutRetry is the mock command's `RunWithoutRetry` func. +func (m *MockCommandRunner) RunWithoutRetry(c *util.Command) (string, error) { + m.Commands = append(m.Commands, c.String()) + return GetRunWithoutRetryFunc(c) +}