diff --git a/docs/content/doc/usage/issue-pull-request-templates.en-us.md b/docs/content/doc/usage/issue-pull-request-templates.en-us.md index 218b8a36427b7..65af260c3100d 100644 --- a/docs/content/doc/usage/issue-pull-request-templates.en-us.md +++ b/docs/content/doc/usage/issue-pull-request-templates.en-us.md @@ -43,6 +43,39 @@ Possible file names for PR templates: - `.github/PULL_REQUEST_TEMPLATE.md` - `.github/pull_request_template.md` +Possible file names for PR default merge message templates: + +- `.gitea/default_merge_message/MERGE_TEMPLATE.md` +- `.gitea/default_merge_message/REBASE_TEMPLATE.md` +- `.gitea/default_merge_message/REBASE-MERGE_TEMPLATE.md` +- `.gitea/default_merge_message/SQUASH_TEMPLATE.md` +- `.gitea/default_merge_message/MANUALLY-MERGED_TEMPLATE.md` +- `.gitea/default_merge_message/REBASE-UPDATE-ONLY_TEMPLATE.md` + +Possible file names for PR default merge message templates: + +- `.gitea/default_merge_message/MERGE_TEMPLATE.md` +- `.gitea/default_merge_message/REBASE_TEMPLATE.md` +- `.gitea/default_merge_message/REBASE-MERGE_TEMPLATE.md` +- `.gitea/default_merge_message/SQUASH_TEMPLATE.md` +- `.gitea/default_merge_message/MANUALLY-MERGED_TEMPLATE.md` +- `.gitea/default_merge_message/REBASE-UPDATE-ONLY_TEMPLATE.md` + +You can use the following variables enclosed in `${}` inside these templates which follow [os.Expand](https://pkg.go.dev/os#Expand) syntax: + +- BaseRepoOwnerName: Base repository owner name of this pull request +- BaseRepoName: Base repository name of this pull request +- BaseBranch: Base repository target branch name of this pull request +- HeadRepoOwnerName: Head repository owner name of this pull request +- HeadRepoName: Head repository name of this pull request +- HeadBranch: Head repository branch name of this pull request +- PullRequestTitle: Pull request's title +- PullRequestDescription: Pull request's description +- PullRequestPosterName: Pull request's poster name +- 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` + Additionally, the New Issue page URL can be suffixed with `?title=Issue+Title&body=Issue+Text` and the form will be populated with those strings. Those strings will be used instead of the template if there is one. ## Issue Template Directory diff --git a/integrations/pull_merge_test.go b/integrations/pull_merge_test.go index 476f7ab52f9f8..c50913383c08b 100644 --- a/integrations/pull_merge_test.go +++ b/integrations/pull_merge_test.go @@ -6,6 +6,7 @@ package integrations import ( "bytes" + "context" "fmt" "net/http" "net/http/httptest" @@ -243,11 +244,11 @@ func TestCantMergeConflict(t *testing.T) { gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name)) assert.NoError(t, err) - err = pull.Merge(pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "CONFLICT") + err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "CONFLICT") assert.Error(t, err, "Merge should return an error due to conflict") assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error") - err = pull.Merge(pr, user1, gitRepo, repo_model.MergeStyleRebase, "", "CONFLICT") + err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleRebase, "", "CONFLICT") assert.Error(t, err, "Merge should return an error due to conflict") assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error") gitRepo.Close() @@ -342,7 +343,7 @@ func TestCantMergeUnrelated(t *testing.T) { BaseBranch: "base", }).(*models.PullRequest) - err = pull.Merge(pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED") + err = pull.Merge(context.Background(), pr, user1, gitRepo, repo_model.MergeStyleMerge, "", "UNRELATED") assert.Error(t, err, "Merge should return an error due to unrelated") assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error") gitRepo.Close() diff --git a/models/pull.go b/models/pull.go index 0fa3bdf14f72a..fc5c0d61b330a 100644 --- a/models/pull.go +++ b/models/pull.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -228,34 +227,6 @@ func (pr *PullRequest) LoadProtectedBranchCtx(ctx context.Context) (err error) { return } -// GetDefaultMergeMessage returns default message used when merging pull request -func (pr *PullRequest) GetDefaultMergeMessage(ctx context.Context) (string, error) { - if pr.HeadRepo == nil { - var err error - pr.HeadRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.HeadRepoID) - if err != nil { - return "", fmt.Errorf("GetRepositoryById[%d]: %v", pr.HeadRepoID, err) - } - } - if err := pr.LoadIssueCtx(ctx); err != nil { - return "", fmt.Errorf("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err) - } - if err := pr.LoadBaseRepoCtx(ctx); err != nil { - return "", fmt.Errorf("LoadBaseRepo: %v", err) - } - - issueReference := "#" - if pr.BaseRepo.UnitEnabledCtx(ctx, unit.TypeExternalTracker) { - issueReference = "!" - } - - if pr.BaseRepoID == pr.HeadRepoID { - return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), nil - } - - return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), nil -} - // ReviewCount represents a count of Reviews type ReviewCount struct { IssueID int64 @@ -338,20 +309,6 @@ func (pr *PullRequest) getReviewedByLines(writer io.Writer) error { return committer.Commit() } -// GetDefaultSquashMessage returns default message used when squash and merging pull request -func (pr *PullRequest) GetDefaultSquashMessage(ctx context.Context) (string, error) { - if err := pr.LoadIssueCtx(ctx); err != nil { - return "", fmt.Errorf("LoadIssue: %v", err) - } - if err := pr.LoadBaseRepoCtx(ctx); err != nil { - return "", fmt.Errorf("LoadBaseRepo: %v", err) - } - if pr.BaseRepo.UnitEnabledCtx(ctx, unit.TypeExternalTracker) { - return fmt.Sprintf("%s (!%d)", pr.Issue.Title, pr.Issue.Index), nil - } - return fmt.Sprintf("%s (#%d)", pr.Issue.Title, pr.Issue.Index), nil -} - // GetGitRefName returns git ref for hidden pull request branch func (pr *PullRequest) GetGitRefName() string { return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index) diff --git a/models/pull_test.go b/models/pull_test.go index 92cf9a6524171..6119bca692769 100644 --- a/models/pull_test.go +++ b/models/pull_test.go @@ -8,10 +8,7 @@ import ( "testing" "code.gitea.io/gitea/models/db" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" ) @@ -256,53 +253,3 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) { pr.Issue.Title = "[wip] " + original assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix()) } - -func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) - - msg, err := pr.GetDefaultMergeMessage(db.DefaultContext) - assert.NoError(t, err) - assert.Equal(t, "Merge pull request 'issue3' (#3) from branch2 into master", msg) - - pr.BaseRepoID = 1 - pr.HeadRepoID = 2 - msg, err = pr.GetDefaultMergeMessage(db.DefaultContext) - assert.NoError(t, err) - assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo1:branch2 into master", msg) -} - -func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - externalTracker := repo_model.RepoUnit{ - Type: unit.TypeExternalTracker, - Config: &repo_model.ExternalTrackerConfig{ - ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}", - }, - } - baseRepo := &repo_model.Repository{Name: "testRepo", ID: 1} - baseRepo.Owner = &user_model.User{Name: "testOwner"} - baseRepo.Units = []*repo_model.RepoUnit{&externalTracker} - - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2, BaseRepo: baseRepo}).(*PullRequest) - - msg, err := pr.GetDefaultMergeMessage(db.DefaultContext) - assert.NoError(t, err) - assert.Equal(t, "Merge pull request 'issue3' (!3) from branch2 into master", msg) - - pr.BaseRepoID = 1 - pr.HeadRepoID = 2 - msg, err = pr.GetDefaultMergeMessage(db.DefaultContext) - assert.NoError(t, err) - assert.Equal(t, "Merge pull request 'issue3' (!3) from user2/repo1:branch2 into master", msg) -} - -func TestPullRequest_GetDefaultSquashMessage(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest) - - msg, err := pr.GetDefaultSquashMessage(db.DefaultContext) - assert.NoError(t, err) - assert.Equal(t, "issue3 (#3)", msg) -} diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 37f1c70545060..4c87f017b36a8 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -173,8 +173,6 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { switch colName { case "type": switch unit.Type(db.Cell2Int64(val)) { - case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects: - r.Config = new(UnitConfig) case unit.TypeExternalWiki: r.Config = new(ExternalWikiConfig) case unit.TypeExternalTracker: @@ -183,8 +181,10 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { r.Config = new(PullRequestsConfig) case unit.TypeIssues: r.Config = new(IssuesConfig) + case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects: + fallthrough default: - panic(fmt.Sprintf("unrecognized repo unit type: %v", *val)) + r.Config = new(UnitConfig) } } } diff --git a/modules/git/commit.go b/modules/git/commit.go index 8337e54fef27d..8c194ef502946 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -17,6 +17,7 @@ import ( "strings" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) // Commit represents a git commit. @@ -306,6 +307,35 @@ func (c *Commit) HasFile(filename string) (bool, error) { return true, nil } +// GetFileContent reads a file content as a string or returns false if this was not possible +func (c *Commit) GetFileContent(filename string, limit int) (string, error) { + entry, err := c.GetTreeEntryByPath(filename) + if err != nil { + return "", err + } + + r, err := entry.Blob().DataAsync() + if err != nil { + return "", err + } + defer r.Close() + + if limit > 0 { + bs := make([]byte, limit) + n, err := util.ReadAtMost(r, bs) + if err != nil { + return "", err + } + return string(bs[:n]), nil + } + + bytes, err := io.ReadAll(r) + if err != nil { + return "", err + } + return string(bytes), nil +} + // GetSubModules get all the sub modules of current revision git tree func (c *Commit) GetSubModules() (*ObjectCache, error) { if c.submoduleCache != nil { diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 91bb57f3fd79a..ecf96ea0c2232 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -801,14 +801,26 @@ func MergePullRequest(ctx *context.APIContext) { return } - // set defaults to propagate needed fields - if err := form.SetDefaults(ctx, pr); err != nil { - ctx.ServerError("SetDefaults", fmt.Errorf("SetDefaults: %v", err)) - return + if len(form.Do) == 0 { + form.Do = string(repo_model.MergeStyleMerge) + } + + message := strings.TrimSpace(form.MergeTitleField) + if len(message) == 0 { + message, err = pull_service.GetDefaultMergeMessage(ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetDefaultMergeMessage", err) + return + } + } + + form.MergeMessageField = strings.TrimSpace(form.MergeMessageField) + if len(form.MergeMessageField) > 0 { + message += "\n\n" + form.MergeMessageField } if form.MergeWhenChecksSucceed { - scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), form.MergeTitleField) + scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message) if err != nil { if pull_model.IsErrAlreadyScheduledToAutoMerge(err) { ctx.Error(http.StatusConflict, "ScheduleAutoMerge", err) @@ -823,7 +835,7 @@ func MergePullRequest(ctx *context.APIContext) { } } - if err := pull_service.Merge(pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, form.MergeTitleField); err != nil { + if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message); err != nil { if models.IsErrInvalidMergeStyle(err) { ctx.Error(http.StatusMethodNotAllowed, "Invalid merge style", fmt.Errorf("%s is not allowed an allowed merge style for this repository", repo_model.MergeStyle(form.Do))) } else if models.IsErrMergeConflicts(err) { diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 620b76f46d360..7ddeb05f719cf 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -712,8 +712,6 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull } func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) { - var bytes []byte - if ctx.Repo.Commit == nil { var err error ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) @@ -734,7 +732,7 @@ func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (str return "", false } defer r.Close() - bytes, err = io.ReadAll(r) + bytes, err := io.ReadAll(r) if err != nil { return "", false } @@ -1574,26 +1572,42 @@ func ViewIssue(ctx *context.Context) { } prConfig := prUnit.PullRequestsConfig() + var mergeStyle repo_model.MergeStyle // Check correct values and select default if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok || !prConfig.IsMergeStyleAllowed(ms) { defaultMergeStyle := prConfig.GetDefaultMergeStyle() if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok { - ctx.Data["MergeStyle"] = defaultMergeStyle + mergeStyle = defaultMergeStyle } else if prConfig.AllowMerge { - ctx.Data["MergeStyle"] = repo_model.MergeStyleMerge + mergeStyle = repo_model.MergeStyleMerge } else if prConfig.AllowRebase { - ctx.Data["MergeStyle"] = repo_model.MergeStyleRebase + mergeStyle = repo_model.MergeStyleRebase } else if prConfig.AllowRebaseMerge { - ctx.Data["MergeStyle"] = repo_model.MergeStyleRebaseMerge + mergeStyle = repo_model.MergeStyleRebaseMerge } else if prConfig.AllowSquash { - ctx.Data["MergeStyle"] = repo_model.MergeStyleSquash + mergeStyle = repo_model.MergeStyleSquash } else if prConfig.AllowManualMerge { - ctx.Data["MergeStyle"] = repo_model.MergeStyleManuallyMerged - } else { - ctx.Data["MergeStyle"] = "" + mergeStyle = repo_model.MergeStyleManuallyMerged } } + + ctx.Data["MergeStyle"] = mergeStyle + + defaultMergeMessage, err := pull_service.GetDefaultMergeMessage(ctx.Repo.GitRepo, pull, mergeStyle) + if err != nil { + ctx.ServerError("GetDefaultMergeMessage", err) + return + } + ctx.Data["DefaultMergeMessage"] = defaultMergeMessage + + defaultSquashMergeMessage, err := pull_service.GetDefaultMergeMessage(ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash) + if err != nil { + ctx.ServerError("GetDefaultSquashMergeMessage", err) + return + } + ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage + if err = pull.LoadProtectedBranch(); err != nil { ctx.ServerError("LoadProtectedBranch", err) return diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index a0b356773857a..27b61309a5756 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -950,13 +950,22 @@ func MergePullRequest(ctx *context.Context) { return } - // set defaults to propagate needed fields - if err := form.SetDefaults(ctx, pr); err != nil { - ctx.ServerError("SetDefaults", fmt.Errorf("SetDefaults: %v", err)) - return + message := strings.TrimSpace(form.MergeTitleField) + if len(message) == 0 { + var err error + message, err = pull_service.GetDefaultMergeMessage(ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do)) + if err != nil { + ctx.ServerError("GetDefaultMergeMessage", err) + return + } + } + + form.MergeMessageField = strings.TrimSpace(form.MergeMessageField) + if len(form.MergeMessageField) > 0 { + message += "\n\n" + form.MergeMessageField } - if err := pull_service.Merge(pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, form.MergeTitleField); err != nil { + if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message); err != nil { if models.IsErrInvalidMergeStyle(err) { ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option")) ctx.Redirect(issue.Link()) diff --git a/services/automerge/automerge.go b/services/automerge/automerge.go index 389546ed5784f..e098f2cec0db8 100644 --- a/services/automerge/automerge.go +++ b/services/automerge/automerge.go @@ -234,7 +234,7 @@ func handlePull(pullID int64, sha string) { defer baseGitRepo.Close() } - if err := pull_service.Merge(pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message); err != nil { + if err := pull_service.Merge(ctx, pr, doer, baseGitRepo, scheduledPRM.MergeStyle, "", scheduledPRM.Message); err != nil { log.Error("pull_service.Merge: %v", err) return } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index bacee9a13ca7b..6c61ed00ecb8d 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -6,7 +6,6 @@ package forms import ( - stdContext "context" "net/http" "net/url" "strings" @@ -602,31 +601,6 @@ func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } -// SetDefaults if not provided for mergestyle and commit message -func (f *MergePullRequestForm) SetDefaults(ctx stdContext.Context, pr *models.PullRequest) (err error) { - if f.Do == "" { - f.Do = "merge" - } - - f.MergeTitleField = strings.TrimSpace(f.MergeTitleField) - if len(f.MergeTitleField) == 0 { - switch f.Do { - case "merge", "rebase-merge": - f.MergeTitleField, err = pr.GetDefaultMergeMessage(ctx) - case "squash": - f.MergeTitleField, err = pr.GetDefaultSquashMessage(ctx) - } - } - - f.MergeMessageField = strings.TrimSpace(f.MergeMessageField) - if len(f.MergeMessageField) > 0 { - f.MergeTitleField += "\n\n" + f.MergeMessageField - f.MergeMessageField = "" - } - - return -} - // CodeCommentForm form for adding code comments for PRs type CodeCommentForm struct { Origin string `binding:"Required;In(timeline,diff)"` diff --git a/services/pull/merge.go b/services/pull/merge.go index 8cc4d8888845d..b14abcd78069d 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "regexp" + "strconv" "strings" "time" @@ -33,9 +34,101 @@ import ( issue_service "code.gitea.io/gitea/services/issue" ) +// GetDefaultMergeMessage returns default message used when merging pull request +func GetDefaultMergeMessage(baseGitRepo *git.Repository, pr *models.PullRequest, mergeStyle repo_model.MergeStyle) (string, error) { + if err := pr.LoadHeadRepo(); err != nil { + return "", err + } + if err := pr.LoadBaseRepo(); err != nil { + return "", err + } + if pr.BaseRepo == nil { + return "", repo_model.ErrRepoNotExist{ID: pr.BaseRepoID} + } + + if err := pr.LoadIssue(); err != nil { + return "", err + } + + isExternalTracker := pr.BaseRepo.UnitEnabled(unit.TypeExternalTracker) + issueReference := "#" + if isExternalTracker { + issueReference = "!" + } + + if mergeStyle != "" { + templateFilepath := fmt.Sprintf(".gitea/default_merge_message/%s_TEMPLATE.md", strings.ToUpper(string(mergeStyle))) + commit, err := baseGitRepo.GetBranchCommit(pr.BaseRepo.DefaultBranch) + if err != nil { + return "", err + } + templateContent, err := commit.GetFileContent(templateFilepath, setting.Repository.PullRequest.DefaultMergeMessageSize) + if err != nil { + if !git.IsErrNotExist(err) { + return "", err + } + } else { + vars := map[string]string{ + "BaseRepoOwnerName": pr.BaseRepo.OwnerName, + "BaseRepoName": pr.BaseRepo.Name, + "BaseBranch": pr.BaseBranch, + "HeadRepoOwnerName": "", + "HeadRepoName": "", + "HeadBranch": pr.HeadBranch, + "PullRequestTitle": pr.Issue.Title, + "PullRequestDescription": pr.Issue.Content, + "PullRequestPosterName": pr.Issue.Poster.Name, + "PullRequestIndex": strconv.FormatInt(pr.Index, 10), + "PullRequestReference": fmt.Sprintf("%s%d", issueReference, pr.Index), + } + if pr.HeadRepo != nil { + vars["HeadRepoOwnerName"] = pr.HeadRepo.OwnerName + vars["HeadRepoName"] = pr.HeadRepo.Name + } + refs, err := pr.ResolveCrossReferences(baseGitRepo.Ctx) + if err == nil { + closeIssueIndexes := make([]string, 0, len(refs)) + closeWord := "close" + if len(setting.Repository.PullRequest.CloseKeywords) > 0 { + closeWord = setting.Repository.PullRequest.CloseKeywords[0] + } + for _, ref := range refs { + if ref.RefAction == references.XRefActionCloses { + closeIssueIndexes = append(closeIssueIndexes, fmt.Sprintf("%s %s%d", closeWord, issueReference, ref.Issue.Index)) + } + } + if len(closeIssueIndexes) > 0 { + vars["ClosingIssues"] = strings.Join(closeIssueIndexes, ", ") + } else { + vars["ClosingIssues"] = "" + } + } + + return os.Expand(templateContent, func(s string) string { + return vars[s] + }), nil + } + } + + // Squash merge has a different from other styles. + if mergeStyle == repo_model.MergeStyleSquash { + return fmt.Sprintf("%s (%s%d)", pr.Issue.Title, issueReference, pr.Issue.Index), nil + } + + if pr.BaseRepoID == pr.HeadRepoID { + return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), nil + } + + if pr.HeadRepo == nil { + return fmt.Sprintf("Merge pull request '%s' (%s%d) from :%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), nil + } + + return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), nil +} + // Merge merges pull request to base repository. // Caller should check PR is ready to be merged (review and status checks) -func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) error { +func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) error { if err := pr.LoadHeadRepo(); err != nil { log.Error("LoadHeadRepo: %v", err) return fmt.Errorf("LoadHeadRepo: %v", err) @@ -79,18 +172,18 @@ func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repos pr.Merger = doer pr.MergerID = doer.ID - if _, err := pr.SetMerged(db.DefaultContext); err != nil { + if _, err := pr.SetMerged(ctx); err != nil { log.Error("setMerged [%d]: %v", pr.ID, err) } - if err := pr.LoadIssueCtx(db.DefaultContext); err != nil { + if err := pr.LoadIssueCtx(ctx); err != nil { log.Error("loadIssue [%d]: %v", pr.ID, err) } - if err := pr.Issue.LoadRepo(db.DefaultContext); err != nil { + if err := pr.Issue.LoadRepo(ctx); err != nil { log.Error("loadRepo for issue [%d]: %v", pr.ID, err) } - if err := pr.Issue.Repo.GetOwner(db.DefaultContext); err != nil { + if err := pr.Issue.Repo.GetOwner(ctx); err != nil { log.Error("GetOwner for issue repo [%d]: %v", pr.ID, err) } @@ -100,17 +193,17 @@ func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repos cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true)) // Resolve cross references - refs, err := pr.ResolveCrossReferences(db.DefaultContext) + refs, err := pr.ResolveCrossReferences(ctx) if err != nil { log.Error("ResolveCrossReferences: %v", err) return nil } for _, ref := range refs { - if err = ref.LoadIssueCtx(db.DefaultContext); err != nil { + if err = ref.LoadIssueCtx(ctx); err != nil { return err } - if err = ref.Issue.LoadRepo(db.DefaultContext); err != nil { + if err = ref.Issue.LoadRepo(ctx); err != nil { return err } close := ref.RefAction == references.XRefActionCloses diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go index 81627ebb77bcf..09bae97780929 100644 --- a/services/pull/pull_test.go +++ b/services/pull/pull_test.go @@ -8,6 +8,12 @@ package pull import ( "testing" + "code.gitea.io/gitea/models" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/git" + "github.com/stretchr/testify/assert" ) @@ -29,3 +35,57 @@ func TestPullRequest_CommitMessageTrailersPattern(t *testing.T) { assert.True(t, commitMessageTrailersPattern.MatchString("Additional whitespace is accepted.\n\nSigned-off-by \t : \tBob ")) assert.True(t, commitMessageTrailersPattern.MatchString("Folded value.\n\nFolded-trailer: This is\n a folded\n trailer value\nOther-Trailer: Value")) } + +func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2}).(*models.PullRequest) + + assert.NoError(t, pr.LoadBaseRepo()) + gitRepo, err := git.OpenRepository(git.DefaultContext, pr.BaseRepo.RepoPath()) + assert.NoError(t, err) + defer gitRepo.Close() + + mergeMessage, err := GetDefaultMergeMessage(gitRepo, pr, "") + assert.NoError(t, err) + assert.Equal(t, "Merge pull request 'issue3' (#3) from branch2 into master", mergeMessage) + + pr.BaseRepoID = 1 + pr.HeadRepoID = 2 + mergeMessage, err = GetDefaultMergeMessage(gitRepo, pr, "") + assert.NoError(t, err) + assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo1:branch2 into master", mergeMessage) +} + +func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + externalTracker := repo_model.RepoUnit{ + Type: unit.TypeExternalTracker, + Config: &repo_model.ExternalTrackerConfig{ + ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}", + }, + } + baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) + baseRepo.Units = []*repo_model.RepoUnit{&externalTracker} + + pr := unittest.AssertExistsAndLoadBean(t, &models.PullRequest{ID: 2, BaseRepo: baseRepo}).(*models.PullRequest) + + assert.NoError(t, pr.LoadBaseRepo()) + gitRepo, err := git.OpenRepository(git.DefaultContext, pr.BaseRepo.RepoPath()) + assert.NoError(t, err) + defer gitRepo.Close() + + mergeMessage, err := GetDefaultMergeMessage(gitRepo, pr, "") + assert.NoError(t, err) + + assert.Equal(t, "Merge pull request 'issue3' (!3) from branch2 into master", mergeMessage) + + pr.BaseRepoID = 1 + pr.HeadRepoID = 2 + pr.BaseRepo = nil + pr.HeadRepo = nil + mergeMessage, err = GetDefaultMergeMessage(gitRepo, pr, "") + assert.NoError(t, err) + + assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo2:branch2 into master", mergeMessage) +} diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index 8c43ace32e134..07b2e89d2432c 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -329,7 +329,7 @@ {{.CsrfTokenHtml}}
- +
@@ -375,10 +375,7 @@ {{.CsrfTokenHtml}}
- -
-
- +