1// Copyright 2019 The Gitea Authors.
2// All rights reserved.
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file.
5
6package pull
7
8import (
9	"bufio"
10	"bytes"
11	"fmt"
12	"os"
13	"path/filepath"
14	"regexp"
15	"strings"
16	"time"
17
18	"code.gitea.io/gitea/models"
19	"code.gitea.io/gitea/models/db"
20	repo_model "code.gitea.io/gitea/models/repo"
21	"code.gitea.io/gitea/models/unit"
22	user_model "code.gitea.io/gitea/models/user"
23	"code.gitea.io/gitea/modules/cache"
24	"code.gitea.io/gitea/modules/git"
25	"code.gitea.io/gitea/modules/log"
26	"code.gitea.io/gitea/modules/notification"
27	"code.gitea.io/gitea/modules/references"
28	"code.gitea.io/gitea/modules/setting"
29	"code.gitea.io/gitea/modules/timeutil"
30	asymkey_service "code.gitea.io/gitea/services/asymkey"
31	issue_service "code.gitea.io/gitea/services/issue"
32)
33
34// Merge merges pull request to base repository.
35// Caller should check PR is ready to be merged (review and status checks)
36// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
37func Merge(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (err error) {
38	if err = pr.LoadHeadRepo(); err != nil {
39		log.Error("LoadHeadRepo: %v", err)
40		return fmt.Errorf("LoadHeadRepo: %v", err)
41	} else if err = pr.LoadBaseRepo(); err != nil {
42		log.Error("LoadBaseRepo: %v", err)
43		return fmt.Errorf("LoadBaseRepo: %v", err)
44	}
45
46	prUnit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests)
47	if err != nil {
48		log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err)
49		return err
50	}
51	prConfig := prUnit.PullRequestsConfig()
52
53	// Check if merge style is correct and allowed
54	if !prConfig.IsMergeStyleAllowed(mergeStyle) {
55		return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle}
56	}
57
58	defer func() {
59		go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false, "", "")
60	}()
61
62	pr.MergedCommitID, err = rawMerge(pr, doer, mergeStyle, expectedHeadCommitID, message)
63	if err != nil {
64		return err
65	}
66
67	pr.MergedUnix = timeutil.TimeStampNow()
68	pr.Merger = doer
69	pr.MergerID = doer.ID
70
71	if _, err := pr.SetMerged(); err != nil {
72		log.Error("setMerged [%d]: %v", pr.ID, err)
73	}
74
75	if err := pr.LoadIssue(); err != nil {
76		log.Error("loadIssue [%d]: %v", pr.ID, err)
77	}
78
79	if err := pr.Issue.LoadRepo(); err != nil {
80		log.Error("loadRepo for issue [%d]: %v", pr.ID, err)
81	}
82	if err := pr.Issue.Repo.GetOwner(db.DefaultContext); err != nil {
83		log.Error("GetOwner for issue repo [%d]: %v", pr.ID, err)
84	}
85
86	notification.NotifyMergePullRequest(pr, doer)
87
88	// Reset cached commit count
89	cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true))
90
91	// Resolve cross references
92	refs, err := pr.ResolveCrossReferences()
93	if err != nil {
94		log.Error("ResolveCrossReferences: %v", err)
95		return nil
96	}
97
98	for _, ref := range refs {
99		if err = ref.LoadIssue(); err != nil {
100			return err
101		}
102		if err = ref.Issue.LoadRepo(); err != nil {
103			return err
104		}
105		close := ref.RefAction == references.XRefActionCloses
106		if close != ref.Issue.IsClosed {
107			if err = issue_service.ChangeStatus(ref.Issue, doer, close); err != nil {
108				// Allow ErrDependenciesLeft
109				if !models.IsErrDependenciesLeft(err) {
110					return err
111				}
112			}
113		}
114	}
115
116	return nil
117}
118
119// rawMerge perform the merge operation without changing any pull information in database
120func rawMerge(pr *models.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) {
121	err := git.LoadGitVersion()
122	if err != nil {
123		log.Error("git.LoadGitVersion: %v", err)
124		return "", fmt.Errorf("Unable to get git version: %v", err)
125	}
126
127	// Clone base repo.
128	tmpBasePath, err := createTemporaryRepo(pr)
129	if err != nil {
130		log.Error("CreateTemporaryPath: %v", err)
131		return "", err
132	}
133	defer func() {
134		if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
135			log.Error("Merge: RemoveTemporaryPath: %s", err)
136		}
137	}()
138
139	baseBranch := "base"
140	trackingBranch := "tracking"
141	stagingBranch := "staging"
142
143	if expectedHeadCommitID != "" {
144		trackingCommitID, err := git.NewCommand("show-ref", "--hash", git.BranchPrefix+trackingBranch).RunInDir(tmpBasePath)
145		if err != nil {
146			log.Error("show-ref[%s] --hash refs/heads/trackingn: %v", tmpBasePath, git.BranchPrefix+trackingBranch, err)
147			return "", fmt.Errorf("getDiffTree: %v", err)
148		}
149		if strings.TrimSpace(trackingCommitID) != expectedHeadCommitID {
150			return "", models.ErrSHADoesNotMatch{
151				GivenSHA:   expectedHeadCommitID,
152				CurrentSHA: trackingCommitID,
153			}
154		}
155	}
156
157	var outbuf, errbuf strings.Builder
158
159	// Enable sparse-checkout
160	sparseCheckoutList, err := getDiffTree(tmpBasePath, baseBranch, trackingBranch)
161	if err != nil {
162		log.Error("getDiffTree(%s, %s, %s): %v", tmpBasePath, baseBranch, trackingBranch, err)
163		return "", fmt.Errorf("getDiffTree: %v", err)
164	}
165
166	infoPath := filepath.Join(tmpBasePath, ".git", "info")
167	if err := os.MkdirAll(infoPath, 0700); err != nil {
168		log.Error("Unable to create .git/info in %s: %v", tmpBasePath, err)
169		return "", fmt.Errorf("Unable to create .git/info in tmpBasePath: %v", err)
170	}
171
172	sparseCheckoutListPath := filepath.Join(infoPath, "sparse-checkout")
173	if err := os.WriteFile(sparseCheckoutListPath, []byte(sparseCheckoutList), 0600); err != nil {
174		log.Error("Unable to write .git/info/sparse-checkout file in %s: %v", tmpBasePath, err)
175		return "", fmt.Errorf("Unable to write .git/info/sparse-checkout file in tmpBasePath: %v", err)
176	}
177
178	var gitConfigCommand func() *git.Command
179	if git.CheckGitVersionAtLeast("1.8.0") == nil {
180		gitConfigCommand = func() *git.Command {
181			return git.NewCommand("config", "--local")
182		}
183	} else {
184		gitConfigCommand = func() *git.Command {
185			return git.NewCommand("config")
186		}
187	}
188
189	// Switch off LFS process (set required, clean and smudge here also)
190	if err := gitConfigCommand().AddArguments("filter.lfs.process", "").RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
191		log.Error("git config [filter.lfs.process -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
192		return "", fmt.Errorf("git config [filter.lfs.process -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
193	}
194	outbuf.Reset()
195	errbuf.Reset()
196
197	if err := gitConfigCommand().AddArguments("filter.lfs.required", "false").RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
198		log.Error("git config [filter.lfs.required -> <false> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
199		return "", fmt.Errorf("git config [filter.lfs.required -> <false> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
200	}
201	outbuf.Reset()
202	errbuf.Reset()
203
204	if err := gitConfigCommand().AddArguments("filter.lfs.clean", "").RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
205		log.Error("git config [filter.lfs.clean -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
206		return "", fmt.Errorf("git config [filter.lfs.clean -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
207	}
208	outbuf.Reset()
209	errbuf.Reset()
210
211	if err := gitConfigCommand().AddArguments("filter.lfs.smudge", "").RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
212		log.Error("git config [filter.lfs.smudge -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
213		return "", fmt.Errorf("git config [filter.lfs.smudge -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
214	}
215	outbuf.Reset()
216	errbuf.Reset()
217
218	if err := gitConfigCommand().AddArguments("core.sparseCheckout", "true").RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
219		log.Error("git config [core.sparseCheckout -> true ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
220		return "", fmt.Errorf("git config [core.sparsecheckout -> true]: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
221	}
222	outbuf.Reset()
223	errbuf.Reset()
224
225	// Read base branch index
226	if err := git.NewCommand("read-tree", "HEAD").RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
227		log.Error("git read-tree HEAD: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
228		return "", fmt.Errorf("Unable to read base branch in to the index: %v\n%s\n%s", err, outbuf.String(), errbuf.String())
229	}
230	outbuf.Reset()
231	errbuf.Reset()
232
233	sig := doer.NewGitSig()
234	committer := sig
235
236	// Determine if we should sign
237	signArg := ""
238	if git.CheckGitVersionAtLeast("1.7.9") == nil {
239		sign, keyID, signer, _ := asymkey_service.SignMerge(pr, doer, tmpBasePath, "HEAD", trackingBranch)
240		if sign {
241			signArg = "-S" + keyID
242			if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
243				committer = signer
244			}
245		} else if git.CheckGitVersionAtLeast("2.0.0") == nil {
246			signArg = "--no-gpg-sign"
247		}
248	}
249
250	commitTimeStr := time.Now().Format(time.RFC3339)
251
252	// Because this may call hooks we should pass in the environment
253	env := append(os.Environ(),
254		"GIT_AUTHOR_NAME="+sig.Name,
255		"GIT_AUTHOR_EMAIL="+sig.Email,
256		"GIT_AUTHOR_DATE="+commitTimeStr,
257		"GIT_COMMITTER_NAME="+committer.Name,
258		"GIT_COMMITTER_EMAIL="+committer.Email,
259		"GIT_COMMITTER_DATE="+commitTimeStr,
260	)
261
262	// Merge commits.
263	switch mergeStyle {
264	case repo_model.MergeStyleMerge:
265		cmd := git.NewCommand("merge", "--no-ff", "--no-commit", trackingBranch)
266		if err := runMergeCommand(pr, mergeStyle, cmd, tmpBasePath); err != nil {
267			log.Error("Unable to merge tracking into base: %v", err)
268			return "", err
269		}
270
271		if err := commitAndSignNoAuthor(pr, message, signArg, tmpBasePath, env); err != nil {
272			log.Error("Unable to make final commit: %v", err)
273			return "", err
274		}
275	case repo_model.MergeStyleRebase:
276		fallthrough
277	case repo_model.MergeStyleRebaseUpdate:
278		fallthrough
279	case repo_model.MergeStyleRebaseMerge:
280		// Checkout head branch
281		if err := git.NewCommand("checkout", "-b", stagingBranch, trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
282			log.Error("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
283			return "", fmt.Errorf("git checkout base prior to merge post staging rebase  [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
284		}
285		outbuf.Reset()
286		errbuf.Reset()
287
288		// Rebase before merging
289		if err := git.NewCommand("rebase", baseBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
290			// Rebase will leave a REBASE_HEAD file in .git if there is a conflict
291			if _, statErr := os.Stat(filepath.Join(tmpBasePath, ".git", "REBASE_HEAD")); statErr == nil {
292				var commitSha string
293				ok := false
294				failingCommitPaths := []string{
295					filepath.Join(tmpBasePath, ".git", "rebase-apply", "original-commit"), // Git < 2.26
296					filepath.Join(tmpBasePath, ".git", "rebase-merge", "stopped-sha"),     // Git >= 2.26
297				}
298				for _, failingCommitPath := range failingCommitPaths {
299					if _, statErr := os.Stat(failingCommitPath); statErr == nil {
300						commitShaBytes, readErr := os.ReadFile(failingCommitPath)
301						if readErr != nil {
302							// Abandon this attempt to handle the error
303							log.Error("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
304							return "", fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
305						}
306						commitSha = strings.TrimSpace(string(commitShaBytes))
307						ok = true
308						break
309					}
310				}
311				if !ok {
312					log.Error("Unable to determine failing commit sha for this rebase message. Cannot cast as models.ErrRebaseConflicts.")
313					log.Error("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
314					return "", fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
315				}
316				log.Debug("RebaseConflict at %s [%s:%s -> %s:%s]: %v\n%s\n%s", commitSha, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
317				return "", models.ErrRebaseConflicts{
318					Style:     mergeStyle,
319					CommitSHA: commitSha,
320					StdOut:    outbuf.String(),
321					StdErr:    errbuf.String(),
322					Err:       err,
323				}
324			}
325			log.Error("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
326			return "", fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
327		}
328		outbuf.Reset()
329		errbuf.Reset()
330
331		// not need merge, just update by rebase. so skip
332		if mergeStyle == repo_model.MergeStyleRebaseUpdate {
333			break
334		}
335
336		// Checkout base branch again
337		if err := git.NewCommand("checkout", baseBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
338			log.Error("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
339			return "", fmt.Errorf("git checkout base prior to merge post staging rebase  [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
340		}
341		outbuf.Reset()
342		errbuf.Reset()
343
344		cmd := git.NewCommand("merge")
345		if mergeStyle == repo_model.MergeStyleRebase {
346			cmd.AddArguments("--ff-only")
347		} else {
348			cmd.AddArguments("--no-ff", "--no-commit")
349		}
350		cmd.AddArguments(stagingBranch)
351
352		// Prepare merge with commit
353		if err := runMergeCommand(pr, mergeStyle, cmd, tmpBasePath); err != nil {
354			log.Error("Unable to merge staging into base: %v", err)
355			return "", err
356		}
357		if mergeStyle == repo_model.MergeStyleRebaseMerge {
358			if err := commitAndSignNoAuthor(pr, message, signArg, tmpBasePath, env); err != nil {
359				log.Error("Unable to make final commit: %v", err)
360				return "", err
361			}
362		}
363	case repo_model.MergeStyleSquash:
364		// Merge with squash
365		cmd := git.NewCommand("merge", "--squash", trackingBranch)
366		if err := runMergeCommand(pr, mergeStyle, cmd, tmpBasePath); err != nil {
367			log.Error("Unable to merge --squash tracking into base: %v", err)
368			return "", err
369		}
370
371		if err = pr.Issue.LoadPoster(); err != nil {
372			log.Error("LoadPoster: %v", err)
373			return "", fmt.Errorf("LoadPoster: %v", err)
374		}
375		sig := pr.Issue.Poster.NewGitSig()
376		if signArg == "" {
377			if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil {
378				log.Error("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
379				return "", fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
380			}
381		} else {
382			if setting.Repository.PullRequest.AddCoCommitterTrailers && committer.String() != sig.String() {
383				// add trailer
384				message += fmt.Sprintf("\nCo-authored-by: %s\nCo-committed-by: %s\n", sig.String(), sig.String())
385			}
386			if err := git.NewCommand("commit", signArg, fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil {
387				log.Error("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
388				return "", fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
389			}
390		}
391		outbuf.Reset()
392		errbuf.Reset()
393	default:
394		return "", models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle}
395	}
396
397	// OK we should cache our current head and origin/headbranch
398	mergeHeadSHA, err := git.GetFullCommitID(tmpBasePath, "HEAD")
399	if err != nil {
400		return "", fmt.Errorf("Failed to get full commit id for HEAD: %v", err)
401	}
402	mergeBaseSHA, err := git.GetFullCommitID(tmpBasePath, "original_"+baseBranch)
403	if err != nil {
404		return "", fmt.Errorf("Failed to get full commit id for origin/%s: %v", pr.BaseBranch, err)
405	}
406	mergeCommitID, err := git.GetFullCommitID(tmpBasePath, baseBranch)
407	if err != nil {
408		return "", fmt.Errorf("Failed to get full commit id for the new merge: %v", err)
409	}
410
411	// Now it's questionable about where this should go - either after or before the push
412	// I think in the interests of data safety - failures to push to the lfs should prevent
413	// the merge as you can always remerge.
414	if setting.LFS.StartServer {
415		if err := LFSPush(tmpBasePath, mergeHeadSHA, mergeBaseSHA, pr); err != nil {
416			return "", err
417		}
418	}
419
420	var headUser *user_model.User
421	err = pr.HeadRepo.GetOwner(db.DefaultContext)
422	if err != nil {
423		if !user_model.IsErrUserNotExist(err) {
424			log.Error("Can't find user: %d for head repository - %v", pr.HeadRepo.OwnerID, err)
425			return "", err
426		}
427		log.Error("Can't find user: %d for head repository - defaulting to doer: %s - %v", pr.HeadRepo.OwnerID, doer.Name, err)
428		headUser = doer
429	} else {
430		headUser = pr.HeadRepo.Owner
431	}
432
433	env = models.FullPushingEnvironment(
434		headUser,
435		doer,
436		pr.BaseRepo,
437		pr.BaseRepo.Name,
438		pr.ID,
439	)
440
441	var pushCmd *git.Command
442	if mergeStyle == repo_model.MergeStyleRebaseUpdate {
443		// force push the rebase result to head branch
444		pushCmd = git.NewCommand("push", "-f", "head_repo", stagingBranch+":"+git.BranchPrefix+pr.HeadBranch)
445	} else {
446		pushCmd = git.NewCommand("push", "origin", baseBranch+":"+git.BranchPrefix+pr.BaseBranch)
447	}
448
449	// Push back to upstream.
450	if err := pushCmd.RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil {
451		if strings.Contains(errbuf.String(), "non-fast-forward") {
452			return "", &git.ErrPushOutOfDate{
453				StdOut: outbuf.String(),
454				StdErr: errbuf.String(),
455				Err:    err,
456			}
457		} else if strings.Contains(errbuf.String(), "! [remote rejected]") {
458			err := &git.ErrPushRejected{
459				StdOut: outbuf.String(),
460				StdErr: errbuf.String(),
461				Err:    err,
462			}
463			err.GenerateMessage()
464			return "", err
465		}
466		return "", fmt.Errorf("git push: %s", errbuf.String())
467	}
468	outbuf.Reset()
469	errbuf.Reset()
470
471	return mergeCommitID, nil
472}
473
474func commitAndSignNoAuthor(pr *models.PullRequest, message, signArg, tmpBasePath string, env []string) error {
475	var outbuf, errbuf strings.Builder
476	if signArg == "" {
477		if err := git.NewCommand("commit", "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil {
478			log.Error("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
479			return fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
480		}
481	} else {
482		if err := git.NewCommand("commit", signArg, "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil {
483			log.Error("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
484			return fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
485		}
486	}
487	return nil
488}
489
490func runMergeCommand(pr *models.PullRequest, mergeStyle repo_model.MergeStyle, cmd *git.Command, tmpBasePath string) error {
491	var outbuf, errbuf strings.Builder
492	if err := cmd.RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil {
493		// Merge will leave a MERGE_HEAD file in the .git folder if there is a conflict
494		if _, statErr := os.Stat(filepath.Join(tmpBasePath, ".git", "MERGE_HEAD")); statErr == nil {
495			// We have a merge conflict error
496			log.Debug("MergeConflict [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
497			return models.ErrMergeConflicts{
498				Style:  mergeStyle,
499				StdOut: outbuf.String(),
500				StdErr: errbuf.String(),
501				Err:    err,
502			}
503		} else if strings.Contains(errbuf.String(), "refusing to merge unrelated histories") {
504			log.Debug("MergeUnrelatedHistories [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
505			return models.ErrMergeUnrelatedHistories{
506				Style:  mergeStyle,
507				StdOut: outbuf.String(),
508				StdErr: errbuf.String(),
509				Err:    err,
510			}
511		}
512		log.Error("git merge [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
513		return fmt.Errorf("git merge [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String())
514	}
515
516	return nil
517}
518
519var escapedSymbols = regexp.MustCompile(`([*[?! \\])`)
520
521func getDiffTree(repoPath, baseBranch, headBranch string) (string, error) {
522	getDiffTreeFromBranch := func(repoPath, baseBranch, headBranch string) (string, error) {
523		var outbuf, errbuf strings.Builder
524		// Compute the diff-tree for sparse-checkout
525		if err := git.NewCommand("diff-tree", "--no-commit-id", "--name-only", "-r", "-z", "--root", baseBranch, headBranch, "--").RunInDirPipeline(repoPath, &outbuf, &errbuf); err != nil {
526			return "", fmt.Errorf("git diff-tree [%s base:%s head:%s]: %s", repoPath, baseBranch, headBranch, errbuf.String())
527		}
528		return outbuf.String(), nil
529	}
530
531	scanNullTerminatedStrings := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
532		if atEOF && len(data) == 0 {
533			return 0, nil, nil
534		}
535		if i := bytes.IndexByte(data, '\x00'); i >= 0 {
536			return i + 1, data[0:i], nil
537		}
538		if atEOF {
539			return len(data), data, nil
540		}
541		return 0, nil, nil
542	}
543
544	list, err := getDiffTreeFromBranch(repoPath, baseBranch, headBranch)
545	if err != nil {
546		return "", err
547	}
548
549	// Prefixing '/' for each entry, otherwise all files with the same name in subdirectories would be matched.
550	out := bytes.Buffer{}
551	scanner := bufio.NewScanner(strings.NewReader(list))
552	scanner.Split(scanNullTerminatedStrings)
553	for scanner.Scan() {
554		filepath := scanner.Text()
555		// escape '*', '?', '[', spaces and '!' prefix
556		filepath = escapedSymbols.ReplaceAllString(filepath, `\$1`)
557		// no necessary to escape the first '#' symbol because the first symbol is '/'
558		fmt.Fprintf(&out, "/%s\n", filepath)
559	}
560
561	return out.String(), nil
562}
563
564// IsSignedIfRequired check if merge will be signed if required
565func IsSignedIfRequired(pr *models.PullRequest, doer *user_model.User) (bool, error) {
566	if err := pr.LoadProtectedBranch(); err != nil {
567		return false, err
568	}
569
570	if pr.ProtectedBranch == nil || !pr.ProtectedBranch.RequireSignedCommits {
571		return true, nil
572	}
573
574	sign, _, _, err := asymkey_service.SignMerge(pr, doer, pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName())
575
576	return sign, err
577}
578
579// IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections
580func IsUserAllowedToMerge(pr *models.PullRequest, p models.Permission, user *user_model.User) (bool, error) {
581	if user == nil {
582		return false, nil
583	}
584
585	err := pr.LoadProtectedBranch()
586	if err != nil {
587		return false, err
588	}
589
590	if (p.CanWrite(unit.TypeCode) && pr.ProtectedBranch == nil) || (pr.ProtectedBranch != nil && models.IsUserMergeWhitelisted(pr.ProtectedBranch, user.ID, p)) {
591		return true, nil
592	}
593
594	return false, nil
595}
596
597// CheckPRReadyToMerge checks whether the PR is ready to be merged (reviews and status checks)
598func CheckPRReadyToMerge(pr *models.PullRequest, skipProtectedFilesCheck bool) (err error) {
599	if err = pr.LoadBaseRepo(); err != nil {
600		return fmt.Errorf("LoadBaseRepo: %v", err)
601	}
602
603	if err = pr.LoadProtectedBranch(); err != nil {
604		return fmt.Errorf("LoadProtectedBranch: %v", err)
605	}
606	if pr.ProtectedBranch == nil {
607		return nil
608	}
609
610	isPass, err := IsPullCommitStatusPass(pr)
611	if err != nil {
612		return err
613	}
614	if !isPass {
615		return models.ErrNotAllowedToMerge{
616			Reason: "Not all required status checks successful",
617		}
618	}
619
620	if !pr.ProtectedBranch.HasEnoughApprovals(pr) {
621		return models.ErrNotAllowedToMerge{
622			Reason: "Does not have enough approvals",
623		}
624	}
625	if pr.ProtectedBranch.MergeBlockedByRejectedReview(pr) {
626		return models.ErrNotAllowedToMerge{
627			Reason: "There are requested changes",
628		}
629	}
630	if pr.ProtectedBranch.MergeBlockedByOfficialReviewRequests(pr) {
631		return models.ErrNotAllowedToMerge{
632			Reason: "There are official review requests",
633		}
634	}
635
636	if pr.ProtectedBranch.MergeBlockedByOutdatedBranch(pr) {
637		return models.ErrNotAllowedToMerge{
638			Reason: "The head branch is behind the base branch",
639		}
640	}
641
642	if skipProtectedFilesCheck {
643		return nil
644	}
645
646	if pr.ProtectedBranch.MergeBlockedByProtectedFiles(pr) {
647		return models.ErrNotAllowedToMerge{
648			Reason: "Changed protected files",
649		}
650	}
651
652	return nil
653}
654
655// MergedManually mark pr as merged manually
656func MergedManually(pr *models.PullRequest, doer *user_model.User, baseGitRepo *git.Repository, commitID string) (err error) {
657	prUnit, err := pr.BaseRepo.GetUnit(unit.TypePullRequests)
658	if err != nil {
659		return
660	}
661	prConfig := prUnit.PullRequestsConfig()
662
663	// Check if merge style is correct and allowed
664	if !prConfig.IsMergeStyleAllowed(repo_model.MergeStyleManuallyMerged) {
665		return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: repo_model.MergeStyleManuallyMerged}
666	}
667
668	if len(commitID) < 40 {
669		return fmt.Errorf("Wrong commit ID")
670	}
671
672	commit, err := baseGitRepo.GetCommit(commitID)
673	if err != nil {
674		if git.IsErrNotExist(err) {
675			return fmt.Errorf("Wrong commit ID")
676		}
677		return
678	}
679
680	ok, err := baseGitRepo.IsCommitInBranch(commitID, pr.BaseBranch)
681	if err != nil {
682		return
683	}
684	if !ok {
685		return fmt.Errorf("Wrong commit ID")
686	}
687
688	pr.MergedCommitID = commitID
689	pr.MergedUnix = timeutil.TimeStamp(commit.Author.When.Unix())
690	pr.Status = models.PullRequestStatusManuallyMerged
691	pr.Merger = doer
692	pr.MergerID = doer.ID
693
694	merged := false
695	if merged, err = pr.SetMerged(); err != nil {
696		return
697	} else if !merged {
698		return fmt.Errorf("SetMerged failed")
699	}
700
701	notification.NotifyMergePullRequest(pr, doer)
702	log.Info("manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s", pr.ID, pr.BaseRepo.Name, pr.BaseBranch, commit.ID.String())
703	return nil
704}
705