From bf4badad1d68c18d7ffb92c69e09e4e8aa252935 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Tue, 5 Feb 2019 19:54:49 +0800
Subject: [PATCH] Pull request conflict files detection (#5951)

* add conflict detection

* test pull request conflict files

* fix detection files number

* fix comments
---
 models/pull.go                              | 35 ++++++++++++++++++---
 options/locale/locale_en-US.ini             |  1 +
 routers/repo/pull.go                        |  5 +++
 templates/repo/issue/view_content/pull.tmpl |  9 ++++++
 4 files changed, 45 insertions(+), 5 deletions(-)

diff --git a/models/pull.go b/models/pull.go
index d84be139c9..35fbc41788 100644
--- a/models/pull.go
+++ b/models/pull.go
@@ -54,9 +54,10 @@ const (
 
 // PullRequest represents relation between pull request and repositories.
 type PullRequest struct {
-	ID     int64 `xorm:"pk autoincr"`
-	Type   PullRequestType
-	Status PullRequestStatus
+	ID              int64 `xorm:"pk autoincr"`
+	Type            PullRequestType
+	Status          PullRequestStatus
+	ConflictedFiles []string `xorm:"TEXT JSON"`
 
 	IssueID int64  `xorm:"INDEX"`
 	Issue   *Issue `xorm:"-"`
@@ -843,6 +844,7 @@ func (pr *PullRequest) testPatch(e Engine) (err error) {
 		args = append(args, "--ignore-whitespace")
 	}
 	args = append(args, patchPath)
+	pr.ConflictedFiles = []string{}
 
 	_, stderr, err = process.GetManager().ExecDirEnv(-1, "", fmt.Sprintf("testPatch (git apply --check): %d", pr.BaseRepo.ID),
 		[]string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()},
@@ -851,8 +853,26 @@ func (pr *PullRequest) testPatch(e Engine) (err error) {
 		for i := range patchConflicts {
 			if strings.Contains(stderr, patchConflicts[i]) {
 				log.Trace("PullRequest[%d].testPatch (apply): has conflict", pr.ID)
-				fmt.Println(stderr)
+				const prefix = "error: patch failed:"
 				pr.Status = PullRequestStatusConflict
+				pr.ConflictedFiles = make([]string, 0, 5)
+				scanner := bufio.NewScanner(strings.NewReader(stderr))
+				for scanner.Scan() {
+					line := scanner.Text()
+
+					if strings.HasPrefix(line, prefix) {
+						pr.ConflictedFiles = append(pr.ConflictedFiles, strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0]))
+					}
+					// only list 10 conflicted files
+					if len(pr.ConflictedFiles) >= 10 {
+						break
+					}
+				}
+
+				if len(pr.ConflictedFiles) > 0 {
+					log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles)
+				}
+
 				return nil
 			}
 		}
@@ -1375,7 +1395,7 @@ func (pr *PullRequest) checkAndUpdateStatus() {
 
 	// Make sure there is no waiting test to process before leaving the checking status.
 	if !pullRequestQueue.Exist(pr.ID) {
-		if err := pr.UpdateCols("status"); err != nil {
+		if err := pr.UpdateCols("status, conflicted_files"); err != nil {
 			log.Error(4, "Update[%d]: %v", pr.ID, err)
 		}
 	}
@@ -1396,6 +1416,11 @@ func (pr *PullRequest) IsWorkInProgress() bool {
 	return false
 }
 
+// IsFilesConflicted determines if the  Pull Request has changes conflicting with the target branch.
+func (pr *PullRequest) IsFilesConflicted() bool {
+	return len(pr.ConflictedFiles) > 0
+}
+
 // GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress.
 // It returns an empty string when none were found
 func (pr *PullRequest) GetWorkInProgressPrefix() string {
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 502b980c43..f83ad15b3a 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -872,6 +872,7 @@ pulls.has_merged = The pull request has been merged.
 pulls.title_wip_desc = `<a href="#">Start the title with <strong>%s</strong></a> to prevent the pull request from being merged accidentally.`
 pulls.cannot_merge_work_in_progress = This pull request is marked as a work in progress. Remove the <strong>%s</strong> prefix from the title when it's ready
 pulls.data_broken = This pull request is broken due to missing fork information.
+pulls.files_conflicted = This pull request has changes conflicting with the target branch.
 pulls.is_checking = "Merge conflict checking is in progress. Try again in few moments."
 pulls.blocked_by_approvals = "This Pull Request doesn't have enough approvals yet. %d of %d approvals granted."
 pulls.can_auto_merge_desc = This pull request can be merged automatically.
diff --git a/routers/repo/pull.go b/routers/repo/pull.go
index c060aeb4b5..a65b68a96e 100644
--- a/routers/repo/pull.go
+++ b/routers/repo/pull.go
@@ -345,6 +345,11 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.PullReq
 		ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix()
 	}
 
+	if pull.IsFilesConflicted() {
+		ctx.Data["IsPullFilesConflicted"] = true
+		ctx.Data["ConflictedFiles"] = pull.ConflictedFiles
+	}
+
 	ctx.Data["NumCommits"] = prInfo.Commits.Len()
 	ctx.Data["NumFiles"] = prInfo.NumFiles
 	return prInfo
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index 72808d089b..0ddb2820fc 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -38,6 +38,7 @@
 	{{if .Issue.PullRequest.HasMerged}}purple
 	{{else if .Issue.IsClosed}}grey
 	{{else if .IsPullWorkInProgress}}grey
+	{{else if .IsFilesConflicted}}grey
 	{{else if .IsPullRequestBroken}}red
 	{{else if .IsBlockedByApprovals}}red
 	{{else if .Issue.PullRequest.IsChecking}}yellow
@@ -59,6 +60,14 @@
 				<div class="item text grey">
 					{{$.i18n.Tr "repo.pulls.reopen_to_merge"}}
 				</div>
+			{{else if .IsPullFilesConflicted}}
+				<div class="item text grey">
+					<span class="octicon octicon-x"></span>
+					{{$.i18n.Tr "repo.pulls.files_conflicted"}}
+					{{range .ConflictedFiles}}
+						<div>{{.}}</div>
+					{{end}}
+				</div>
 			{{else if .IsPullRequestBroken}}
 				<div class="item text red">
 					<span class="octicon octicon-x"></span>