From bf5af87eef8913004df63aef58f71628f9c057d0 Mon Sep 17 00:00:00 2001
From: Elias Norberg <elias@aisle.se>
Date: Tue, 2 Apr 2019 21:54:29 +0200
Subject: [PATCH] Show last commit status in pull request lists (#6465)

---
 integrations/pull_status_test.go     | 93 ++++++++++++++++++++++++++++
 models/pull.go                       | 25 ++++++++
 routers/repo/issue.go                |  8 +++
 routers/user/home.go                 |  6 ++
 templates/repo/issue/list.tmpl       |  6 ++
 templates/user/dashboard/issues.tmpl |  6 ++
 6 files changed, 144 insertions(+)
 create mode 100644 integrations/pull_status_test.go

diff --git a/integrations/pull_status_test.go b/integrations/pull_status_test.go
new file mode 100644
index 0000000000..ad8bb236ef
--- /dev/null
+++ b/integrations/pull_status_test.go
@@ -0,0 +1,93 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+package integrations
+
+import (
+	"fmt"
+	"net/http"
+	"path"
+	"testing"
+
+	"code.gitea.io/gitea/models"
+	api "code.gitea.io/sdk/gitea"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestPullCreate_CommitStatus(t *testing.T) {
+	prepareTestEnv(t)
+	session := loginUser(t, "user1")
+	testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
+	testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
+
+	url := path.Join("user1", "repo1", "compare", "master...status1")
+	req := NewRequestWithValues(t, "POST", url,
+		map[string]string{
+			"_csrf": GetCSRF(t, session, url),
+			"title": "pull request from status1",
+		},
+	)
+	session.MakeRequest(t, req, http.StatusFound)
+
+	req = NewRequest(t, "GET", "/user1/repo1/pulls")
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	doc := NewHTMLParser(t, resp.Body)
+
+	// Request repository commits page
+	req = NewRequest(t, "GET", "/user1/repo1/pulls/1/commits")
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	doc = NewHTMLParser(t, resp.Body)
+
+	// Get first commit URL
+	commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
+	assert.True(t, exists)
+	assert.NotEmpty(t, commitURL)
+
+	commitID := path.Base(commitURL)
+
+	statusList := []models.CommitStatusState{
+		models.CommitStatusPending,
+		models.CommitStatusError,
+		models.CommitStatusFailure,
+		models.CommitStatusWarning,
+		models.CommitStatusSuccess,
+	}
+
+	statesIcons := map[models.CommitStatusState]string{
+		models.CommitStatusPending: "circle icon yellow",
+		models.CommitStatusSuccess: "check icon green",
+		models.CommitStatusError:   "warning icon red",
+		models.CommitStatusFailure: "remove icon red",
+		models.CommitStatusWarning: "warning sign icon yellow",
+	}
+
+	// Update commit status, and check if icon is updated as well
+	for _, status := range statusList {
+
+		// Call API to add status for commit
+		token := getTokenForLoggedInUser(t, session)
+		req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/user1/repo1/statuses/%s?token=%s", commitID, token),
+			api.CreateStatusOption{
+				State:       api.StatusState(status),
+				TargetURL:   "http://test.ci/",
+				Description: "",
+				Context:     "testci",
+			},
+		)
+		session.MakeRequest(t, req, http.StatusCreated)
+
+		req = NewRequestf(t, "GET", "/user1/repo1/pulls/1/commits")
+		resp = session.MakeRequest(t, req, http.StatusOK)
+		doc = NewHTMLParser(t, resp.Body)
+
+		commitURL, exists = doc.doc.Find("#commits-table tbody tr td.sha a").Last().Attr("href")
+		assert.True(t, exists)
+		assert.NotEmpty(t, commitURL)
+		assert.EqualValues(t, commitID, path.Base(commitURL))
+
+		cls, ok := doc.doc.Find("#commits-table tbody tr td.message i.commit-status").Last().Attr("class")
+		assert.True(t, ok)
+		assert.EqualValues(t, "commit-status "+statesIcons[status], cls)
+	}
+}
diff --git a/models/pull.go b/models/pull.go
index ccd8155317..88f9b1f6e8 100644
--- a/models/pull.go
+++ b/models/pull.go
@@ -292,6 +292,31 @@ func (pr *PullRequest) CanAutoMerge() bool {
 	return pr.Status == PullRequestStatusMergeable
 }
 
+// GetLastCommitStatus returns the last commit status for this pull request.
+func (pr *PullRequest) GetLastCommitStatus() (status *CommitStatus, err error) {
+	if err = pr.GetHeadRepo(); err != nil {
+		return nil, err
+	}
+
+	headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
+	if err != nil {
+		return nil, err
+	}
+
+	repo := pr.HeadRepo
+	lastCommitID, err := headGitRepo.GetBranchCommitID(pr.HeadBranch)
+	if err != nil {
+		return nil, err
+	}
+
+	var statusList []*CommitStatus
+	statusList, err = GetLatestCommitStatus(repo, lastCommitID, 0)
+	if err != nil {
+		return nil, err
+	}
+	return CalcCommitStatus(statusList), nil
+}
+
 // MergeStyle represents the approach to merge commits into base branch.
 type MergeStyle string
 
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index 45033e67c2..42f3ddf4e8 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -214,6 +214,8 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB
 		}
 	}
 
+	var commitStatus = make(map[int64]*models.CommitStatus, len(issues))
+
 	// Get posters.
 	for i := range issues {
 		// Check read status
@@ -223,8 +225,14 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB
 			ctx.ServerError("GetIsRead", err)
 			return
 		}
+
+		if isPullOption == util.OptionalBoolTrue {
+			commitStatus[issues[i].PullRequest.ID], _ = issues[i].PullRequest.GetLastCommitStatus()
+		}
 	}
+
 	ctx.Data["Issues"] = issues
+	ctx.Data["CommitStatus"] = commitStatus
 
 	// Get assignees.
 	ctx.Data["Assignees"], err = repo.GetAssignees()
diff --git a/routers/user/home.go b/routers/user/home.go
index c4e169befd..740a9edc4e 100644
--- a/routers/user/home.go
+++ b/routers/user/home.go
@@ -319,8 +319,13 @@ func Issues(ctx *context.Context) {
 		return
 	}
 
+	var commitStatus = make(map[int64]*models.CommitStatus, len(issues))
 	for _, issue := range issues {
 		issue.Repo = showReposMap[issue.RepoID]
+
+		if isPullList {
+			commitStatus[issue.PullRequest.ID], _ = issue.PullRequest.GetLastCommitStatus()
+		}
 	}
 
 	issueStats, err := models.GetUserIssueStats(models.UserIssueStatsOptions{
@@ -344,6 +349,7 @@ func Issues(ctx *context.Context) {
 	}
 
 	ctx.Data["Issues"] = issues
+	ctx.Data["CommitStatus"] = commitStatus
 	ctx.Data["Repos"] = showRepos
 	ctx.Data["Counts"] = counts
 	ctx.Data["Page"] = paginater.New(total, setting.UI.IssuePagingNum, page, 5)
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index 78352acb50..88716484d2 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -203,6 +203,12 @@
 					<div class="ui {{if .IsRead}}black{{else}}green{{end}} label">#{{.Index}}</div>
 					<a class="title has-emoji" href="{{$.Link}}/{{.Index}}">{{.Title}}</a>
 
+                    {{if .IsPull }}
+                        {{if (index $.CommitStatus .ID)}}
+                            {{template "repo/commit_status" (index $.CommitStatus .ID)}}
+						{{end}}
+					{{end}}
+
 					{{if .Ref}}
 						<a class="ui label" href="{{$.RepoLink}}/src/branch/{{.Ref}}">{{.Ref}}</a>
 					{{end}}
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index 93286df578..0caa9a60dd 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -66,6 +66,12 @@
 							<div class="ui label">{{if not $.RepoID}}{{.Repo.FullName}}{{end}}#{{.Index}}</div>
 							<a class="title has-emoji" href="{{AppSubUrl}}/{{.Repo.Owner.Name}}/{{.Repo.Name}}/issues/{{.Index}}">{{.Title}}</a>
 
+                            {{if .IsPull }}
+                                {{if (index $.CommitStatus .ID)}}
+                                    {{template "repo/commit_status" (index $.CommitStatus .ID)}}
+                                {{end}}
+                            {{end}}
+
 							{{with .Labels}}
 								{{/* If we have any labels, we should show them
 								with a 2.5 line height, this way they don't look