Skip to content

Commit 0c07f1d

Browse files
zeripathguillep2k
authored andcommitted
Provide Default messages for merges (#9393)
Co-Authored-By: guillep2k <[email protected]>
1 parent b983ff4 commit 0c07f1d

File tree

7 files changed

+276
-15
lines changed

7 files changed

+276
-15
lines changed

custom/conf/app.ini.sample

+10
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
7676
CLOSE_KEYWORDS=close,closes,closed,fix,fixes,fixed,resolve,resolves,resolved
7777
; List of keywords used in Pull Request comments to automatically reopen a related issue
7878
REOPEN_KEYWORDS=reopen,reopens,reopened
79+
; In the default merge message for squash commits include at most this many commits
80+
DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT=50
81+
; In the default merge message for squash commits limit the size of the commit messages to this
82+
DEFAULT_MERGE_MESSAGE_SIZE=5120
83+
; In the default merge message for squash commits walk all commits to include all authors in the Co-authored-by otherwise just use those in the limited list
84+
DEFAULT_MERGE_MESSAGE_ALL_AUTHORS=false
85+
; In default merge messages limit the number of approvers listed as Reviewed-by: to this many
86+
DEFAULT_MERGE_MESSAGE_MAX_APPROVERS=10
87+
; In default merge messages only include approvers who are official
88+
DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY=true
7989

8090
[repository.issue]
8191
; List of reasons why a Pull Request or Issue can be locked

docs/content/doc/advanced/config-cheat-sheet.en-us.md

+5
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
7777
keywords used in Pull Request comments to automatically close a related issue
7878
- `REOPEN_KEYWORDS`: **reopen**, **reopens**, **reopened**: List of keywords used in Pull Request comments to automatically reopen
7979
a related issue
80+
- `DEFAULT_MERGE_MESSAGE_COMMITS_LIMIT`: **50**: In the default merge message for squash commits include at most this many commits. Set to `-1` to include all commits
81+
- `DEFAULT_MERGE_MESSAGE_SIZE`: **5120**: In the default merge message for squash commits limit the size of the commit messages. Set to `-1` to have no limit.
82+
- `DEFAULT_MERGE_MESSAGE_ALL_AUTHORS`: **false**: In the default merge message for squash commits walk all commits to include all authors in the Co-authored-by otherwise just use those in the limited list
83+
- `DEFAULT_MERGE_MESSAGE_MAX_APPROVERS`: **10**: In default merge messages limit the number of approvers listed as `Reviewed-by:`. Set to `-1` to include all.
84+
- `DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY`: **true**: In default merge messages only include approvers who are officially allowed to review.
8085

8186
### Repository - Issue (`repository.issue`)
8287

models/pull.go

+201
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package models
77

88
import (
99
"fmt"
10+
"io"
1011
"strings"
1112

1213
"code.gitea.io/gitea/modules/git"
@@ -177,6 +178,206 @@ func (pr *PullRequest) GetDefaultMergeMessage() string {
177178
return fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.MustHeadUserName(), pr.HeadRepo.Name, pr.BaseBranch)
178179
}
179180

181+
// GetCommitMessages returns the commit messages between head and merge base (if there is one)
182+
func (pr *PullRequest) GetCommitMessages() string {
183+
if err := pr.LoadIssue(); err != nil {
184+
log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
185+
return ""
186+
}
187+
188+
if err := pr.Issue.LoadPoster(); err != nil {
189+
log.Error("Cannot load poster %d for pr id %d, index %d Error: %v", pr.Issue.PosterID, pr.ID, pr.Index, err)
190+
return ""
191+
}
192+
193+
if pr.HeadRepo == nil {
194+
var err error
195+
pr.HeadRepo, err = GetRepositoryByID(pr.HeadRepoID)
196+
if err != nil {
197+
log.Error("GetRepositoryById[%d]: %v", pr.HeadRepoID, err)
198+
return ""
199+
}
200+
}
201+
202+
gitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
203+
if err != nil {
204+
log.Error("Unable to open head repository: Error: %v", err)
205+
return ""
206+
}
207+
defer gitRepo.Close()
208+
209+
headCommit, err := gitRepo.GetBranchCommit(pr.HeadBranch)
210+
if err != nil {
211+
log.Error("Unable to get head commit: %s Error: %v", pr.HeadBranch, err)
212+
return ""
213+
}
214+
215+
mergeBase, err := gitRepo.GetCommit(pr.MergeBase)
216+
if err != nil {
217+
log.Error("Unable to get merge base commit: %s Error: %v", pr.MergeBase, err)
218+
return ""
219+
}
220+
221+
limit := setting.Repository.PullRequest.DefaultMergeMessageCommitsLimit
222+
223+
list, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, 0)
224+
if err != nil {
225+
log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
226+
return ""
227+
}
228+
229+
maxSize := setting.Repository.PullRequest.DefaultMergeMessageSize
230+
231+
posterSig := pr.Issue.Poster.NewGitSig().String()
232+
233+
authorsMap := map[string]bool{}
234+
authors := make([]string, 0, list.Len())
235+
stringBuilder := strings.Builder{}
236+
element := list.Front()
237+
for element != nil {
238+
commit := element.Value.(*git.Commit)
239+
240+
if maxSize < 0 || stringBuilder.Len() < maxSize {
241+
toWrite := []byte(commit.CommitMessage)
242+
if len(toWrite) > maxSize-stringBuilder.Len() && maxSize > -1 {
243+
toWrite = append(toWrite[:maxSize-stringBuilder.Len()], "..."...)
244+
}
245+
if _, err := stringBuilder.Write(toWrite); err != nil {
246+
log.Error("Unable to write commit message Error: %v", err)
247+
return ""
248+
}
249+
250+
if _, err := stringBuilder.WriteRune('\n'); err != nil {
251+
log.Error("Unable to write commit message Error: %v", err)
252+
return ""
253+
}
254+
}
255+
256+
authorString := commit.Author.String()
257+
if !authorsMap[authorString] && authorString != posterSig {
258+
authors = append(authors, authorString)
259+
authorsMap[authorString] = true
260+
}
261+
element = element.Next()
262+
}
263+
264+
// Consider collecting the remaining authors
265+
if limit >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors {
266+
skip := limit
267+
limit = 30
268+
for {
269+
list, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, skip)
270+
if err != nil {
271+
log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
272+
return ""
273+
274+
}
275+
if list.Len() == 0 {
276+
break
277+
}
278+
element := list.Front()
279+
for element != nil {
280+
commit := element.Value.(*git.Commit)
281+
282+
authorString := commit.Author.String()
283+
if !authorsMap[authorString] && authorString != posterSig {
284+
authors = append(authors, authorString)
285+
authorsMap[authorString] = true
286+
}
287+
element = element.Next()
288+
}
289+
290+
}
291+
}
292+
293+
if len(authors) > 0 {
294+
if _, err := stringBuilder.WriteRune('\n'); err != nil {
295+
log.Error("Unable to write to string builder Error: %v", err)
296+
return ""
297+
}
298+
}
299+
300+
for _, author := range authors {
301+
if _, err := stringBuilder.Write([]byte("Co-authored-by: ")); err != nil {
302+
log.Error("Unable to write to string builder Error: %v", err)
303+
return ""
304+
}
305+
if _, err := stringBuilder.Write([]byte(author)); err != nil {
306+
log.Error("Unable to write to string builder Error: %v", err)
307+
return ""
308+
}
309+
if _, err := stringBuilder.WriteRune('\n'); err != nil {
310+
log.Error("Unable to write to string builder Error: %v", err)
311+
return ""
312+
}
313+
}
314+
315+
return stringBuilder.String()
316+
}
317+
318+
// GetApprovers returns the approvers of the pull request
319+
func (pr *PullRequest) GetApprovers() string {
320+
321+
stringBuilder := strings.Builder{}
322+
if err := pr.getReviewedByLines(&stringBuilder); err != nil {
323+
log.Error("Unable to getReviewedByLines: Error: %v", err)
324+
return ""
325+
}
326+
327+
return stringBuilder.String()
328+
}
329+
330+
func (pr *PullRequest) getReviewedByLines(writer io.Writer) error {
331+
maxReviewers := setting.Repository.PullRequest.DefaultMergeMessageMaxApprovers
332+
333+
if maxReviewers == 0 {
334+
return nil
335+
}
336+
337+
sess := x.NewSession()
338+
defer sess.Close()
339+
if err := sess.Begin(); err != nil {
340+
return err
341+
}
342+
343+
// Note: This doesn't page as we only expect a very limited number of reviews
344+
reviews, err := findReviews(sess, FindReviewOptions{
345+
Type: ReviewTypeApprove,
346+
IssueID: pr.IssueID,
347+
OfficialOnly: setting.Repository.PullRequest.DefaultMergeMessageOfficialApproversOnly,
348+
})
349+
if err != nil {
350+
log.Error("Unable to FindReviews for PR ID %d: %v", pr.ID, err)
351+
return err
352+
}
353+
354+
reviewersWritten := 0
355+
356+
for _, review := range reviews {
357+
if maxReviewers > 0 && reviewersWritten > maxReviewers {
358+
break
359+
}
360+
361+
if err := review.loadReviewer(sess); err != nil && !IsErrUserNotExist(err) {
362+
log.Error("Unable to LoadReviewer[%d] for PR ID %d : %v", review.ReviewerID, pr.ID, err)
363+
return err
364+
} else if review.Reviewer == nil {
365+
continue
366+
}
367+
if _, err := writer.Write([]byte("Reviewed-by: ")); err != nil {
368+
return err
369+
}
370+
if _, err := writer.Write([]byte(review.Reviewer.NewGitSig().String())); err != nil {
371+
return err
372+
}
373+
if _, err := writer.Write([]byte{'\n'}); err != nil {
374+
return err
375+
}
376+
reviewersWritten++
377+
}
378+
return sess.Commit()
379+
}
380+
180381
// GetDefaultSquashMessage returns default message used when squash and merging pull request
181382
func (pr *PullRequest) GetDefaultSquashMessage() string {
182383
if err := pr.LoadIssue(); err != nil {

models/review.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,10 @@ func GetReviewByID(id int64) (*Review, error) {
125125

126126
// FindReviewOptions represent possible filters to find reviews
127127
type FindReviewOptions struct {
128-
Type ReviewType
129-
IssueID int64
130-
ReviewerID int64
128+
Type ReviewType
129+
IssueID int64
130+
ReviewerID int64
131+
OfficialOnly bool
131132
}
132133

133134
func (opts *FindReviewOptions) toCond() builder.Cond {
@@ -141,6 +142,9 @@ func (opts *FindReviewOptions) toCond() builder.Cond {
141142
if opts.Type != ReviewTypeUnknown {
142143
cond = cond.And(builder.Eq{"type": opts.Type})
143144
}
145+
if opts.OfficialOnly {
146+
cond = cond.And(builder.Eq{"official": true})
147+
}
144148
return cond
145149
}
146150

modules/git/repo_commit.go

+25-1
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,28 @@ func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (in
315315

316316
// CommitsBetween returns a list that contains commits between [last, before).
317317
func (repo *Repository) CommitsBetween(last *Commit, before *Commit) (*list.List, error) {
318-
stdout, err := NewCommand("rev-list", before.ID.String()+"..."+last.ID.String()).RunInDirBytes(repo.Path)
318+
var stdout []byte
319+
var err error
320+
if before == nil {
321+
stdout, err = NewCommand("rev-list", before.ID.String()).RunInDirBytes(repo.Path)
322+
} else {
323+
stdout, err = NewCommand("rev-list", before.ID.String()+"..."+last.ID.String()).RunInDirBytes(repo.Path)
324+
}
325+
if err != nil {
326+
return nil, err
327+
}
328+
return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
329+
}
330+
331+
// CommitsBetweenLimit returns a list that contains at most limit commits skipping the first skip commits between [last, before)
332+
func (repo *Repository) CommitsBetweenLimit(last *Commit, before *Commit, limit, skip int) (*list.List, error) {
333+
var stdout []byte
334+
var err error
335+
if before == nil {
336+
stdout, err = NewCommand("rev-list", "--max-count", strconv.Itoa(limit), "--skip", strconv.Itoa(skip), last.ID.String()).RunInDirBytes(repo.Path)
337+
} else {
338+
stdout, err = NewCommand("rev-list", "--max-count", strconv.Itoa(limit), "--skip", strconv.Itoa(skip), before.ID.String()+"..."+last.ID.String()).RunInDirBytes(repo.Path)
339+
}
319340
if err != nil {
320341
return nil, err
321342
}
@@ -328,6 +349,9 @@ func (repo *Repository) CommitsBetweenIDs(last, before string) (*list.List, erro
328349
if err != nil {
329350
return nil, err
330351
}
352+
if before == "" {
353+
return repo.CommitsBetween(lastCommit, nil)
354+
}
331355
beforeCommit, err := repo.GetCommit(before)
332356
if err != nil {
333357
return nil, err

modules/setting/repository.go

+23-8
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,14 @@ var (
6060

6161
// Pull request settings
6262
PullRequest struct {
63-
WorkInProgressPrefixes []string
64-
CloseKeywords []string
65-
ReopenKeywords []string
63+
WorkInProgressPrefixes []string
64+
CloseKeywords []string
65+
ReopenKeywords []string
66+
DefaultMergeMessageCommitsLimit int
67+
DefaultMergeMessageSize int
68+
DefaultMergeMessageAllAuthors bool
69+
DefaultMergeMessageMaxApprovers int
70+
DefaultMergeMessageOfficialApproversOnly bool
6671
} `ini:"repository.pull-request"`
6772

6873
// Issue Setting
@@ -127,15 +132,25 @@ var (
127132

128133
// Pull request settings
129134
PullRequest: struct {
130-
WorkInProgressPrefixes []string
131-
CloseKeywords []string
132-
ReopenKeywords []string
135+
WorkInProgressPrefixes []string
136+
CloseKeywords []string
137+
ReopenKeywords []string
138+
DefaultMergeMessageCommitsLimit int
139+
DefaultMergeMessageSize int
140+
DefaultMergeMessageAllAuthors bool
141+
DefaultMergeMessageMaxApprovers int
142+
DefaultMergeMessageOfficialApproversOnly bool
133143
}{
134144
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
135145
// Same as GitHub. See
136146
// https://help.github.com/articles/closing-issues-via-commit-messages
137-
CloseKeywords: strings.Split("close,closes,closed,fix,fixes,fixed,resolve,resolves,resolved", ","),
138-
ReopenKeywords: strings.Split("reopen,reopens,reopened", ","),
147+
CloseKeywords: strings.Split("close,closes,closed,fix,fixes,fixed,resolve,resolves,resolved", ","),
148+
ReopenKeywords: strings.Split("reopen,reopens,reopened", ","),
149+
DefaultMergeMessageCommitsLimit: 50,
150+
DefaultMergeMessageSize: 5 * 1024,
151+
DefaultMergeMessageAllAuthors: false,
152+
DefaultMergeMessageMaxApprovers: 10,
153+
DefaultMergeMessageOfficialApproversOnly: true,
139154
},
140155

141156
// Issue settings

templates/repo/issue/view_content/pull.tmpl

+5-3
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
{{end}}
132132
{{if .AllowMerge}}
133133
{{$prUnit := .Repository.MustGetUnit $.UnitTypePullRequests}}
134+
{{$approvers := .Issue.PullRequest.GetApprovers}}
134135
{{if or $prUnit.PullRequestsConfig.AllowMerge $prUnit.PullRequestsConfig.AllowRebase $prUnit.PullRequestsConfig.AllowRebaseMerge $prUnit.PullRequestsConfig.AllowSquash}}
135136
<div class="ui divider"></div>
136137
{{if $prUnit.PullRequestsConfig.AllowMerge}}
@@ -141,7 +142,7 @@
141142
<input type="text" name="merge_title_field" value="{{.Issue.PullRequest.GetDefaultMergeMessage}}">
142143
</div>
143144
<div class="field">
144-
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}"></textarea>
145+
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}">{{$approvers}}</textarea>
145146
</div>
146147
<button class="ui green button" type="submit" name="do" value="merge">
147148
{{$.i18n.Tr "repo.pulls.merge_pull_request"}}
@@ -173,7 +174,7 @@
173174
<input type="text" name="merge_title_field" value="{{.Issue.PullRequest.GetDefaultMergeMessage}}">
174175
</div>
175176
<div class="field">
176-
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}"></textarea>
177+
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}">{{$approvers}}</textarea>
177178
</div>
178179
<button class="ui green button" type="submit" name="do" value="rebase-merge">
179180
{{$.i18n.Tr "repo.pulls.rebase_merge_commit_pull_request"}}
@@ -185,14 +186,15 @@
185186
</div>
186187
{{end}}
187188
{{if $prUnit.PullRequestsConfig.AllowSquash}}
189+
{{$commitMessages := .Issue.PullRequest.GetCommitMessages}}
188190
<div class="ui form squash-fields" style="display: none">
189191
<form action="{{.Link}}/merge" method="post">
190192
{{.CsrfTokenHtml}}
191193
<div class="field">
192194
<input type="text" name="merge_title_field" value="{{.Issue.PullRequest.GetDefaultSquashMessage}}">
193195
</div>
194196
<div class="field">
195-
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}"></textarea>
197+
<textarea name="merge_message_field" rows="5" placeholder="{{$.i18n.Tr "repo.editor.commit_message_desc"}}">{{$commitMessages}}{{$approvers}}</textarea>
196198
</div>
197199
<button class="ui green button" type="submit" name="do" value="squash">
198200
{{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}}

0 commit comments

Comments
 (0)