feat: use git-replay for rebasing (#7527)

Closes #7525

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.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7527
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Remo Senekowitsch <remo@buenzli.dev>
Co-committed-by: Remo Senekowitsch <remo@buenzli.dev>
This commit is contained in:
Remo Senekowitsch 2025-04-29 20:51:56 +00:00 committed by David Rotermund
parent 665b19c67f
commit 19736a85e9
4 changed files with 94 additions and 18 deletions

View file

@ -236,10 +236,72 @@ 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
// if there is a conflict it will return a models.ErrRebaseConflicts
func rebaseTrackingOnToBase(ctx *mergeContext, mergeStyle repo_model.MergeStyle) error {
// Checkout head branch
if err := git.NewCommand(ctx, "checkout", "-b").AddDynamicArguments(stagingBranch, trackingBranch).
// Create staging branch
if err := git.NewCommand(ctx, "branch").AddDynamicArguments(stagingBranch, trackingBranch).
Run(ctx.RunOpts()); err != nil {
return fmt.Errorf("unable to git checkout tracking as staging in temp repo for %v: %w\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
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()
// 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 {
// 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 {
// git-replay doesn't tell us which commit first created a merge conflict.
// In order to preserve the quality of our error messages, fall back to
// regular git-rebase.
goto regular_rebase
}
// 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, or git-replay
// failed and we want to determine the first commit that produced a
// merge-conflict. Fall back to regular rebase.
regular_rebase:
// Checkout head 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()