Skip to content

Commit d791145

Browse files
committed
Fix rebase boundaries for recovery and regular merges
Keep named trunk refs for legacy fork-point discovery and preserve recorded bases already integrated by regular merges.
1 parent 1ea5db0 commit d791145

4 files changed

Lines changed: 108 additions & 1 deletion

File tree

‎cmd/rebase.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,8 @@ func continueRebase(cfg *config.Config, gitDir string) error {
454454
NeedsOnto: state.UseOnto,
455455
OntoOldBase: state.OntoOldBase,
456456
CommitterDateIsAuthorDate: state.CommitterDateIsAuthorDate,
457-
TrunkRef: trunkBase,
457+
TrunkRef: trunkRef,
458+
TrunkSHA: state.TrunkSHA,
458459
})
459460

460461
if result.Err != nil {

‎cmd/rebase_test.go‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2544,6 +2544,52 @@ func TestRebase_Issue512StaleRecordedBase(t *testing.T) {
25442544
})
25452545
}
25462546

2547+
func testIssue512RepeatedRegularMerge(t *testing.T, run func(*config.Config) error) {
2548+
t.Helper()
2549+
repo := setupIssue512Repo(t)
2550+
issue250WriteFile(t, repo.writer, "trunk-only.txt", "before merge\n")
2551+
issue250Git(t, repo.writer, "add", ".")
2552+
issue250Git(t, repo.writer, "commit", "-m", "trunk before merge")
2553+
issue250Git(t, repo.writer, "merge", "--no-ff", "origin/parent", "-m", "merge parent")
2554+
issue250Git(t, repo.writer, "push", "origin", "main")
2555+
withIssue250Repo(t, repo.dir)
2556+
cfg := issue512Config(t, repo.mergedHead)
2557+
require.NoError(t, run(cfg))
2558+
assertIssue512History(t, repo)
2559+
2560+
issue250Git(t, repo.dir, "branch", "-D", "parent")
2561+
sf, err := stack.Load(repo.gitDir)
2562+
require.NoError(t, err)
2563+
sf.Stacks[0].Branches[0].Head = ""
2564+
require.NoError(t, stack.Save(repo.gitDir, sf))
2565+
issue250WriteFile(t, repo.writer, "trunk-only.txt", "after merge\n")
2566+
issue250Git(t, repo.writer, "add", ".")
2567+
issue250Git(t, repo.writer, "commit", "-m", "trunk after merge")
2568+
issue250Git(t, repo.writer, "push", "origin", "main")
2569+
2570+
require.NoError(t, run(cfg))
2571+
assertIssue512History(t, repo)
2572+
assert.Equal(t, "after merge", issue250Git(t, repo.dir, "show", "child:trunk-only.txt"))
2573+
}
2574+
2575+
func TestRebase_Issue512RepeatedRegularMerge(t *testing.T) {
2576+
testIssue512RepeatedRegularMerge(t, func(cfg *config.Config) error {
2577+
return runRebase(cfg, &rebaseOptions{remote: "origin"})
2578+
})
2579+
}
2580+
2581+
func TestRebase_MergedHeadPreservesUnmergedRecordedBase(t *testing.T) {
2582+
restore := git.SetOps(&git.MockOps{
2583+
IsAncestorFn: func(a, b string) (bool, error) {
2584+
return !(a == "unmerged-local" && b == "main"), nil
2585+
},
2586+
})
2587+
defer restore()
2588+
base, err := resolveMergedRebaseOldBase("merged", "unmerged-local", "unmerged-local", "main", "child")
2589+
require.NoError(t, err)
2590+
assert.Equal(t, "merged", base, "a recorded base not in the destination must not discard unmerged work")
2591+
}
2592+
25472593
func TestRebase_Issue512SelectedRangeWithPrunedParent(t *testing.T) {
25482594
for _, mode := range []string{"upstack", "downstack", "no-trunk"} {
25492595
t.Run(mode, func(t *testing.T) {
@@ -3458,6 +3504,44 @@ func TestRebase_LegacyContinuePersistsCatalogUnderOperationLock(t *testing.T) {
34583504
assert.ErrorIs(t, err, os.ErrNotExist)
34593505
}
34603506

3507+
func TestRebase_LegacyContinueKeepsNamedTrunkForForkPoint(t *testing.T) {
3508+
dir := t.TempDir()
3509+
writeStackFile(t, dir, stack.Stack{
3510+
Trunk: stack.BranchRef{Branch: "main"},
3511+
Branches: []stack.BranchRef{
3512+
{Branch: "parent", PullRequest: &stack.PullRequestRef{Number: 10, Merged: true}},
3513+
{Branch: "child", Base: "recorded"},
3514+
},
3515+
})
3516+
require.NoError(t, saveRebaseState(dir, &rebaseState{
3517+
OriginalBranch: "child", ConflictBranch: "parent", RemainingBranches: []string{"child"},
3518+
OriginalRefs: map[string]string{"parent": "stale", "child": "before"},
3519+
UseOnto: true, OntoOldBase: "stale", TrunkRef: "main", TrunkSHA: "pinned",
3520+
}))
3521+
mock := newRebaseMock(dir, "child")
3522+
mock.IsAncestorFn = func(a, b string) (bool, error) { return a != "stale", nil }
3523+
mock.MergeBaseForkPointFn = func(ref, branch string) (string, error) {
3524+
assert.Equal(t, "main", ref)
3525+
if ref != "main" {
3526+
return "", fmt.Errorf("No such ref: %s", ref)
3527+
}
3528+
return "", nil
3529+
}
3530+
rebased := false
3531+
mock.RebaseOntoFn = func(target, oldBase, branch string, _ git.RebaseOpts) error {
3532+
rebased = true
3533+
assert.Equal(t, "pinned", target)
3534+
assert.Equal(t, "recorded", oldBase)
3535+
assert.Equal(t, "child", branch)
3536+
return nil
3537+
}
3538+
restore := git.SetOps(mock)
3539+
defer restore()
3540+
require.NoError(t, runRebase(issue250TestConfig(t), &rebaseOptions{cont: true}))
3541+
assert.True(t, rebased)
3542+
assert.NoFileExists(t, filepath.Join(dir, rebaseStateFile))
3543+
}
3544+
34613545
func TestRebase_LegacyPhaseGuards(t *testing.T) {
34623546
for _, tc := range []struct {
34633547
phase string

‎cmd/sync_test.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2681,6 +2681,12 @@ func TestSync_Issue512StaleRecordedBase(t *testing.T) {
26812681
})
26822682
}
26832683

2684+
func TestSync_Issue512RepeatedRegularMerge(t *testing.T) {
2685+
testIssue512RepeatedRegularMerge(t, func(cfg *config.Config) error {
2686+
return runSync(cfg, &syncOptions{remote: "origin"})
2687+
})
2688+
}
2689+
26842690
func TestSync_Issue512WorktreePruneRepeatedRebase(t *testing.T) {
26852691
repo := setupIssue512Repo(t)
26862692
squash := issue512Squash(t, repo)

‎cmd/utils.go‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,6 +1349,22 @@ func resolveMergedRebaseOldBase(mergedHead, localTip, recordedBase, newBase, bra
13491349
if valid, err := rebaseBoundaryContains(mergedHead, branch); err != nil {
13501350
return "", err
13511351
} else if valid {
1352+
if recordedBase != "" && recordedBase != mergedHead {
1353+
// A regular merge keeps the PR head reachable through a newer base.
1354+
// Only skip that extra history if the destination contains it too.
1355+
for _, pair := range [][2]string{
1356+
{recordedBase, branch}, {mergedHead, recordedBase}, {recordedBase, newBase},
1357+
} {
1358+
contains, err := rebaseBoundaryContains(pair[0], pair[1])
1359+
if err != nil {
1360+
return "", err
1361+
}
1362+
if !contains {
1363+
return mergedHead, nil
1364+
}
1365+
}
1366+
return recordedBase, nil
1367+
}
13521368
return mergedHead, nil
13531369
}
13541370
return resolveRebaseOldBase(localTip, recordedBase, newBase, branch)

0 commit comments

Comments
 (0)