Skip to content

Commit 8179ef8

Browse files
committed
Speedup pull-request mergeing
The old way of PR merging has to checkout the working tree of the repository. This change make it possible to do the merge by manipulating the index only.
1 parent ca68a75 commit 8179ef8

File tree

1 file changed

+77
-42
lines changed

1 file changed

+77
-42
lines changed

models/pull.go

Lines changed: 77 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -270,63 +270,98 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error
270270
return fmt.Errorf("OpenRepository: %v", err)
271271
}
272272

273-
// Clone base repo.
274-
tmpBasePath := path.Join(setting.AppDataPath, "tmp/repos", com.ToStr(time.Now().Nanosecond())+".git")
273+
baseRepoPath := pr.BaseRepo.RepoPath()
275274

276-
if err := os.MkdirAll(path.Dir(tmpBasePath), os.ModePerm); err != nil {
277-
return fmt.Errorf("Failed to create dir %s: %v", tmpBasePath, err)
278-
}
275+
pullHead := fmt.Sprintf("refs/pull/%d/head", pr.Index)
279276

280-
defer os.RemoveAll(path.Dir(tmpBasePath))
277+
// A temporary Git index file we are going to build. The merge commit will be based on it.
278+
indexTmpPath := filepath.Join(os.TempDir(), "gitea-"+pr.BaseRepo.Name+"-"+strconv.Itoa(time.Now().Nanosecond()))
279+
defer os.Remove(indexTmpPath)
281280

282-
var stderr string
283-
if _, stderr, err = process.GetManager().ExecTimeout(5*time.Minute,
284-
fmt.Sprintf("PullRequest.Merge (git clone): %s", tmpBasePath),
285-
"git", "clone", baseGitRepo.Path, tmpBasePath); err != nil {
286-
return fmt.Errorf("git clone: %s", stderr)
281+
// A temporary Git working tree for git-merge-index and git-merge-one-file.
282+
workTreeTmpPath, err := ioutil.TempDir("", "gitea-merge-")
283+
if err != nil {
284+
return fmt.Errorf("Cannot create temporary directory for git-merge-index")
287285
}
286+
defer os.RemoveAll(workTreeTmpPath)
288287

289-
// Check out base branch.
290-
if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
291-
fmt.Sprintf("PullRequest.Merge (git checkout): %s", tmpBasePath),
292-
"git", "checkout", pr.BaseBranch); err != nil {
293-
return fmt.Errorf("git checkout: %s", stderr)
294-
}
288+
log.Trace("BaseRepo:%s Index:%s Ancestor:%s BaseBranch:%s HeadBranch:%s PullHead:%s",
289+
baseRepoPath, indexTmpPath, pr.MergeBase, pr.BaseBranch, pr.HeadBranch, pullHead)
290+
291+
var stdout, stderr string
295292

296-
// Add head repo remote.
297-
if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
298-
fmt.Sprintf("PullRequest.Merge (git remote add): %s", tmpBasePath),
299-
"git", "remote", "add", "head_repo", headRepoPath); err != nil {
300-
return fmt.Errorf("git remote add [%s -> %s]: %s", headRepoPath, tmpBasePath, stderr)
293+
// Build the index from the common ancestor, the base-branch head, and the pull head, for 3-way merge.
294+
if _, stderr, err = process.GetManager().ExecDirEnv(-1, "",
295+
fmt.Sprintf("PullRequest.Merge (git read-tree): %s", baseRepoPath),
296+
[]string{"GIT_DIR=" + baseRepoPath, "GIT_INDEX_FILE=" + indexTmpPath},
297+
"git", "read-tree", "-im", pr.MergeBase, pr.BaseBranch, pullHead); err != nil {
298+
return fmt.Errorf("git read-tree -im %s %s %s: %s (%v)", pr.MergeBase, pr.BaseBranch, pullHead, stderr, err)
301299
}
302300

303-
// Merge commits.
304-
if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
305-
fmt.Sprintf("PullRequest.Merge (git fetch): %s", tmpBasePath),
306-
"git", "fetch", "head_repo"); err != nil {
307-
return fmt.Errorf("git fetch [%s -> %s]: %s", headRepoPath, tmpBasePath, stderr)
301+
// Run the merge algorithm throughout the prepared index.
302+
if stdout, stderr, err = process.GetManager().ExecDirEnv(-1, "",
303+
fmt.Sprintf("PullRequest.Merge (git merge-index)"),
304+
[]string{
305+
"GIT_DIR=" + baseRepoPath,
306+
"GIT_INDEX_FILE=" + indexTmpPath,
307+
"GIT_WORK_TREE=" + workTreeTmpPath,
308+
},
309+
"git", "merge-index", "git-merge-one-file", "-a"); err != nil {
310+
return fmt.Errorf("git merge-index git merge-one-file -a: %s (%v)", stderr, err)
308311
}
309312

310-
if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
311-
fmt.Sprintf("PullRequest.Merge (git merge --no-ff --no-commit): %s", tmpBasePath),
312-
"git", "merge", "--no-ff", "--no-commit", "head_repo/"+pr.HeadBranch); err != nil {
313-
return fmt.Errorf("git merge --no-ff --no-commit [%s]: %v - %s", tmpBasePath, err, stderr)
313+
// Write the tree object back to the Git object store from the index.
314+
if stdout, stderr, err = process.GetManager().ExecDirEnv(-1, "",
315+
fmt.Sprintf("PullRequest.Merge (git write-tree): %s", baseRepoPath),
316+
[]string{"GIT_DIR=" + baseRepoPath, "GIT_INDEX_FILE=" + indexTmpPath},
317+
"git", "write-tree"); err != nil {
318+
return fmt.Errorf("git write-tree: %s (%v)", stderr, err)
314319
}
320+
treeID := strings.TrimSpace(stdout)
315321

316-
sig := doer.NewGitSig()
317-
if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
318-
fmt.Sprintf("PullRequest.Merge (git merge): %s", tmpBasePath),
319-
"git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
320-
"-m", fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.HeadUserName, pr.HeadRepo.Name, pr.BaseBranch)); err != nil {
321-
return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, stderr)
322+
log.Trace("BaseRepo:%s TreeId:%s", baseRepoPath, treeID)
323+
324+
// Create the commit object based on the tree object we just built.
325+
var headBranch *Branch
326+
if headBranch, err = pr.HeadRepo.GetBranch(pr.HeadBranch); err != nil {
327+
return fmt.Errorf("Getting head branch: %s: %s (%v)", pr.HeadBranch, stderr, err)
322328
}
323329

324-
// Push back to upstream.
325-
if _, stderr, err = process.GetManager().ExecDir(-1, tmpBasePath,
326-
fmt.Sprintf("PullRequest.Merge (git push): %s", tmpBasePath),
327-
"git", "push", baseGitRepo.Path, pr.BaseBranch); err != nil {
328-
return fmt.Errorf("git push: %s", stderr)
330+
var headCommit *git.Commit
331+
if headCommit, err = headBranch.GetCommit(); err != nil {
332+
return fmt.Errorf("Getting head commit: %s: %s (%v)", pr.HeadBranch, stderr, err)
329333
}
334+
commitMessage := fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.HeadUserName, pr.HeadRepo.Name, pr.BaseBranch)
335+
336+
sig := doer.NewGitSig()
337+
if stdout, stderr, err = process.GetManager().ExecDirEnv(-1, "",
338+
fmt.Sprintf("PullRequest.Merge (git commit-tree): %s", baseRepoPath),
339+
[]string{
340+
"GIT_DIR=" + baseRepoPath,
341+
"GIT_AUTHOR_NAME=" + headCommit.Author.Name,
342+
"GIT_AUTHOR_EMAIL=" + headCommit.Author.Email,
343+
"GIT_AUTHOR_DATE=" + headCommit.Author.When.Format("Mon, 02 Jan 2006 15:04:05 -0700"),
344+
"GIT_COMMITTER_NAME=" + sig.Name,
345+
"GIT_COMMITTER_EMAIL=" + sig.Email,
346+
"GIT_COMMITTER_DATE=" + sig.When.Format("Mon, 02 Jan 2006 15:04:05 -0700"),
347+
},
348+
"git", "commit-tree", treeID, "-p", pr.BaseBranch, "-p", pullHead, "-m", commitMessage); err != nil {
349+
return fmt.Errorf("git commit-tree %s -p %s -p %s: %s (%v)", strings.TrimSpace(treeID), pr.BaseBranch, pullHead, stderr, err)
350+
}
351+
commitID := strings.TrimSpace(stdout)
352+
353+
log.Trace("BaseRepo:%s CommitId:%s", baseRepoPath, commitID)
354+
355+
// Update the ref of the base-branch to point to the new commit object.
356+
refPath := fmt.Sprintf("refs/heads/%s", pr.BaseBranch)
357+
if _, stderr, err = process.GetManager().ExecDirEnv(-1, "",
358+
fmt.Sprintf("PullRequest.Merge (git update-ref): %s", baseRepoPath),
359+
[]string{"GIT_DIR=" + baseRepoPath},
360+
"git", "update-ref", refPath, commitID); err != nil {
361+
return fmt.Errorf("git update-ref -m %s %s: %s (%v)", commitMessage, refPath, stderr, err)
362+
}
363+
364+
log.Trace("BaseRepo:%s Update-ref: %s->%s. Done.", baseRepoPath, refPath, commitID)
330365

331366
pr.MergedCommitID, err = baseGitRepo.GetBranchCommitID(pr.BaseBranch)
332367
if err != nil {

0 commit comments

Comments
 (0)