feat: use git-replay for rebasing

This is better for performance, because it can do more work in-memory.
It also preserves unknown headers, which can be important for some
clients. For example, Jujutsu uses a non-standard "change-id" header to
track commits across rebase and amend, but regular git-rebase drops such
unknown headers.
This commit is contained in:
Remo Senekowitsch 2025-04-12 14:09:34 +02:00
parent eb3feaad45
commit 472bd4f97a
No known key found for this signature in database

View file

@ -234,8 +234,64 @@ func getDiffTree(ctx context.Context, repoPath, baseBranch, headBranch string, o
} }
// rebaseTrackingOnToBase checks out the tracking branch as staging and rebases it on to the base branch // rebaseTrackingOnToBase checks out the tracking branch as staging and rebases it on to the base branch
// if there is a conflict it will return a models.ErrRebaseConflicts
func rebaseTrackingOnToBase(ctx *mergeContext, mergeStyle repo_model.MergeStyle) error { func rebaseTrackingOnToBase(ctx *mergeContext, mergeStyle repo_model.MergeStyle) error {
// Check git version for availability of git-replay. If it is available, we use
// it for performance and to preserve unknown commit headers like the
// "change-id" header used by Jujutsu and GitButler to track changes across
// rebase, amend etc.
if err := git.CheckGitVersionAtLeast("2.44"); err == nil {
// Create staging branch
if err := git.NewCommand(ctx, "branch").AddDynamicArguments(stagingBranch, trackingBranch).
Run(ctx.RunOpts()); err != nil {
return fmt.Errorf(
"unable to git branch tracking as staging in temp repo for %v: %w\n%s\n%s",
ctx.pr, err,
ctx.outbuf.String(),
ctx.errbuf.String(),
)
}
ctx.outbuf.Reset()
ctx.errbuf.Reset()
// Use git-replay for performance and to preserve unknown headers,
// like the "change-id" header used by Jujutsu and GitButler.
if err := git.NewCommand(ctx, "replay", "--onto").AddDynamicArguments(baseBranch).
AddDynamicArguments(fmt.Sprintf("%s..%s", baseBranch, stagingBranch)).
Run(ctx.RunOpts()); err != nil {
return fmt.Errorf("Failed to replay commits on base branch")
}
// git-replay worked, stdout contains the instructions for update-ref
updateRefInstructions := ctx.outbuf.String()
opts := ctx.RunOpts()
opts.Stdin = strings.NewReader(updateRefInstructions)
if err := git.NewCommand(ctx, "update-ref", "--stdin").Run(opts); err != nil {
return fmt.Errorf(
"Failed to update ref for %v: %w\n%s\n%s",
ctx.pr,
err,
ctx.outbuf.String(),
ctx.errbuf.String(),
)
}
// Checkout staging branch
if err := git.NewCommand(ctx, "checkout").AddDynamicArguments(stagingBranch).
Run(ctx.RunOpts()); err != nil {
return fmt.Errorf(
"unable to git checkout staging in temp repo for %v: %w\n%s\n%s",
ctx.pr,
err,
ctx.outbuf.String(),
ctx.errbuf.String(),
)
}
ctx.outbuf.Reset()
ctx.errbuf.Reset()
return nil
}
// The available git version is too old to support git-replay.
// Fall back to regular rebase.
// Checkout head branch // Checkout head branch
if err := git.NewCommand(ctx, "checkout", "-b").AddDynamicArguments(stagingBranch, trackingBranch). if err := git.NewCommand(ctx, "checkout", "-b").AddDynamicArguments(stagingBranch, trackingBranch).
Run(ctx.RunOpts()); err != nil { Run(ctx.RunOpts()); err != nil {