Skip to content

Commit 03c4b92

Browse files
committed
enable git rerere
1 parent f1421ad commit 03c4b92

4 files changed

Lines changed: 106 additions & 14 deletions

File tree

cmd/init.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ func runInit(cfg *config.Config, opts *initOptions) error {
5353

5454
// Determine trunk branch
5555
trunk := opts.base
56+
57+
// Enable git rerere so conflict resolutions are remembered.
58+
_ = git.EnableRerere()
59+
5660
if trunk == "" {
5761
trunk, err = git.DefaultBranch()
5862
if err != nil {

cmd/rebase.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
111111
}
112112

113113
cfg.Printf("Fetching origin ...")
114+
115+
// Enable git rerere so conflict resolutions are remembered.
116+
_ = git.EnableRerere()
117+
114118
if err := git.Fetch("origin"); err != nil {
115119
cfg.Warningf("Failed to fetch origin: %v", err)
116120
} else {
@@ -227,11 +231,22 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
227231
} else {
228232
cfg.Printf("Rebasing %s onto %s ...", br.Branch, base)
229233

230-
if err := git.CheckoutBranch(br.Branch); err != nil {
231-
return fmt.Errorf("checking out %s: %w", br.Branch, err)
234+
var rebaseErr error
235+
if absIdx > 0 {
236+
// Use --onto to replay only this branch's unique commits.
237+
// Without --onto, git may try to replay commits shared with
238+
// the parent, causing duplicate-patch conflicts when the
239+
// parent's rebase rewrote those commits.
240+
rebaseErr = git.RebaseOnto(base, originalRefs[base], br.Branch)
241+
} else {
242+
if err := git.CheckoutBranch(br.Branch); err != nil {
243+
return fmt.Errorf("checking out %s: %w", br.Branch, err)
244+
}
245+
// Use regular rebase for the first branch.
246+
rebaseErr = git.Rebase(base)
232247
}
233248

234-
if err := git.Rebase(base); err != nil {
249+
if rebaseErr != nil {
235250
cfg.Warningf("Rebasing %s onto %s ... conflict", br.Branch, base)
236251

237252
remaining := make([]string, 0)
@@ -410,12 +425,19 @@ func continueRebase(cfg *config.Config, gitDir string) error {
410425
} else {
411426
cfg.Printf("Rebasing %s onto %s ...", branchName, base)
412427

413-
if err := git.CheckoutBranch(branchName); err != nil {
414-
cfg.Errorf("checking out %s: %s", branchName, err)
415-
return nil
428+
var rebaseErr error
429+
if idx > 0 {
430+
// Use --onto to replay only this branch's unique commits.
431+
rebaseErr = git.RebaseOnto(base, state.OriginalRefs[base], branchName)
432+
} else {
433+
if err := git.CheckoutBranch(branchName); err != nil {
434+
cfg.Errorf("checking out %s: %s", branchName, err)
435+
return nil
436+
}
437+
rebaseErr = git.Rebase(base)
416438
}
417439

418-
if err := git.Rebase(base); err != nil {
440+
if rebaseErr != nil {
419441
remainIdx := -1
420442
for ri, rb := range state.RemainingBranches {
421443
if rb == branchName {

cmd/sync.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ func runSync(cfg *config.Config, _ *syncOptions) error {
7474

7575
// --- Step 1: Fetch ---
7676
cfg.Printf("Fetching origin ...")
77+
78+
// Enable git rerere so conflict resolutions are remembered.
79+
_ = git.EnableRerere()
80+
7781
if err := git.Fetch("origin"); err != nil {
7882
cfg.Warningf("Failed to fetch origin: %v", err)
7983
} else {
@@ -188,13 +192,20 @@ func runSync(cfg *config.Config, _ *syncOptions) error {
188192
cfg.Successf("Rebased %s onto %s (squash-merge detected)", br.Branch, newBase)
189193
ontoOldBase = originalRefs[br.Branch]
190194
} else {
191-
if err := git.CheckoutBranch(br.Branch); err != nil {
192-
cfg.Errorf("Failed to checkout %s: %v", br.Branch, err)
193-
conflicted = true
194-
break
195+
var rebaseErr error
196+
if i > 0 {
197+
// Use --onto to replay only this branch's unique commits.
198+
rebaseErr = git.RebaseOnto(base, originalRefs[base], br.Branch)
199+
} else {
200+
if err := git.CheckoutBranch(br.Branch); err != nil {
201+
cfg.Errorf("Failed to checkout %s: %v", br.Branch, err)
202+
conflicted = true
203+
break
204+
}
205+
rebaseErr = git.Rebase(base)
195206
}
196207

197-
if err := git.Rebase(base); err != nil {
208+
if rebaseErr != nil {
198209
// Conflict detected — abort and restore everything
199210
if git.IsRebaseInProgress() {
200211
_ = git.RebaseAbort()

internal/git/git.go

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,23 @@ func Push(remote string, branches []string, force, atomic bool) error {
106106
}
107107

108108
// Rebase rebases the current branch onto the given base.
109+
// If rerere resolves all conflicts automatically, the rebase continues
110+
// without user intervention.
109111
func Rebase(base string) error {
110-
return runSilent("rebase", base)
112+
err := runSilent("rebase", base)
113+
if err == nil {
114+
return nil
115+
}
116+
return tryAutoResolveRebase(err)
117+
}
118+
119+
// EnableRerere enables git rerere (reuse recorded resolution) and
120+
// rerere.autoupdate (auto-stage resolved files) for the repository.
121+
func EnableRerere() error {
122+
if err := runSilent("config", "rerere.enabled", "true"); err != nil {
123+
return err
124+
}
125+
return runSilent("config", "rerere.autoupdate", "true")
111126
}
112127

113128
// RebaseOnto rebases a branch using the three-argument form:
@@ -117,19 +132,59 @@ func Rebase(base string) error {
117132
// This replays commits after oldBase from branch onto newBase. It is used
118133
// when a prior branch was squash-merged and the normal rebase cannot detect
119134
// which commits have already been applied.
135+
// If rerere resolves all conflicts automatically, the rebase continues
136+
// without user intervention.
120137
func RebaseOnto(newBase, oldBase, branch string) error {
121-
return runSilent("rebase", "--onto", newBase, oldBase, branch)
138+
err := runSilent("rebase", "--onto", newBase, oldBase, branch)
139+
if err == nil {
140+
return nil
141+
}
142+
return tryAutoResolveRebase(err)
122143
}
123144

124145
// RebaseContinue continues an in-progress rebase.
125146
// It sets GIT_EDITOR=true to prevent git from opening an interactive editor
126147
// for the commit message, which would cause the command to hang.
148+
// If rerere resolves subsequent conflicts automatically, the rebase continues
149+
// without user intervention.
127150
func RebaseContinue() error {
151+
err := rebaseContinueOnce()
152+
if err == nil {
153+
return nil
154+
}
155+
return tryAutoResolveRebase(err)
156+
}
157+
158+
// rebaseContinueOnce runs a single git rebase --continue without auto-resolve.
159+
func rebaseContinueOnce() error {
128160
cmd := exec.Command("git", "rebase", "--continue")
129161
cmd.Env = append(os.Environ(), "GIT_EDITOR=true")
130162
return cmd.Run()
131163
}
132164

165+
// tryAutoResolveRebase checks whether rerere has resolved all conflicts
166+
// from a failed rebase. If so, it auto-continues the rebase (potentially
167+
// multiple times for multi-commit rebases). Returns originalErr if any
168+
// conflicts remain that need manual resolution.
169+
func tryAutoResolveRebase(originalErr error) error {
170+
for i := 0; i < 1000; i++ {
171+
if !IsRebaseInProgress() {
172+
return nil
173+
}
174+
conflicts, _ := ConflictedFiles()
175+
if len(conflicts) > 0 {
176+
return originalErr
177+
}
178+
// Rerere resolved all conflicts — auto-continue.
179+
if rebaseContinueOnce() == nil {
180+
return nil
181+
}
182+
// Continue hit another conflicting commit; loop to check
183+
// if rerere resolved that one too.
184+
}
185+
return originalErr
186+
}
187+
133188
// RebaseAbort aborts an in-progress rebase.
134189
func RebaseAbort() error {
135190
return runSilent("rebase", "--abort")

0 commit comments

Comments
 (0)