diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 036fa2d..70ef74e 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -16,6 +16,7 @@ import org.scalatra.forms._ import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.PersonIdent +import org.eclipse.jgit.revwalk.RevWalk import scala.collection.JavaConverters._ @@ -50,7 +51,8 @@ )(PullRequestForm.apply) val mergeForm = mapping( - "message" -> trim(label("Message", text(required))) + "message" -> trim(label("Message", text(required))), + "strategy" -> trim(label("Strategy", text(required))) )(MergeForm.apply) case class PullRequestForm( @@ -69,7 +71,7 @@ labelNames: Option[String] ) - case class MergeForm(message: String) + case class MergeForm(message: String, strategy: String) get("/:owner/:repository/pulls")(referrersOnly { repository => val q = request.getParameter("q") @@ -258,14 +260,30 @@ // record activity recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message) - // merge git repository - mergePullRequest(git, pullreq.branch, issueId, - s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + form.message, - new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)) - val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom, pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo) + val revCommits = using(new RevWalk( git.getRepository )){ revWalk => + commits.flatten.map { commit => + revWalk.parseCommit(git.getRepository.resolve(commit.id)) + } + }.reverse + + // merge git repository + form.strategy match { + case "merge-commit" => + mergePullRequest(git, pullreq.branch, issueId, + s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + form.message, + new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)) + case "rebase" => + rebasePullRequest(git, pullreq.branch, issueId, revCommits, + new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)) + case "squash" => + squashPullRequest(git, pullreq.branch, issueId, + s"${issue.title} (#${issueId})\n\n" + form.message, + new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)) + } + // close issue by content of pull request val defaultBranch = getRepository(owner, name).get.repository.defaultBranch if(pullreq.branch == defaultBranch){ diff --git a/src/main/scala/gitbucket/core/service/MergeService.scala b/src/main/scala/gitbucket/core/service/MergeService.scala index c67f255..2f609bd 100644 --- a/src/main/scala/gitbucket/core/service/MergeService.scala +++ b/src/main/scala/gitbucket/core/service/MergeService.scala @@ -3,25 +3,26 @@ import gitbucket.core.model.Account import gitbucket.core.util.Directory._ import gitbucket.core.util.SyntaxSugars._ - import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.api.Git import org.eclipse.jgit.transport.RefSpec import org.eclipse.jgit.errors.NoMergeBaseException -import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent, Repository} -import org.eclipse.jgit.revwalk.RevWalk +import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository} +import org.eclipse.jgit.revwalk.{RevCommit, RevWalk} trait MergeService { import MergeService._ + /** * Checks whether conflict will be caused in merging within pull request. * Returns true if conflict will be caused. */ def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Boolean = { using(Git.open(getRepositoryDir(userName, repositoryName))) { git => - MergeCacheInfo(git, branch, issueId).checkConflict() + new MergeCacheInfo(git, branch, issueId).checkConflict() } } + /** * Checks whether conflict will be caused in merging within pull request. * only cache check. @@ -30,13 +31,25 @@ */ def checkConflictCache(userName: String, repositoryName: String, branch: String, issueId: Int): Option[Boolean] = { using(Git.open(getRepositoryDir(userName, repositoryName))) { git => - MergeCacheInfo(git, branch, issueId).checkConflictCache() + new MergeCacheInfo(git, branch, issueId).checkConflictCache() } } - /** merge pull request */ - def mergePullRequest(git:Git, branch: String, issueId: Int, message:String, committer:PersonIdent): Unit = { - MergeCacheInfo(git, branch, issueId).merge(message, committer) + + /** merge the pull request with a merge commit */ + def mergePullRequest(git: Git, branch: String, issueId: Int, message: String, committer: PersonIdent): Unit = { + new MergeCacheInfo(git, branch, issueId).merge(message, committer) } + + /** rebase to the head of the pull request branch */ + def rebasePullRequest(git: Git, branch: String, issueId: Int, commits: Seq[RevCommit], committer: PersonIdent): Unit = { + new MergeCacheInfo(git, branch, issueId).rebase(committer, commits) + } + + /** squash commits in the pull request and append it */ + def squashPullRequest(git: Git, branch: String, issueId: Int, message: String, committer: PersonIdent): Unit = { + new MergeCacheInfo(git, branch, issueId).squash(message, committer) + } + /** fetch remote branch to my repository refs/pull/{issueId}/head */ def fetchAsPullRequest(userName: String, repositoryName: String, requestUserName: String, requestRepositoryName: String, requestBranch:String, issueId:Int){ using(Git.open(getRepositoryDir(userName, repositoryName))){ git => @@ -46,6 +59,7 @@ .call } } + /** * Checks whether conflict will be caused in merging. Returns true if conflict will be caused. */ @@ -81,6 +95,7 @@ } } } + /** * Checks whether conflict will be caused in merging. Returns true if conflict will be caused. */ @@ -91,20 +106,23 @@ def pullRemote(localUserName: String, localRepositoryName: String, localBranch: String, remoteUserName: String, remoteRepositoryName: String, remoteBranch: String, loginAccount: Account, message: String): Option[ObjectId] = { - tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map{ case (newTreeId, oldBaseId, oldHeadId) => - using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git => - val committer = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) - val newCommit = Util.createMergeCommit(git.getRepository, newTreeId, committer, message, Seq(oldBaseId, oldHeadId)) - Util.updateRefs(git.getRepository, s"refs/heads/${localBranch}", newCommit, false, committer, Some("merge")) + tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch) + .map { case (newTreeId, oldBaseId, oldHeadId) => + using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git => + val committer = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) + val newCommit = Util.createMergeCommit(git.getRepository, newTreeId, committer, message, Seq(oldBaseId, oldHeadId)) + Util.updateRefs(git.getRepository, s"refs/heads/${localBranch}", newCommit, false, committer, Some("merge")) + } + oldBaseId } - oldBaseId - } } } + object MergeService{ + object Util{ - // return treeId + // return merge commit id def createMergeCommit(repository: Repository, treeId: ObjectId, committer: PersonIdent, message: String, parents: Seq[ObjectId]): ObjectId = { val mergeCommit = new CommitBuilder() mergeCommit.setTreeId(treeId) @@ -113,14 +131,14 @@ mergeCommit.setCommitter(committer) mergeCommit.setMessage(message) // insertObject and got mergeCommit Object Id - val inserter = repository.newObjectInserter - val mergeCommitId = inserter.insert(mergeCommit) - inserter.flush() - inserter.close() - mergeCommitId + using(repository.newObjectInserter){ inserter => + val mergeCommitId = inserter.insert(mergeCommit) + inserter.flush() + mergeCommitId + } } - def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None):Unit = { - // update refs + + def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None): Unit = { val refUpdate = repository.updateRef(ref) refUpdate.setNewObjectId(newObjectId) refUpdate.setForceUpdate(force) @@ -129,21 +147,25 @@ refUpdate.update() } } - case class MergeCacheInfo(git:Git, branch:String, issueId:Int){ - val repository = git.getRepository - val mergedBranchName = s"refs/pull/${issueId}/merge" - val conflictedBranchName = s"refs/pull/${issueId}/conflict" + + class MergeCacheInfo(git: Git, branch: String, issueId: Int){ + + private val repository = git.getRepository + private val mergedBranchName = s"refs/pull/${issueId}/merge" + private val conflictedBranchName = s"refs/pull/${issueId}/conflict" + lazy val mergeBaseTip = repository.resolve(s"refs/heads/${branch}") lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head") + def checkConflictCache(): Option[Boolean] = { - Option(repository.resolve(mergedBranchName)).flatMap{ merged => - if(parseCommit(merged).getParents().toSet == Set( mergeBaseTip, mergeTip )){ - // merged branch exists - Some(false) - } else { - None - } - }.orElse(Option(repository.resolve(conflictedBranchName)).flatMap{ conflicted => + Option(repository.resolve(mergedBranchName)).flatMap { merged => + if(parseCommit(merged).getParents().toSet == Set( mergeBaseTip, mergeTip )){ + // merged branch exists + Some(false) + } else { + None + } + }.orElse(Option(repository.resolve(conflictedBranchName)).flatMap { conflicted => if(parseCommit(conflicted).getParents().toSet == Set( mergeBaseTip, mergeTip )){ // conflict branch exists Some(true) @@ -152,10 +174,12 @@ } }) } - def checkConflict():Boolean ={ + + def checkConflict(): Boolean = { checkConflictCache.getOrElse(checkConflictForce) } - def checkConflictForce():Boolean ={ + + def checkConflictForce(): Boolean = { val merger = MergeStrategy.RECURSIVE.newMerger(repository, true) val conflicted = try { !merger.merge(mergeBaseTip, mergeTip) @@ -164,35 +188,101 @@ } val mergeTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeTip )) val committer = mergeTipCommit.getCommitterIdent - def updateBranch(treeId:ObjectId, message:String, branchName:String){ + + def _updateBranch(treeId: ObjectId, message: String, branchName: String){ // creates merge commit val mergeCommitId = createMergeCommit(treeId, committer, message) Util.updateRefs(repository, branchName, mergeCommitId, true, committer) } + if(!conflicted){ - updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName) + _updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName) git.branchDelete().setForce(true).setBranchNames(conflictedBranchName).call() } else { - updateBranch(mergeTipCommit.getTree().getId(), s"can't merge ${mergeTip.name} into ${mergeBaseTip.name}", conflictedBranchName) + _updateBranch(mergeTipCommit.getTree().getId(), s"can't merge ${mergeTip.name} into ${mergeBaseTip.name}", conflictedBranchName) git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call() } conflicted } - // update branch from cache - def merge(message:String, committer:PersonIdent) = { + + def merge(message: String, committer: PersonIdent): Unit = { if(checkConflict()){ throw new RuntimeException("This pull request can't merge automatically.") } - val mergeResultCommit = parseCommit( Option(repository.resolve(mergedBranchName)).getOrElse(throw new RuntimeException(s"not found branch ${mergedBranchName}")) ) + val mergeResultCommit = parseCommit(Option(repository.resolve(mergedBranchName)).getOrElse { + throw new RuntimeException(s"not found branch ${mergedBranchName}") + }) // creates merge commit val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message) // update refs Util.updateRefs(repository, s"refs/heads/${branch}", mergeCommitId, false, committer, Some("merged")) } + + def rebase(committer: PersonIdent, commits: Seq[RevCommit]): Unit = { + if(checkConflict()){ + throw new RuntimeException("This pull request can't merge automatically.") + } + + def _cloneCommit(commit: RevCommit, parents: Array[ObjectId]): CommitBuilder = { + val newCommit = new CommitBuilder() + newCommit.setTreeId(commit.getTree.getId) + parents.foreach { parentId => + newCommit.addParentId(parentId) + } + newCommit.setAuthor(commit.getAuthorIdent) + newCommit.setCommitter(committer) + newCommit.setMessage(commit.getFullMessage) + newCommit + } + + val mergeBaseTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeBaseTip )) + var previousId = mergeBaseTipCommit.getId + + using(repository.newObjectInserter){ inserter => + commits.foreach { commit => + val nextCommit = _cloneCommit(commit, Array(previousId)) + previousId = inserter.insert(nextCommit) + } + inserter.flush() + } + + Util.updateRefs(repository, s"refs/heads/${branch}", previousId, false, committer, Some("rebased")) + } + + def squash(message: String, committer: PersonIdent): Unit = { + if(checkConflict()){ + throw new RuntimeException("This pull request can't merge automatically.") + } + + val mergeBaseTipCommit = using(new RevWalk( repository ))(_.parseCommit(mergeBaseTip)) + val mergeBranchHeadCommit = using(new RevWalk( repository ))(_.parseCommit(repository.resolve(mergedBranchName))) + + // Create squash commit + val mergeCommit = new CommitBuilder() + mergeCommit.setTreeId(mergeBranchHeadCommit.getTree.getId) + mergeCommit.setParentId(mergeBaseTipCommit) + mergeCommit.setAuthor(mergeBranchHeadCommit.getAuthorIdent) + mergeCommit.setCommitter(committer) + mergeCommit.setMessage(message) + + // insertObject and got squash commit Object Id + val newCommitId = using(repository.newObjectInserter){ inserter => + val newCommitId = inserter.insert(mergeCommit) + inserter.flush() + newCommitId + } + + Util.updateRefs(repository, mergedBranchName, newCommitId, true, committer) + + // rebase to squash commit + Util.updateRefs(repository, s"refs/heads/${branch}", repository.resolve(mergedBranchName), false, committer, Some("squashed")) + } + // return treeId private def createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) = Util.createMergeCommit(repository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip)) - private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id)) + private def parseCommit(id: ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id)) + } } diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala index fd70e67..e34627e 100644 --- a/src/main/scala/gitbucket/core/util/JGitUtil.scala +++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala @@ -981,7 +981,7 @@ val blame = blamer.call() var blameMap = Map[String, JGitUtil.BlameInfo]() var idLine = List[(String, Int)]() - val commits = 0.to(blame.getResultContents().size() - 1).map{ i => + val commits = 0.to(blame.getResultContents().size() - 1).map { i => val c = blame.getSourceCommit(i) if(!blameMap.contains(c.name)){ blameMap += c.name -> JGitUtil.BlameInfo( diff --git a/src/main/twirl/gitbucket/core/pulls/mergeguide.scala.html b/src/main/twirl/gitbucket/core/pulls/mergeguide.scala.html index 1eb4c22..0dcf106 100644 --- a/src/main/twirl/gitbucket/core/pulls/mergeguide.scala.html +++ b/src/main/twirl/gitbucket/core/pulls/mergeguide.scala.html @@ -139,8 +139,34 @@