Allow custom default merge message with .gitea/default_merge_message/<merge_style>_TEMPLATE.md (#18177)

* Allow custom default merge message with .gitea/MERGE_MESSAGE_<merge_style>_TEMPLATE.md

* Some improvements

* Follow some advices

* Fix bug

* Fix bug

* Fix lint

* Fix close comment

* Fix test

* Fix and docs

* Improve codes

* Update docs and remove unnecessary variables

* return error for GetDefaultMergeMessage

* Fix test

* improve code

* ignore unknow unit type

* return error for GetDefaultMergeMessage

* Update services/pull/merge.go

* Some improvements

* Follow some advices

* Fix bug

* Fix lint

* Improve codes

* Update docs and remove unnecessary variables

* return error for GetDefaultMergeMessage

* improve code

* Handle deleted HeadRepo in GetDefaultMergeMessage

Signed-off-by: Andrew Thornton <art27@cantab.net>

* Fix test

* Fix test

Co-authored-by: zeripath <art27@cantab.net>
This commit is contained in:
Lunny Xiao 2022-05-08 20:32:45 +08:00 committed by GitHub
parent 5ca224a789
commit 4344a64107
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 292 additions and 165 deletions

View file

@ -43,6 +43,39 @@ Possible file names for PR templates:
- `.github/PULL_REQUEST_TEMPLATE.md` - `.github/PULL_REQUEST_TEMPLATE.md`
- `.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. 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 ## Issue Template Directory

View file

@ -6,6 +6,7 @@ package integrations
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "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)) gitRepo, err := git.OpenRepository(git.DefaultContext, repo_model.RepoPath(user1.Name, repo1.Name))
assert.NoError(t, err) 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.Error(t, err, "Merge should return an error due to conflict")
assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error") 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.Error(t, err, "Merge should return an error due to conflict")
assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error") assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error")
gitRepo.Close() gitRepo.Close()
@ -342,7 +343,7 @@ func TestCantMergeUnrelated(t *testing.T) {
BaseBranch: "base", BaseBranch: "base",
}).(*models.PullRequest) }).(*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.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") assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error")
gitRepo.Close() gitRepo.Close()

View file

@ -13,7 +13,6 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -228,34 +227,6 @@ func (pr *PullRequest) LoadProtectedBranchCtx(ctx context.Context) (err error) {
return 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 // ReviewCount represents a count of Reviews
type ReviewCount struct { type ReviewCount struct {
IssueID int64 IssueID int64
@ -338,20 +309,6 @@ func (pr *PullRequest) getReviewedByLines(writer io.Writer) error {
return committer.Commit() 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 // GetGitRefName returns git ref for hidden pull request branch
func (pr *PullRequest) GetGitRefName() string { func (pr *PullRequest) GetGitRefName() string {
return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index) return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index)

View file

@ -8,10 +8,7 @@ import (
"testing" "testing"
"code.gitea.io/gitea/models/db" "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" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -256,53 +253,3 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
pr.Issue.Title = "[wip] " + original pr.Issue.Title = "[wip] " + original
assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix()) 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)
}

View file

@ -173,8 +173,6 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
switch colName { switch colName {
case "type": case "type":
switch unit.Type(db.Cell2Int64(val)) { switch unit.Type(db.Cell2Int64(val)) {
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects:
r.Config = new(UnitConfig)
case unit.TypeExternalWiki: case unit.TypeExternalWiki:
r.Config = new(ExternalWikiConfig) r.Config = new(ExternalWikiConfig)
case unit.TypeExternalTracker: case unit.TypeExternalTracker:
@ -183,8 +181,10 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
r.Config = new(PullRequestsConfig) r.Config = new(PullRequestsConfig)
case unit.TypeIssues: case unit.TypeIssues:
r.Config = new(IssuesConfig) r.Config = new(IssuesConfig)
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects:
fallthrough
default: default:
panic(fmt.Sprintf("unrecognized repo unit type: %v", *val)) r.Config = new(UnitConfig)
} }
} }
} }

View file

@ -17,6 +17,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
) )
// Commit represents a git commit. // Commit represents a git commit.
@ -306,6 +307,35 @@ func (c *Commit) HasFile(filename string) (bool, error) {
return true, nil 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 // GetSubModules get all the sub modules of current revision git tree
func (c *Commit) GetSubModules() (*ObjectCache, error) { func (c *Commit) GetSubModules() (*ObjectCache, error) {
if c.submoduleCache != nil { if c.submoduleCache != nil {

View file

@ -801,14 +801,26 @@ func MergePullRequest(ctx *context.APIContext) {
return return
} }
// set defaults to propagate needed fields if len(form.Do) == 0 {
if err := form.SetDefaults(ctx, pr); err != nil { form.Do = string(repo_model.MergeStyleMerge)
ctx.ServerError("SetDefaults", fmt.Errorf("SetDefaults: %v", err)) }
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 return
} }
}
form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
if len(form.MergeMessageField) > 0 {
message += "\n\n" + form.MergeMessageField
}
if form.MergeWhenChecksSucceed { 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 err != nil {
if pull_model.IsErrAlreadyScheduledToAutoMerge(err) { if pull_model.IsErrAlreadyScheduledToAutoMerge(err) {
ctx.Error(http.StatusConflict, "ScheduleAutoMerge", 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) { 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))) 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) { } else if models.IsErrMergeConflicts(err) {

View file

@ -712,8 +712,6 @@ func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull
} }
func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) { func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) {
var bytes []byte
if ctx.Repo.Commit == nil { if ctx.Repo.Commit == nil {
var err error var err error
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) 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 return "", false
} }
defer r.Close() defer r.Close()
bytes, err = io.ReadAll(r) bytes, err := io.ReadAll(r)
if err != nil { if err != nil {
return "", false return "", false
} }
@ -1574,26 +1572,42 @@ func ViewIssue(ctx *context.Context) {
} }
prConfig := prUnit.PullRequestsConfig() prConfig := prUnit.PullRequestsConfig()
var mergeStyle repo_model.MergeStyle
// Check correct values and select default // Check correct values and select default
if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok || if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok ||
!prConfig.IsMergeStyleAllowed(ms) { !prConfig.IsMergeStyleAllowed(ms) {
defaultMergeStyle := prConfig.GetDefaultMergeStyle() defaultMergeStyle := prConfig.GetDefaultMergeStyle()
if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok { if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
ctx.Data["MergeStyle"] = defaultMergeStyle mergeStyle = defaultMergeStyle
} else if prConfig.AllowMerge { } else if prConfig.AllowMerge {
ctx.Data["MergeStyle"] = repo_model.MergeStyleMerge mergeStyle = repo_model.MergeStyleMerge
} else if prConfig.AllowRebase { } else if prConfig.AllowRebase {
ctx.Data["MergeStyle"] = repo_model.MergeStyleRebase mergeStyle = repo_model.MergeStyleRebase
} else if prConfig.AllowRebaseMerge { } else if prConfig.AllowRebaseMerge {
ctx.Data["MergeStyle"] = repo_model.MergeStyleRebaseMerge mergeStyle = repo_model.MergeStyleRebaseMerge
} else if prConfig.AllowSquash { } else if prConfig.AllowSquash {
ctx.Data["MergeStyle"] = repo_model.MergeStyleSquash mergeStyle = repo_model.MergeStyleSquash
} else if prConfig.AllowManualMerge { } else if prConfig.AllowManualMerge {
ctx.Data["MergeStyle"] = repo_model.MergeStyleManuallyMerged mergeStyle = repo_model.MergeStyleManuallyMerged
} else {
ctx.Data["MergeStyle"] = ""
} }
} }
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 { if err = pull.LoadProtectedBranch(); err != nil {
ctx.ServerError("LoadProtectedBranch", err) ctx.ServerError("LoadProtectedBranch", err)
return return

View file

@ -950,13 +950,22 @@ func MergePullRequest(ctx *context.Context) {
return return
} }
// set defaults to propagate needed fields message := strings.TrimSpace(form.MergeTitleField)
if err := form.SetDefaults(ctx, pr); err != nil { if len(message) == 0 {
ctx.ServerError("SetDefaults", fmt.Errorf("SetDefaults: %v", err)) 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 return
} }
}
if err := pull_service.Merge(pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, form.MergeTitleField); err != nil { form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
if len(form.MergeMessageField) > 0 {
message += "\n\n" + form.MergeMessageField
}
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) { if models.IsErrInvalidMergeStyle(err) {
ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option")) ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option"))
ctx.Redirect(issue.Link()) ctx.Redirect(issue.Link())

View file

@ -234,7 +234,7 @@ func handlePull(pullID int64, sha string) {
defer baseGitRepo.Close() 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) log.Error("pull_service.Merge: %v", err)
return return
} }

View file

@ -6,7 +6,6 @@
package forms package forms
import ( import (
stdContext "context"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -602,31 +601,6 @@ func (f *MergePullRequestForm) Validate(req *http.Request, errs binding.Errors)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale) 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 // CodeCommentForm form for adding code comments for PRs
type CodeCommentForm struct { type CodeCommentForm struct {
Origin string `binding:"Required;In(timeline,diff)"` Origin string `binding:"Required;In(timeline,diff)"`

View file

@ -13,6 +13,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time" "time"
@ -33,9 +34,101 @@ import (
issue_service "code.gitea.io/gitea/services/issue" 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 <deleted>:%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. // Merge merges pull request to base repository.
// Caller should check PR is ready to be merged (review and status checks) // 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 { if err := pr.LoadHeadRepo(); err != nil {
log.Error("LoadHeadRepo: %v", err) log.Error("LoadHeadRepo: %v", err)
return fmt.Errorf("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.Merger = doer
pr.MergerID = doer.ID 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) 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) 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) 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) 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)) cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true))
// Resolve cross references // Resolve cross references
refs, err := pr.ResolveCrossReferences(db.DefaultContext) refs, err := pr.ResolveCrossReferences(ctx)
if err != nil { if err != nil {
log.Error("ResolveCrossReferences: %v", err) log.Error("ResolveCrossReferences: %v", err)
return nil return nil
} }
for _, ref := range refs { for _, ref := range refs {
if err = ref.LoadIssueCtx(db.DefaultContext); err != nil { if err = ref.LoadIssueCtx(ctx); err != nil {
return err return err
} }
if err = ref.Issue.LoadRepo(db.DefaultContext); err != nil { if err = ref.Issue.LoadRepo(ctx); err != nil {
return err return err
} }
close := ref.RefAction == references.XRefActionCloses close := ref.RefAction == references.XRefActionCloses

View file

@ -8,6 +8,12 @@ package pull
import ( import (
"testing" "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" "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 <bob@example.com> ")) assert.True(t, commitMessageTrailersPattern.MatchString("Additional whitespace is accepted.\n\nSigned-off-by \t : \tBob <bob@example.com> "))
assert.True(t, commitMessageTrailersPattern.MatchString("Folded value.\n\nFolded-trailer: This is\n a folded\n trailer value\nOther-Trailer: Value")) 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)
}

View file

@ -329,7 +329,7 @@
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input type="hidden" name="head_commit_id" value="{{.PullHeadCommitID}}"> <input type="hidden" name="head_commit_id" value="{{.PullHeadCommitID}}">
<div class="field"> <div class="field">
<input type="text" name="merge_title_field" value="{{.Issue.PullRequest.GetDefaultMergeMessage $.Context}}"> <input type="text" name="merge_title_field" value="{{.DefaultMergeMessage}}">
</div> </div>
<div class="field"> <div class="field">
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}">Reviewed-on: {{$.Issue.HTMLURL}}&#13;&#10;{{$approvers}}</textarea> <textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}">Reviewed-on: {{$.Issue.HTMLURL}}&#13;&#10;{{$approvers}}</textarea>
@ -375,10 +375,7 @@
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input type="hidden" name="head_commit_id" value="{{.PullHeadCommitID}}"> <input type="hidden" name="head_commit_id" value="{{.PullHeadCommitID}}">
<div class="field"> <div class="field">
<input type="text" name="merge_title_field" value="{{.Issue.PullRequest.GetDefaultMergeMessage $.Context}}"> <input type="text" name="merge_title_field" value="{{.DefaultMergeMessage}}">
</div>
<div class="field">
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}">Reviewed-on: {{$.Issue.HTMLURL}}&#13;&#10;{{$approvers}}</textarea>
</div> </div>
<button class="ui green button" type="submit" name="do" value="rebase-merge"> <button class="ui green button" type="submit" name="do" value="rebase-merge">
{{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}} {{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}}
@ -401,7 +398,7 @@
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input type="hidden" name="head_commit_id" value="{{.PullHeadCommitID}}"> <input type="hidden" name="head_commit_id" value="{{.PullHeadCommitID}}">
<div class="field"> <div class="field">
<input type="text" name="merge_title_field" value="{{.Issue.PullRequest.GetDefaultSquashMessage $.Context}}"> <input type="text" name="merge_title_field" value="{{.DefaultSquashMergeMessage}}">
</div> </div>
<div class="field"> <div class="field">
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}">{{.GetCommitMessages}}Reviewed-on: {{$.Issue.HTMLURL}}&#13;&#10;{{$approvers}}</textarea> <textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}">{{.GetCommitMessages}}Reviewed-on: {{$.Issue.HTMLURL}}&#13;&#10;{{$approvers}}</textarea>