diff --git a/docs/content/doc/usage/merge-message-templates.en-us.md b/docs/content/doc/usage/merge-message-templates.en-us.md index 751f07e13f..03095a3bbe 100644 --- a/docs/content/doc/usage/merge-message-templates.en-us.md +++ b/docs/content/doc/usage/merge-message-templates.en-us.md @@ -48,3 +48,10 @@ You can use the following variables enclosed in `${}` inside these templates whi - PullRequestIndex: Pull request's index number - PullRequestReference: Pull request's reference char with index number. i.e. #1, !2 - ClosingIssues: return a string contains all issues which will be closed by this pull request i.e. `close #1, close #2` + +## Rebase + +When rebasing without a merge commit, `REBASE_TEMPLATE.md` modifies the message of the last commit. The following additional variables are available in this template: + +- CommitTitle: Commit's title +- CommitBody: Commits's body text diff --git a/services/pull/merge.go b/services/pull/merge.go index 12e01e8ce7..85bb90b853 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -34,8 +34,8 @@ import ( issue_service "code.gitea.io/gitea/services/issue" ) -// GetDefaultMergeMessage returns default message used when merging pull request -func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle) (message, body string, err error) { +// getMergeMessage composes the message used when merging a pull request. +func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle, extraVars map[string]string) (message, body string, err error) { if err := pr.LoadBaseRepo(ctx); err != nil { return "", "", err } @@ -81,6 +81,9 @@ func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr vars["HeadRepoOwnerName"] = pr.HeadRepo.OwnerName vars["HeadRepoName"] = pr.HeadRepo.Name } + for extraKey, extraValue := range extraVars { + vars[extraKey] = extraValue + } refs, err := pr.ResolveCrossReferences(ctx) if err == nil { closeIssueIndexes := make([]string, 0, len(refs)) @@ -133,6 +136,11 @@ func expandDefaultMergeMessage(template string, vars map[string]string) (message return os.Expand(message, mapping), os.Expand(body, mapping) } +// GetDefaultMergeMessage returns default message used when merging pull request +func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle) (message, body string, err error) { + return getMergeMessage(ctx, baseGitRepo, pr, mergeStyle, nil) +} + // Merge merges pull request to base repository. // Caller should check PR is ready to be merged (review and status checks) func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, wasAutoMerged bool) error { diff --git a/services/pull/merge_rebase.go b/services/pull/merge_rebase.go index d3bb86d4aa..a88f805ef0 100644 --- a/services/pull/merge_rebase.go +++ b/services/pull/merge_rebase.go @@ -5,13 +5,99 @@ package pull import ( "fmt" + "strings" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" ) -// doMergeStyleRebase rebaases the tracking branch on the base branch as the current HEAD with or with a merge commit to the original pr branch +// getRebaseAmendMessage composes the message to amend commits in rebase merge of a pull request. +func getRebaseAmendMessage(ctx *mergeContext, baseGitRepo *git.Repository) (message string, err error) { + // Get existing commit message. + commitMessage, _, err := git.NewCommand(ctx, "show", "--format=%B", "-s").RunStdString(&git.RunOpts{Dir: ctx.tmpBasePath}) + if err != nil { + return "", err + } + + commitTitle, commitBody, _ := strings.Cut(commitMessage, "\n") + extraVars := map[string]string{"CommitTitle": strings.TrimSpace(commitTitle), "CommitBody": strings.TrimSpace(commitBody)} + + message, body, err := getMergeMessage(ctx, baseGitRepo, ctx.pr, repo_model.MergeStyleRebase, extraVars) + if err != nil || message == "" { + return "", err + } + + if len(body) > 0 { + message = message + "\n\n" + body + } + return message, err +} + +// Perform rebase merge without merge commit. +func doMergeRebaseFastForward(ctx *mergeContext) error { + baseHeadSHA, err := git.GetFullCommitID(ctx, ctx.tmpBasePath, "HEAD") + if err != nil { + return fmt.Errorf("Failed to get full commit id for HEAD: %w", err) + } + + cmd := git.NewCommand(ctx, "merge", "--ff-only").AddDynamicArguments(stagingBranch) + if err := runMergeCommand(ctx, repo_model.MergeStyleRebase, cmd); err != nil { + log.Error("Unable to merge staging into base: %v", err) + return err + } + + // Check if anything actually changed before we amend the message, fast forward can skip commits. + newMergeHeadSHA, err := git.GetFullCommitID(ctx, ctx.tmpBasePath, "HEAD") + if err != nil { + return fmt.Errorf("Failed to get full commit id for HEAD: %w", err) + } + if baseHeadSHA == newMergeHeadSHA { + return nil + } + + // Original repo to read template from. + baseGitRepo, err := git.OpenRepository(ctx, ctx.pr.BaseRepo.RepoPath()) + if err != nil { + log.Error("Unable to get Git repo for rebase: %v", err) + return err + } + defer baseGitRepo.Close() + + // Amend last commit message based on template, if one exists + newMessage, err := getRebaseAmendMessage(ctx, baseGitRepo) + if err != nil { + log.Error("Unable to get commit message for amend: %v", err) + return err + } + + if newMessage != "" { + if err := git.NewCommand(ctx, "commit", "--amend").AddOptionFormat("--message=%s", newMessage).Run(&git.RunOpts{Dir: ctx.tmpBasePath}); err != nil { + log.Error("Unable to amend commit message: %v", err) + return err + } + } + + return nil +} + +// Perform rebase merge with merge commit. +func doMergeRebaseMergeCommit(ctx *mergeContext, message string) error { + cmd := git.NewCommand(ctx, "merge").AddArguments("--no-ff", "--no-commit").AddDynamicArguments(stagingBranch) + + if err := runMergeCommand(ctx, repo_model.MergeStyleRebaseMerge, cmd); err != nil { + log.Error("Unable to merge staging into base: %v", err) + return err + } + if err := commitAndSignNoAuthor(ctx, message); err != nil { + log.Error("Unable to make final commit: %v", err) + return err + } + + return nil +} + +// doMergeStyleRebase rebases the tracking branch on the base branch as the current HEAD with or with a merge commit to the original pr branch func doMergeStyleRebase(ctx *mergeContext, mergeStyle repo_model.MergeStyle, message string) error { if err := rebaseTrackingOnToBase(ctx, mergeStyle); err != nil { return err @@ -26,25 +112,9 @@ func doMergeStyleRebase(ctx *mergeContext, mergeStyle repo_model.MergeStyle, mes ctx.outbuf.Reset() ctx.errbuf.Reset() - cmd := git.NewCommand(ctx, "merge") if mergeStyle == repo_model.MergeStyleRebase { - cmd.AddArguments("--ff-only") - } else { - cmd.AddArguments("--no-ff", "--no-commit") - } - cmd.AddDynamicArguments(stagingBranch) - - // Prepare merge with commit - if err := runMergeCommand(ctx, mergeStyle, cmd); err != nil { - log.Error("Unable to merge staging into base: %v", err) - return err - } - if mergeStyle == repo_model.MergeStyleRebaseMerge { - if err := commitAndSignNoAuthor(ctx, message); err != nil { - log.Error("Unable to make final commit: %v", err) - return err - } + return doMergeRebaseFastForward(ctx) } - return nil + return doMergeRebaseMergeCommit(ctx, message) }