Show conflicting files

This commit is contained in:
Naoki Takezoe
2017-12-12 03:20:50 +09:00
parent 847f96d537
commit 1033122fec
4 changed files with 51 additions and 32 deletions

View File

@@ -115,13 +115,13 @@ trait PullRequestsControllerBase extends ControllerBase {
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) => getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
val hasConflict = LockUtil.lock(s"${owner}/${name}"){ val conflictMessage = LockUtil.lock(s"${owner}/${name}"){
checkConflict(owner, name, pullreq.branch, issueId) checkConflict(owner, name, pullreq.branch, issueId)
} }
val hasMergePermission = hasDeveloperRole(owner, name, context.loginAccount) val hasMergePermission = hasDeveloperRole(owner, name, context.loginAccount)
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch) val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
val mergeStatus = PullRequestService.MergeStatus( val mergeStatus = PullRequestService.MergeStatus(
hasConflict = hasConflict, conflictMessage = conflictMessage,
commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo), commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo),
branchProtection = branchProtection, branchProtection = branchProtection,
branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom), branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom),
@@ -437,7 +437,7 @@ trait PullRequestsControllerBase extends ControllerBase {
checkConflict(originRepository.owner, originRepository.name, originBranch, checkConflict(originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch) forkedRepository.owner, forkedRepository.name, forkedBranch)
} }
html.mergecheck(conflict) html.mergecheck(conflict.isDefined)
} }
}) getOrElse NotFound() }) getOrElse NotFound()
}) })

View File

@@ -3,21 +3,22 @@ package gitbucket.core.service
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import org.eclipse.jgit.merge.{MergeStrategy, Merger, RecursiveMerger}
import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.api.{Git, MergeResult}
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.transport.RefSpec import org.eclipse.jgit.transport.RefSpec
import org.eclipse.jgit.errors.NoMergeBaseException import org.eclipse.jgit.errors.NoMergeBaseException
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent, Repository} import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository}
import org.eclipse.jgit.revwalk.RevWalk import org.eclipse.jgit.revwalk.RevWalk
import scala.collection.JavaConverters._
trait MergeService { trait MergeService {
import MergeService._ import MergeService._
/** /**
* Checks whether conflict will be caused in merging within pull request. * Checks whether conflict will be caused in merging within pull request.
* Returns true if conflict will be caused. * Returns true if conflict will be caused.
*/ */
def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Boolean = { def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Option[String] = {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git => using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
MergeCacheInfo(git, branch, issueId).checkConflict() MergeCacheInfo(git, branch, issueId).checkConflict()
} }
@@ -28,7 +29,7 @@ trait MergeService {
* Returns Some(true) if conflict will be caused. * Returns Some(true) if conflict will be caused.
* Returns None if cache has not created yet. * Returns None if cache has not created yet.
*/ */
def checkConflictCache(userName: String, repositoryName: String, branch: String, issueId: Int): Option[Boolean] = { def checkConflictCache(userName: String, repositoryName: String, branch: String, issueId: Int): Option[Option[String]] = {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git => using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
MergeCacheInfo(git, branch, issueId).checkConflictCache() MergeCacheInfo(git, branch, issueId).checkConflictCache()
} }
@@ -50,7 +51,7 @@ trait MergeService {
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused. * Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/ */
def tryMergeRemote(localUserName: String, localRepositoryName: String, localBranch: String, def tryMergeRemote(localUserName: String, localRepositoryName: String, localBranch: String,
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Option[(ObjectId, ObjectId, ObjectId)] = { remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Either[String, (ObjectId, ObjectId, ObjectId)] = {
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git => using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${remoteBranch}" val remoteRefName = s"refs/heads/${remoteBranch}"
val tmpRefName = s"refs/remote-temp/${remoteUserName}/${remoteRepositoryName}/${remoteBranch}" val tmpRefName = s"refs/remote-temp/${remoteUserName}/${remoteRepositoryName}/${remoteBranch}"
@@ -67,12 +68,12 @@ trait MergeService {
val mergeTip = git.getRepository.resolve(tmpRefName) val mergeTip = git.getRepository.resolve(tmpRefName)
try { try {
if(merger.merge(mergeBaseTip, mergeTip)){ if(merger.merge(mergeBaseTip, mergeTip)){
Some((merger.getResultTreeId, mergeBaseTip, mergeTip)) Right((merger.getResultTreeId, mergeBaseTip, mergeTip))
} else { } else {
None Left(createConflictMessage(mergeTip, mergeBaseTip, merger))
} }
} catch { } catch {
case e: NoMergeBaseException => None case e: NoMergeBaseException => Left(e.toString)
} }
} finally { } finally {
val refUpdate = git.getRepository.updateRef(refSpec.getDestination) val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
@@ -81,24 +82,25 @@ trait MergeService {
} }
} }
} }
/** /**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused. * Checks whether conflict will be caused in merging. Returns `Some(errorMessage)` if conflict will be caused.
*/ */
def checkConflict(userName: String, repositoryName: String, branch: String, def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = requestUserName: String, requestRepositoryName: String, requestBranch: String): Option[String] =
tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).isEmpty tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).left.toOption
def pullRemote(localUserName: String, localRepositoryName: String, localBranch: String, def pullRemote(localUserName: String, localRepositoryName: String, localBranch: String,
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String, remoteUserName: String, remoteRepositoryName: String, remoteBranch: String,
loginAccount: Account, message: String): Option[ObjectId] = { loginAccount: Account, message: String): Option[ObjectId] = {
tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map{ case (newTreeId, oldBaseId, oldHeadId) => tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map { case (newTreeId, oldBaseId, oldHeadId) =>
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git => using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
val committer = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) val committer = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
val newCommit = Util.createMergeCommit(git.getRepository, newTreeId, committer, message, Seq(oldBaseId, oldHeadId)) 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")) Util.updateRefs(git.getRepository, s"refs/heads/${localBranch}", newCommit, false, committer, Some("merge"))
} }
oldBaseId oldBaseId
} }.toOption
} }
} }
@@ -135,27 +137,28 @@ object MergeService{
val conflictedBranchName = s"refs/pull/${issueId}/conflict" val conflictedBranchName = s"refs/pull/${issueId}/conflict"
lazy val mergeBaseTip = repository.resolve(s"refs/heads/${branch}") lazy val mergeBaseTip = repository.resolve(s"refs/heads/${branch}")
lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head") lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head")
def checkConflictCache(): Option[Boolean] = { def checkConflictCache(): Option[Option[String]] = {
Option(repository.resolve(mergedBranchName)).flatMap{ merged => Option(repository.resolve(mergedBranchName)).flatMap { merged =>
if(parseCommit(merged).getParents().toSet == Set( mergeBaseTip, mergeTip )){ if(parseCommit(merged).getParents().toSet == Set( mergeBaseTip, mergeTip )){
// merged branch exists // merged branch exists
Some(false) Some(None)
} else { } else {
None None
} }
}.orElse(Option(repository.resolve(conflictedBranchName)).flatMap{ conflicted => }.orElse(Option(repository.resolve(conflictedBranchName)).flatMap{ conflicted =>
if(parseCommit(conflicted).getParents().toSet == Set( mergeBaseTip, mergeTip )){ val commit = parseCommit(conflicted)
if(commit.getParents().toSet == Set( mergeBaseTip, mergeTip )){
// conflict branch exists // conflict branch exists
Some(true) Some(Some(commit.getFullMessage))
} else { } else {
None None
} }
}) })
} }
def checkConflict():Boolean ={ def checkConflict():Option[String] ={
checkConflictCache.getOrElse(checkConflictForce) checkConflictCache.getOrElse(checkConflictForce)
} }
def checkConflictForce():Boolean ={ def checkConflictForce():Option[String] ={
val merger = MergeStrategy.RECURSIVE.newMerger(repository, true) val merger = MergeStrategy.RECURSIVE.newMerger(repository, true)
val conflicted = try { val conflicted = try {
!merger.merge(mergeBaseTip, mergeTip) !merger.merge(mergeBaseTip, mergeTip)
@@ -172,15 +175,17 @@ object MergeService{
if(!conflicted){ 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() git.branchDelete().setForce(true).setBranchNames(conflictedBranchName).call()
None
} else { } else {
updateBranch(mergeTipCommit.getTree().getId(), s"can't merge ${mergeTip.name} into ${mergeBaseTip.name}", conflictedBranchName) val message = createConflictMessage(mergeTip, mergeBaseTip, merger)
updateBranch(mergeTipCommit.getTree().getId(), message, conflictedBranchName)
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call() git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
Some(message)
} }
conflicted
} }
// update branch from cache // update branch from cache
def merge(message:String, committer:PersonIdent) = { def merge(message:String, committer:PersonIdent) = {
if(checkConflict()){ if(checkConflict().isDefined){
throw new RuntimeException("This pull request can't merge automatically.") 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}")) )
@@ -195,4 +200,13 @@ object MergeService{
private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id)) private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
} }
private def createConflictMessage(mergeTip: ObjectId, mergeBaseTip: ObjectId, merger: Merger): String = {
val mergeResults = merger.asInstanceOf[RecursiveMerger].getMergeResults
s"can't merge ${mergeTip.name} into ${mergeBaseTip.name}\n\n" +
"Conflicting files:\n" +
mergeResults.asScala.map { case (key, _) => "- " + key + "\n" }.mkString
}
} }

View File

@@ -244,8 +244,8 @@ object PullRequestService {
case class PullRequestCount(userName: String, count: Int) case class PullRequestCount(userName: String, count: Int)
case class MergeStatus( case class MergeStatus(
hasConflict: Boolean, conflictMessage: Option[String],
commitStatues:List[CommitStatus], commitStatues: List[CommitStatus],
branchProtection: ProtectedBranchService.ProtectedBranchInfo, branchProtection: ProtectedBranchService.ProtectedBranchInfo,
branchIsOutOfDate: Boolean, branchIsOutOfDate: Boolean,
hasUpdatePermission: Boolean, hasUpdatePermission: Boolean,
@@ -253,12 +253,13 @@ object PullRequestService {
hasMergePermission: Boolean, hasMergePermission: Boolean,
commitIdTo: String){ commitIdTo: String){
val hasConflict = conflictMessage.isDefined
val statuses: List[CommitStatus] = val statuses: List[CommitStatus] =
commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.map(_.context).toSet).map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _)) commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.map(_.context).toSet).map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _))
val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS)) val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS))
val hasProblem = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS) val hasProblem = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
val canUpdate = branchIsOutOfDate && !hasConflict val canUpdate = branchIsOutOfDate && !hasConflict
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
lazy val commitStateSummary:(CommitState, String) = { lazy val commitStateSummary:(CommitState, String) = {
val stateMap = statuses.groupBy(_.state) val stateMap = statuses.groupBy(_.state)
val state = CommitState.combine(stateMap.keySet) val state = CommitState.combine(stateMap.keySet)

View File

@@ -41,6 +41,10 @@
Only those with write access to this repository can merge pull requests. Only those with write access to this repository can merge pull requests.
} }
</div> </div>
<div>
<hr>
@status.conflictMessage.map { message => @helpers.markdown(message, originRepository, false, true, false) }
</div>
} else { } else {
@if(status.branchIsOutOfDate){ @if(status.branchIsOutOfDate){
@if(status.hasUpdatePermission){ @if(status.hasUpdatePermission){