From 472bd4f97aacb1fb8a7579b00bc47bb4b11d17d5 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sat, 12 Apr 2025 14:09:34 +0200 Subject: [PATCH] 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. --- services/pull/merge_prepare.go | 58 +++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go index fb09515dbd..9beb28e31b 100644 --- a/services/pull/merge_prepare.go +++ b/services/pull/merge_prepare.go @@ -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 -// if there is a conflict it will return a models.ErrRebaseConflicts 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 if err := git.NewCommand(ctx, "checkout", "-b").AddDynamicArguments(stagingBranch, trackingBranch). Run(ctx.RunOpts()); err != nil {