diff --git a/CHANGELOG.md b/CHANGELOG.md index 26e7baa..8eccd3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ Exceptions are acceptable depending on the circumstances (critical bug fixes tha - changed the Go module dependencies to their latest versions - changed per-repo logging in parallel operations to run inside goroutines for progressive feedback +### Fixed + +- fixed `RestoreAfterSync` to stay on the default branch after a successful sync + ## [0.4.0] - 2026-03-31 ### Added diff --git a/internal/repo/sync.go b/internal/repo/sync.go index 19351bf..eab3742 100644 --- a/internal/repo/sync.go +++ b/internal/repo/sync.go @@ -151,7 +151,7 @@ func SyncAndRestore( return SyncResult{Name: name, Status: fmt.Sprintf("FAIL (pull --rebase: %v)", err)} } - return RestoreAfterSync(repoPath, name, defaultBranch, currentBranch, wipBranch, isDirty, runner) + return FinalizeSync(repoPath, name, defaultBranch, wipBranch, isDirty, runner) } // RestoreBranch restores the appropriate branch after a failure. @@ -163,9 +163,9 @@ func RestoreBranch(repoPath, currentBranch, wipBranch string, isDirty bool, runn } } -// RestoreAfterSync restores the original branch state after a successful sync. -func RestoreAfterSync( - repoPath, name, defaultBranch, currentBranch, wipBranch string, isDirty bool, runner GitRunner, +// FinalizeSync rebases the WIP branch (if dirty) and ensures the repo ends on the default branch. +func FinalizeSync( + repoPath, name, defaultBranch, wipBranch string, isDirty bool, runner GitRunner, ) SyncResult { status := "synced" if isDirty { @@ -173,10 +173,10 @@ func RestoreAfterSync( if err := runner.Run(repoPath, "rebase", defaultBranch); err != nil { _ = runner.Run(repoPath, "rebase", "--abort") } - _ = runner.Run(repoPath, "checkout", currentBranch) + if err := runner.Run(repoPath, "checkout", defaultBranch); err != nil { + return SyncResult{Name: name, Status: fmt.Sprintf("FAIL (checkout %s: %v)", defaultBranch, err)} + } status = fmt.Sprintf("synced (wip: %s)", wipBranch) - } else if currentBranch != defaultBranch { - _ = runner.Run(repoPath, "checkout", currentBranch) } return SyncResult{Name: name, Status: status} } diff --git a/internal/repo/sync_test.go b/internal/repo/sync_test.go index 08ffc1a..19446f0 100644 --- a/internal/repo/sync_test.go +++ b/internal/repo/sync_test.go @@ -198,6 +198,26 @@ func TestSyncAndRestore(t *testing.T) { // then assert.Contains(t, result.Status, "FAIL (pull --rebase") }) + + t.Run("should stay on default branch when starting from non-default branch", func(t *testing.T) { + t.Parallel() + // given + var checkouts []string + runner := doubles.NewGitRunnerStub() + runner.RunFunc = func(_ string, args ...string) error { + if len(args) > 0 && args[0] == "checkout" { + checkouts = append(checkouts, args[1]) + } + return nil + } + + // when + result := repo.SyncAndRestore("/repo", "my-repo", "main", "feat/x", "wip/feat/x", false, runner) + + // then + assert.Equal(t, "synced", result.Status) + assert.Equal(t, []string{"main"}, checkouts) + }) } func TestRestoreBranch(t *testing.T) { @@ -242,7 +262,7 @@ func TestRestoreBranch(t *testing.T) { }) } -func TestRestoreAfterSync(t *testing.T) { +func TestFinalizeSync(t *testing.T) { t.Parallel() t.Run("should return synced for clean repo on default branch", func(t *testing.T) { @@ -251,13 +271,13 @@ func TestRestoreAfterSync(t *testing.T) { runner := doubles.NewGitRunnerStub() // when - result := repo.RestoreAfterSync("/repo", "my-repo", "main", "main", "wip/main", false, runner) + result := repo.FinalizeSync("/repo", "my-repo", "main", "wip/main", false, runner) // then assert.Equal(t, "synced", result.Status) }) - t.Run("should checkout original branch when not on default", func(t *testing.T) { + t.Run("should stay on default branch when clean repo was on non-default branch", func(t *testing.T) { t.Parallel() // given var checkedOut string @@ -270,23 +290,50 @@ func TestRestoreAfterSync(t *testing.T) { } // when - result := repo.RestoreAfterSync("/repo", "my-repo", "main", "feat/x", "wip/feat/x", false, runner) + result := repo.FinalizeSync("/repo", "my-repo", "main", "wip/feat/x", false, runner) // then assert.Equal(t, "synced", result.Status) - assert.Equal(t, "feat/x", checkedOut) + assert.Empty(t, checkedOut) }) - t.Run("should rebase WIP branch and return wip status for dirty repo", func(t *testing.T) { + t.Run("should rebase WIP branch and checkout default branch for dirty repo", func(t *testing.T) { t.Parallel() // given + var lastCheckedOut string runner := doubles.NewGitRunnerStub() + runner.RunFunc = func(_ string, args ...string) error { + if len(args) > 0 && args[0] == "checkout" { + lastCheckedOut = args[1] + } + return nil + } // when - result := repo.RestoreAfterSync("/repo", "my-repo", "main", "feat/x", "wip/feat/x", true, runner) + result := repo.FinalizeSync("/repo", "my-repo", "main", "wip/feat/x", true, runner) // then assert.Contains(t, result.Status, "synced (wip: wip/feat/x)") + assert.Equal(t, "main", lastCheckedOut) + }) + + t.Run("should return FAIL when checkout to default branch fails for dirty repo", func(t *testing.T) { + t.Parallel() + // given + runner := doubles.NewGitRunnerStub() + runner.RunFunc = func(_ string, args ...string) error { + if len(args) >= 2 && args[0] == "checkout" && args[1] == "main" { + return errors.New("checkout failed") + } + return nil + } + + // when + result := repo.FinalizeSync("/repo", "my-repo", "main", "wip/feat/x", true, runner) + + // then + assert.Contains(t, result.Status, "FAIL") + assert.Contains(t, result.Status, "checkout main") }) }