Skip to content

Commit 6daa31f

Browse files
committed
Add --committer-date-is-author-date flag to gh stack rebase
Introduce an opt-in `--committer-date-is-author-date` flag (with `--preserve-dates` alias) for `gh stack rebase`. The flag is passed through to every underlying `git rebase` invocation in the cascade, keeping committer dates equal to author dates so that identical content rebased onto an identical parent produces stable SHAs. This reduces spurious force-push notifications and noisy review timelines, especially in deep stacks where bottom branches get re-rebased on every merge. Git layer changes: - Add `RebaseOpts` struct with `CommitterDateIsAuthorDate` field to `internal/git/gitops.go` - Update `Ops` interface, `defaultOps`, public wrappers, and `rebaseContinueOnce`/`tryAutoResolveRebase` helpers to accept and forward the flag - Update `MockOps` to match the new signatures Command layer changes: - Register `--committer-date-is-author-date` and `--preserve-dates` flags on the cobra command in `cmd/rebase.go` - Add `CommitterDateIsAuthorDate` to `cascadeRebaseOpts` and thread it to all `git.Rebase`/`git.RebaseOnto` calls in `cmd/utils.go` - Persist the flag in `rebaseState` JSON so `--continue` resumes with the same behavior; pass it to `RebaseContinue` and subsequent cascade calls - Update `internal/modify/apply.go` callers to pass zero-value `RebaseOpts{}` Tests: - Update all existing mock signatures in rebase, sync, and modify tests - Add tests for flag passthrough, `--preserve-dates` alias, state round-trip, `--continue` flag restoration, and conflict state persistence Docs: - Update flag tables and examples in README.md and docs/src/content/docs/reference/cli.md
1 parent 2c93c6d commit 6daa31f

11 files changed

Lines changed: 413 additions & 138 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ If a rebase conflict occurs, the operation pauses and prints the conflicted file
209209
| `--continue` | Continue the rebase after resolving conflicts |
210210
| `--abort` | Abort the rebase and restore all branches to their pre-rebase state |
211211
| `--remote <name>` | Remote to fetch from (defaults to auto-detected remote) |
212+
| `--committer-date-is-author-date` | Preserve commit dates as the same as author dates. Alias: `--preserve-dates` |
212213

213214
| Argument | Description |
214215
|----------|-------------|
@@ -231,6 +232,9 @@ gh stack rebase --continue
231232

232233
# Abort rebase and restore everything
233234
gh stack rebase --abort
235+
236+
# Rebase and preserve committer date as author date
237+
gh stack rebase --committer-date-is-author-date
234238
```
235239

236240
### `gh stack modify`

cmd/rebase.go

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,24 @@ import (
1616
)
1717

1818
type rebaseOptions struct {
19-
branch string
20-
downstack bool
21-
upstack bool
22-
cont bool
23-
abort bool
24-
remote string
19+
branch string
20+
downstack bool
21+
upstack bool
22+
cont bool
23+
abort bool
24+
remote string
25+
committerDateIsAuthorDate bool
2526
}
2627

2728
type rebaseState struct {
28-
CurrentBranchIndex int `json:"currentBranchIndex"`
29-
ConflictBranch string `json:"conflictBranch"`
30-
RemainingBranches []string `json:"remainingBranches"`
31-
OriginalBranch string `json:"originalBranch"`
32-
OriginalRefs map[string]string `json:"originalRefs"`
33-
UseOnto bool `json:"useOnto,omitempty"`
34-
OntoOldBase string `json:"ontoOldBase,omitempty"`
29+
CurrentBranchIndex int `json:"currentBranchIndex"`
30+
ConflictBranch string `json:"conflictBranch"`
31+
RemainingBranches []string `json:"remainingBranches"`
32+
OriginalBranch string `json:"originalBranch"`
33+
OriginalRefs map[string]string `json:"originalRefs"`
34+
UseOnto bool `json:"useOnto,omitempty"`
35+
OntoOldBase string `json:"ontoOldBase,omitempty"`
36+
CommitterDateIsAuthorDate bool `json:"committerDateIsAuthorDate,omitempty"`
3537
}
3638

3739
const rebaseStateFile = "gh-stack-rebase-state"
@@ -74,6 +76,8 @@ layer in its commit history, rebasing if necessary.`,
7476
cmd.Flags().BoolVar(&opts.cont, "continue", false, "Continue rebase after resolving conflicts")
7577
cmd.Flags().BoolVar(&opts.abort, "abort", false, "Abort rebase and restore all branches")
7678
cmd.Flags().StringVar(&opts.remote, "remote", "", "Remote to fetch from (defaults to auto-detected remote)")
79+
cmd.Flags().BoolVar(&opts.committerDateIsAuthorDate, "committer-date-is-author-date", false, "Preserve commit dates as the same as author dates")
80+
cmd.Flags().BoolVar(&opts.committerDateIsAuthorDate, "preserve-dates", false, "Alias for --committer-date-is-author-date")
7781

7882
return cmd
7983
}
@@ -187,13 +191,14 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
187191
}
188192

189193
rebaseResult := cascadeRebase(cascadeRebaseOpts{
190-
Cfg: cfg,
191-
Stack: s,
192-
Branches: branchesToRebase,
193-
StartAbsIdx: startIdx,
194-
OriginalRefs: originalRefs,
195-
NeedsOnto: needsOnto,
196-
OntoOldBase: ontoOldBase,
194+
Cfg: cfg,
195+
Stack: s,
196+
Branches: branchesToRebase,
197+
StartAbsIdx: startIdx,
198+
OriginalRefs: originalRefs,
199+
NeedsOnto: needsOnto,
200+
OntoOldBase: ontoOldBase,
201+
CommitterDateIsAuthorDate: opts.committerDateIsAuthorDate,
197202
})
198203

199204
if rebaseResult.Err != nil {
@@ -205,13 +210,14 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
205210
cfg.Warningf("Rebasing %s onto %s — conflict", rebaseResult.ConflictBranch, rebaseResult.ConflictBase)
206211

207212
state := &rebaseState{
208-
CurrentBranchIndex: rebaseResult.ConflictIdx,
209-
ConflictBranch: rebaseResult.ConflictBranch,
210-
RemainingBranches: rebaseResult.Remaining,
211-
OriginalBranch: currentBranch,
212-
OriginalRefs: originalRefs,
213-
UseOnto: rebaseResult.NeedsOnto,
214-
OntoOldBase: rebaseResult.OntoOldBase,
213+
CurrentBranchIndex: rebaseResult.ConflictIdx,
214+
ConflictBranch: rebaseResult.ConflictBranch,
215+
RemainingBranches: rebaseResult.Remaining,
216+
OriginalBranch: currentBranch,
217+
OriginalRefs: originalRefs,
218+
UseOnto: rebaseResult.NeedsOnto,
219+
OntoOldBase: rebaseResult.OntoOldBase,
220+
CommitterDateIsAuthorDate: opts.committerDateIsAuthorDate,
215221
}
216222
if err := saveRebaseState(gitDir, state); err != nil {
217223
cfg.Warningf("failed to save rebase state: %s", err)
@@ -292,7 +298,8 @@ func continueRebase(cfg *config.Config, gitDir string) error {
292298
conflictBranch, s.Branches[len(s.Branches)-1].Branch)
293299

294300
if git.IsRebaseInProgress() {
295-
if err := git.RebaseContinue(); err != nil {
301+
rebaseOpts := git.RebaseOpts{CommitterDateIsAuthorDate: state.CommitterDateIsAuthorDate}
302+
if err := git.RebaseContinue(rebaseOpts); err != nil {
296303
return fmt.Errorf("rebase continue failed — resolve remaining conflicts and try again: %w", err)
297304
}
298305
}
@@ -334,13 +341,14 @@ func continueRebase(cfg *config.Config, gitDir string) error {
334341
}
335342

336343
result := cascadeRebase(cascadeRebaseOpts{
337-
Cfg: cfg,
338-
Stack: s,
339-
Branches: remainingRefs,
340-
StartAbsIdx: startAbsIdx,
341-
OriginalRefs: state.OriginalRefs,
342-
NeedsOnto: state.UseOnto,
343-
OntoOldBase: state.OntoOldBase,
344+
Cfg: cfg,
345+
Stack: s,
346+
Branches: remainingRefs,
347+
StartAbsIdx: startAbsIdx,
348+
OriginalRefs: state.OriginalRefs,
349+
NeedsOnto: state.UseOnto,
350+
OntoOldBase: state.OntoOldBase,
351+
CommitterDateIsAuthorDate: state.CommitterDateIsAuthorDate,
344352
})
345353

346354
if result.Err != nil {

0 commit comments

Comments
 (0)