Skip to content

Commit

Permalink
#80 Refresh from upstream (#99)
Browse files Browse the repository at this point in the history
* new g/gh methods and clone step

* correct typo

* g and gh tests

* test amendments

* add tests for new methods

* test cases to fail on specific methods

* trim newline from output

* use iota for constants

---------

Co-authored-by: Danny Ranson <danny.ranson@skyscanner.net>
  • Loading branch information
Dan7-7-7 and Danny Ranson authored Oct 12, 2023
1 parent 3d30dd8 commit 24dc087
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 15 deletions.
19 changes: 19 additions & 0 deletions cmd/clone/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,25 @@ func run(c *cobra.Command, _ []string) {
continue
}
createBranchActivity.EndWithSuccess()

if !nofork {
pullFromUpstreamActivity := logger.StartActivity("Pulling latest changes from %s", repo.FullRepoName)
var defaultBranch string
defaultBranch, err = gh.GetDefaultBranchName(pullFromUpstreamActivity.Writer(), repoDirPath, repo.FullRepoName)
if err != nil {
pullFromUpstreamActivity.EndWithFailure(err)
errorCount++
continue
}
err = g.Pull(pullFromUpstreamActivity.Writer(), repoDirPath, "upstream", defaultBranch)
if err != nil {
pullFromUpstreamActivity.EndWithFailure(err)
errorCount++
continue
}
pullFromUpstreamActivity.EndWithSuccess()
}

doneCount++
}

Expand Down
108 changes: 108 additions & 0 deletions cmd/clone/clone_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,112 @@ func TestItLogsCheckoutErrorsButContinuesToTryAll(t *testing.T) {
})
}

func TestItPullsFromUpstreamWhenCloningWithFork(t *testing.T) {
fakeGitHub := github.NewAlwaysSucceedsFakeGitHub()
gh = fakeGitHub
fakeGit := git.NewAlwaysSucceedsFakeGit()
g = fakeGit

testsupport.PrepareTempCampaign(false, "org1/repo1", "org2/repo2")

out, err := runCloneCommandWithFork()
assert.NoError(t, err)
assert.Contains(t, out, "Pulling latest changes from org1/repo1")
assert.Contains(t, out, "Pulling latest changes from org2/repo2")
assert.Contains(t, out, "turbolift clone completed (2 repos cloned, 0 repos skipped)")

fakeGitHub.AssertCalledWith(t, [][]string{
{"work/org1", "org1/repo1"},
{"work/org1/repo1", "org1/repo1"},
{"work/org2", "org2/repo2"},
{"work/org2/repo2", "org2/repo2"},
})
fakeGit.AssertCalledWith(t, [][]string{
{"checkout", "work/org1/repo1", testsupport.Pwd()},
{"pull", "--ff-only", "work/org1/repo1", "upstream", "main"},
{"checkout", "work/org2/repo2", testsupport.Pwd()},
{"pull", "--ff-only", "work/org2/repo2", "upstream", "main"},
})
}

func TestItDoesNotPullFromUpstreamWhenCloningWithoutFork(t *testing.T) {
fakeGitHub := github.NewAlwaysSucceedsFakeGitHub()
gh = fakeGitHub
fakeGit := git.NewAlwaysSucceedsFakeGit()
g = fakeGit

testsupport.PrepareTempCampaign(false, "org1/repo1", "org2/repo2")

out, err := runCloneCommand()
assert.NoError(t, err)
assert.NotContains(t, out, "Pulling latest changes from org1/repo1")
assert.NotContains(t, out, "Pulling latest changes from org2/repo2")
assert.Contains(t, out, "turbolift clone completed (2 repos cloned, 0 repos skipped)")

fakeGitHub.AssertCalledWith(t, [][]string{
{"work/org1", "org1/repo1"},
{"work/org2", "org2/repo2"},
})
fakeGit.AssertCalledWith(t, [][]string{
{"checkout", "work/org1/repo1", testsupport.Pwd()},
{"checkout", "work/org2/repo2", testsupport.Pwd()},
})
}

func TestItLogsDefaultBranchErrorsButContinuesToTryAll(t *testing.T) {
fakeGitHub := github.NewAlwaysFailsOnGetDefaultBranchFakeGitHub()
gh = fakeGitHub
fakeGit := git.NewAlwaysSucceedsFakeGit()
g = fakeGit

testsupport.PrepareTempCampaign(false, "org1/repo1", "org2/repo2")
out, err := runCloneCommandWithFork()
assert.NoError(t, err)
assert.Contains(t, out, "Pulling latest changes from org1/repo1")
assert.Contains(t, out, "Pulling latest changes from org2/repo2")
assert.Contains(t, out, "turbolift clone completed with errors")
assert.Contains(t, out, "2 repos errored")

fakeGitHub.AssertCalledWith(t, [][]string{
{"work/org1", "org1/repo1"},
{"work/org1/repo1", "org1/repo1"},
{"work/org2", "org2/repo2"},
{"work/org2/repo2", "org2/repo2"},
})
fakeGit.AssertCalledWith(t, [][]string{
{"checkout", "work/org1/repo1", testsupport.Pwd()},
{"checkout", "work/org2/repo2", testsupport.Pwd()},
})
}

func TestItLogsPullErrorsButContinuesToTryAll(t *testing.T) {
fakeGitHub := github.NewAlwaysSucceedsFakeGitHub()
gh = fakeGitHub
fakeGit := git.NewAlwaysFailsOnPullFakeGit()
g = fakeGit

testsupport.PrepareTempCampaign(false, "org1/repo1", "org2/repo2")
out, err := runCloneCommandWithFork()
assert.NoError(t, err)
assert.Contains(t, out, "Pulling latest changes from org1/repo1")
assert.Contains(t, out, "Pulling latest changes from org2/repo2")
assert.Contains(t, out, "turbolift clone completed with errors")
assert.Contains(t, out, "2 repos errored")

fakeGitHub.AssertCalledWith(t, [][]string{
{"work/org1", "org1/repo1"},
{"work/org1/repo1", "org1/repo1"},
{"work/org2", "org2/repo2"},
{"work/org2/repo2", "org2/repo2"},
})
fakeGit.AssertCalledWith(t, [][]string{
{"checkout", "work/org1/repo1", testsupport.Pwd()},
{"pull", "--ff-only", "work/org1/repo1", "upstream", "main"},
{"checkout", "work/org2/repo2", testsupport.Pwd()},
{"pull", "--ff-only", "work/org2/repo2", "upstream", "main"},
})
}

func TestItClonesReposFoundInReposFile(t *testing.T) {
fakeGitHub := github.NewAlwaysSucceedsFakeGitHub()
gh = fakeGitHub
Expand Down Expand Up @@ -195,9 +301,11 @@ func TestItSkipsCloningIfAWorkingCopyAlreadyExists(t *testing.T) {

fakeGitHub.AssertCalledWith(t, [][]string{
{"work/org", "org/repo2"},
{"work/org/repo2", "org/repo2"},
})
fakeGit.AssertCalledWith(t, [][]string{
{"checkout", "work/org/repo2", testsupport.Pwd()},
{"pull", "--ff-only", "work/org/repo2", "upstream", "main"},
})
}

Expand Down
16 changes: 16 additions & 0 deletions internal/git/fake_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ func (f *FakeGit) Push(output io.Writer, workingDir string, _ string, branchName
return err
}

func (f *FakeGit) Pull(output io.Writer, workingDir string, remote string, branchName string) error {
call := []string{"pull", "--ff-only", workingDir, remote, branchName}
f.calls = append(f.calls, call)
_, err := f.handler(output, call)
return err
}

func (f *FakeGit) AssertCalledWith(t *testing.T, expected [][]string) {
assert.Equal(t, expected, f.calls)
}
Expand All @@ -77,3 +84,12 @@ func NewAlwaysFailsFakeGit() *FakeGit {
return false, errors.New("synthetic error")
})
}

func NewAlwaysFailsOnPullFakeGit() *FakeGit {
return NewFakeGit(func(_ io.Writer, args []string) (bool, error) {
if args[0] == "pull" {
return false, errors.New("synthetic error")
}
return true, nil
})
}
5 changes: 5 additions & 0 deletions internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Git interface {
Push(stdout io.Writer, workingDir string, remote string, branchName string) error
Commit(output io.Writer, workingDir string, message string) error
IsRepoChanged(output io.Writer, workingDir string) (bool, error)
Pull(output io.Writer, workingDir string, remote string, branchName string) error
}

type RealGit struct {
Expand Down Expand Up @@ -66,6 +67,10 @@ func (r *RealGit) IsRepoChanged(output io.Writer, workingDir string) (bool, erro
return diffSize > 0, nil
}

func (r *RealGit) Pull(output io.Writer, workingDir string, remote string, branchName string) error {
return execInstance.Execute(output, workingDir, "git", "pull", "--ff-only", remote, branchName)
}

func NewRealGit() *RealGit {
return &RealGit{}
}
41 changes: 36 additions & 5 deletions internal/git/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,55 @@ import (
"testing"
)

func TestItReturnsErrorOnFailure(t *testing.T) {
func TestItReturnsErrorOnFailedCheckout(t *testing.T) {
fakeExecutor := executor.NewAlwaysFailsFakeExecutor()
execInstance = fakeExecutor

_, err := runAndCaptureOutput()
_, err := runCheckoutAndCaptureOutput()
assert.Error(t, err)

fakeExecutor.AssertCalledWith(t, [][]string{
{"work/org/repo1", "git", "checkout", "-b", "some_branch"},
})
}

func TestItReturnsNilErrorOnSuccess(t *testing.T) {
func TestItReturnsNilErrorOnSuccessfulCheckout(t *testing.T) {
fakeExecutor := executor.NewAlwaysSucceedsFakeExecutor()
execInstance = fakeExecutor

_, err := runAndCaptureOutput()
_, err := runCheckoutAndCaptureOutput()
assert.NoError(t, err)

fakeExecutor.AssertCalledWith(t, [][]string{
{"work/org/repo1", "git", "checkout", "-b", "some_branch"},
})
}

func runAndCaptureOutput() (string, error) {
func TestItReturnsErrorOnFailedPull(t *testing.T) {
fakeExecutor := executor.NewAlwaysFailsFakeExecutor()
execInstance = fakeExecutor

_, err := runPullAndCaptureOutput()
assert.Error(t, err)

fakeExecutor.AssertCalledWith(t, [][]string{
{"work/org1/repo1", "git", "pull", "--ff-only", "upstream", "main"},
})
}

func TestItReturnsNilErrorOnSuccessfulPull(t *testing.T) {
fakeExecutor := executor.NewAlwaysSucceedsFakeExecutor()
execInstance = fakeExecutor

_, err := runPullAndCaptureOutput()
assert.NoError(t, err)

fakeExecutor.AssertCalledWith(t, [][]string{
{"work/org1/repo1", "git", "pull", "--ff-only", "upstream", "main"},
})
}

func runCheckoutAndCaptureOutput() (string, error) {
sb := strings.Builder{}
err := NewRealGit().Checkout(&sb, "work/org/repo1", "some_branch")

Expand All @@ -55,3 +79,10 @@ func runAndCaptureOutput() (string, error) {
}
return sb.String(), nil
}

func runPullAndCaptureOutput() (string, error) {
sb := strings.Builder{}
err := NewRealGit().Pull(&sb, "work/org1/repo1", "upstream", "main")

return sb.String(), err
}
47 changes: 37 additions & 10 deletions internal/github/fake_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,33 @@ import (
)

type FakeGitHub struct {
handler func(output io.Writer, workingDir string, fullRepoName string) (bool, error)
handler func(output io.Writer, command Command, workingDir string, fullRepoName string) (bool, error)
returningHandler func(output io.Writer, workingDir string) (interface{}, error)
calls [][]string
}

func (f *FakeGitHub) CreatePullRequest(output io.Writer, workingDir string, metadata PullRequest) (didCreate bool, err error) {
f.calls = append(f.calls, []string{workingDir, metadata.Title})
return f.handler(output, workingDir, "")
return f.handler(output, CreatePullRequest, workingDir, "")
}

func (f *FakeGitHub) ForkAndClone(output io.Writer, workingDir string, fullRepoName string) error {
f.calls = append(f.calls, []string{workingDir, fullRepoName})
_, err := f.handler(output, workingDir, fullRepoName)
_, err := f.handler(output, ForkAndClone, workingDir, fullRepoName)
return err
}

func (f *FakeGitHub) Clone(output io.Writer, workingDir string, fullRepoName string) error {
f.calls = append(f.calls, []string{workingDir, fullRepoName})
_, err := f.handler(output, workingDir, fullRepoName)
_, err := f.handler(output, Clone, workingDir, fullRepoName)
return err
}

func (f *FakeGitHub) ClosePullRequest(output io.Writer, workingDir string, branchName string) error {
// TODO: handle this differently; branchName here is replacing fullRepoName
// This is OK for now because fullRepoName is used nowhere in the github mocks
f.calls = append(f.calls, []string{workingDir, branchName})
_, err := f.handler(output, workingDir, branchName)
_, err := f.handler(output, ClosePullRequest, workingDir, branchName)
return err
}

Expand All @@ -63,11 +63,17 @@ func (f *FakeGitHub) GetPR(output io.Writer, workingDir string, _ string) (*PrSt
return result.(*PrStatus), err
}

func (f *FakeGitHub) GetDefaultBranchName(output io.Writer, workingDir string, fullRepoName string) (string, error) {
f.calls = append(f.calls, []string{workingDir, fullRepoName})
_, err := f.handler(output, GetDefaultBranchName, workingDir, fullRepoName)
return "main", err
}

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

func NewFakeGitHub(h func(output io.Writer, workingDir string, fullRepoName string) (bool, error), r func(output io.Writer, workingDir string) (interface{}, error)) *FakeGitHub {
func NewFakeGitHub(h func(output io.Writer, command Command, workingDir string, fullRepoName string) (bool, error), r func(output io.Writer, workingDir string) (interface{}, error)) *FakeGitHub {
return &FakeGitHub{
handler: h,
returningHandler: r,
Expand All @@ -76,33 +82,54 @@ func NewFakeGitHub(h func(output io.Writer, workingDir string, fullRepoName stri
}

func NewAlwaysSucceedsFakeGitHub() *FakeGitHub {
return NewFakeGitHub(func(output io.Writer, workingDir string, fullRepoName string) (bool, error) {
return NewFakeGitHub(func(output io.Writer, command Command, workingDir string, fullRepoName string) (bool, error) {
return true, nil
}, func(output io.Writer, workingDir string) (interface{}, error) {
return PrStatus{}, nil
})
}

func NewAlwaysFailsFakeGitHub() *FakeGitHub {
return NewFakeGitHub(func(output io.Writer, workingDir string, fullRepoName string) (bool, error) {
return NewFakeGitHub(func(output io.Writer, command Command, workingDir string, fullRepoName string) (bool, error) {
return false, errors.New("synthetic error")
}, func(output io.Writer, workingDir string) (interface{}, error) {
return nil, errors.New("synthetic error")
})
}

func NewAlwaysThrowNoPRFound() *FakeGitHub {
return NewFakeGitHub(func(output io.Writer, workingDir string, branchName string) (bool, error) {
return NewFakeGitHub(func(output io.Writer, command Command, workingDir string, branchName string) (bool, error) {
return false, &NoPRFoundError{Path: workingDir, BranchName: branchName}
}, func(output io.Writer, workingDir string) (interface{}, error) {
panic("should not be invoked")
})
}

func NewAlwaysReturnsFalseFakeGitHub() *FakeGitHub {
return NewFakeGitHub(func(output io.Writer, workingDir string, fullRepoName string) (bool, error) {
return NewFakeGitHub(func(output io.Writer, command Command, workingDir string, fullRepoName string) (bool, error) {
return false, nil
}, func(output io.Writer, workingDir string) (interface{}, error) {
return PrStatus{}, nil
})
}

func NewAlwaysFailsOnGetDefaultBranchFakeGitHub() *FakeGitHub {
return NewFakeGitHub(func(output io.Writer, command Command, workingDir string, fullRepoName string) (bool, error) {
if command == GetDefaultBranchName {
return false, errors.New("synthetic error")
}
return true, nil
}, func(output io.Writer, workingDir string) (interface{}, error) {
return PrStatus{}, nil
})
}

type Command int

const (
ForkAndClone Command = iota
Clone
CreatePullRequest
ClosePullRequest
GetDefaultBranchName
)
Loading

0 comments on commit 24dc087

Please sign in to comment.