diff --git a/integrations/editor_test.go b/integrations/editor_test.go index e2dd2e1dc4b87..7c26fbdac0cb0 100644 --- a/integrations/editor_test.go +++ b/integrations/editor_test.go @@ -150,6 +150,19 @@ func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, bra return resp } +func testEditFileToNewBranchAndSendPull(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) *httptest.ResponseRecorder { + testEditFileToNewBranch(t, session, user, repo, branch, targetBranch, filePath, newContent) + + url := path.Join(user, repo, "compare", branch+"..."+targetBranch) + req := NewRequestWithValues(t, "POST", url, + map[string]string{ + "_csrf": GetCSRF(t, session, url), + "title": "pull request from " + targetBranch, + }, + ) + return session.MakeRequest(t, req, http.StatusFound) +} + func TestEditFile(t *testing.T) { prepareTestEnv(t) session := loginUser(t, "user2") diff --git a/integrations/repo_pull_status_test.go b/integrations/repo_pull_status_test.go new file mode 100644 index 0000000000000..d0f9f1a5392e5 --- /dev/null +++ b/integrations/repo_pull_status_test.go @@ -0,0 +1,128 @@ +// Copyright 2017 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" + "strings" + "testing" + + "code.gitea.io/gitea/models" + api "code.gitea.io/sdk/gitea" + + "github.com/PuerkitoBio/goquery" + "github.com/stretchr/testify/assert" +) + +var ( + 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", + } +) + +func TestRepoPullsWithStatus(t *testing.T) { + prepareTestEnv(t) + + session := loginUser(t, "user2") + + var size = 5 + // create some pulls + for i := 0; i < size; i++ { + testEditFileToNewBranchAndSendPull(t, session, "user2", "repo16", "master", fmt.Sprintf("test%d", i), "readme.md", fmt.Sprintf("test%d", i)) + } + + // look for repo's pulls page + req := NewRequest(t, "GET", "/user2/repo16/pulls") + resp := session.MakeRequest(t, req, http.StatusOK) + doc := NewHTMLParser(t, resp.Body) + + var indexes = make([]string, 0, size) + doc.doc.Find("li.item").Each(func(idx int, s *goquery.Selection) { + indexes = append(indexes, strings.TrimLeft(s.Find("div").Eq(1).Text(), "#")) + }) + + indexes = indexes[:5] + + var status = make([]models.CommitStatusState, len(indexes)) + for i := 0; i < len(indexes); i++ { + switch i { + case 0: + status[i] = models.CommitStatusPending + case 1: + status[i] = models.CommitStatusSuccess + case 2: + status[i] = models.CommitStatusError + case 3: + status[i] = models.CommitStatusFailure + case 4: + status[i] = models.CommitStatusWarning + default: + status[i] = models.CommitStatusSuccess + } + } + + for i, index := range indexes { + // Request repository commits page + req = NewRequestf(t, "GET", "/user2/repo16/pulls/%s/commits", index) + 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) + // Call API to add status for commit + req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo16/statuses/"+commitID, + api.CreateStatusOption{ + State: api.StatusState(status[i]), + TargetURL: "http://test.ci/", + Description: "", + Context: "testci", + }, + ) + session.MakeRequest(t, req, http.StatusCreated) + + req = NewRequestf(t, "GET", "/user2/repo16/pulls/%s/commits", index) + 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[i]], cls) + } + + req = NewRequest(t, "GET", "/user2/repo16/pulls") + resp = session.MakeRequest(t, req, http.StatusOK) + doc = NewHTMLParser(t, resp.Body) + + doc.doc.Find("li.item").Each(func(i int, s *goquery.Selection) { + cls, ok := s.Find("i.commit-status").Attr("class") + assert.True(t, ok) + assert.EqualValues(t, "commit-status "+statesIcons[status[i]], cls) + }) + + req = NewRequest(t, "GET", "/pulls?type=all&repo=16&sort=&state=open") + resp = session.MakeRequest(t, req, http.StatusOK) + doc = NewHTMLParser(t, resp.Body) + + doc.doc.Find("li.item").Each(func(i int, s *goquery.Selection) { + cls, ok := s.Find("i.commit-status").Attr("class") + assert.True(t, ok) + assert.EqualValues(t, "commit-status "+statesIcons[status[i]], cls) + }) +} diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index f494cdd1b7efa..b1b7b44e5ba4f 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -220,4 +220,39 @@ repo_id: 28 type: 1 config: "{}" - created_unix: 1524304355 \ No newline at end of file + created_unix: 1524304355 + +- + id: 33 + repo_id: 16 + type: 1 + config: "{}" + created_unix: 946684810 + +- + id: 34 + repo_id: 16 + type: 2 + config: "{\"EnableTimetracker\":false,\"AllowOnlyContributorsToTrackTime\":false}" + created_unix: 946684810 + +- + id: 35 + repo_id: 16 + type: 3 + config: "{}" + created_unix: 946684810 + +- + id: 36 + repo_id: 16 + type: 4 + config: "{}" + created_unix: 946684810 + +- + id: 37 + repo_id: 16 + type: 5 + config: "{}" + created_unix: 946684810 \ No newline at end of file diff --git a/models/pull.go b/models/pull.go index d84be139c9677..3f734abc5c08a 100644 --- a/models/pull.go +++ b/models/pull.go @@ -71,12 +71,12 @@ type PullRequest struct { BaseBranch string ProtectedBranch *ProtectedBranch `xorm:"-"` MergeBase string `xorm:"VARCHAR(40)"` - - HasMerged bool `xorm:"INDEX"` - MergedCommitID string `xorm:"VARCHAR(40)"` - MergerID int64 `xorm:"INDEX"` - Merger *User `xorm:"-"` - MergedUnix util.TimeStamp `xorm:"updated INDEX"` + LastCommitID string `xorm:"-"` + HasMerged bool `xorm:"INDEX"` + MergedCommitID string `xorm:"VARCHAR(40)"` + MergerID int64 `xorm:"INDEX"` + Merger *User `xorm:"-"` + MergedUnix util.TimeStamp `xorm:"updated INDEX"` } // Note: don't try to get Issue because will end up recursive querying. diff --git a/models/status.go b/models/status.go index 91d011f7c512c..60591ca6009d9 100644 --- a/models/status.go +++ b/models/status.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/util" api "code.gitea.io/sdk/gitea" + "github.com/go-xorm/builder" "github.com/go-xorm/xorm" ) @@ -157,6 +158,77 @@ func GetLatestCommitStatus(repo *Repository, sha string, page int) ([]*CommitSta return statuses, x.In("id", ids).Find(&statuses) } +// GetIssuesLatestCommitStatuses returns all statuses with given repoIDs and shas +func GetIssuesLatestCommitStatuses(issues []*Issue) ([][]*CommitStatus, error) { + var cond = builder.NewCond() + var repoCache = make(map[int64]*git.Repository) + var err error + for i := 0; i < len(issues); i++ { + var gitRepo *git.Repository + var ok bool + if gitRepo, ok = repoCache[issues[i].PullRequest.HeadRepoID]; !ok { + if err := issues[i].PullRequest.GetHeadRepo(); err != nil { + log.Error(4, "GetHeadRepo[%d, %d]: %v", issues[i].PullRequest.ID, issues[i].PullRequest.HeadRepoID, err) + continue + } + + gitRepo, err = git.OpenRepository(issues[i].PullRequest.HeadRepo.RepoPath()) + if err != nil { + log.Error(4, "OpenRepository[%d, %s]: %v", issues[i].PullRequest.ID, issues[i].PullRequest.HeadRepo.RepoPath(), err) + continue + } + repoCache[issues[i].PullRequest.HeadRepoID] = gitRepo + } + + issues[i].PullRequest.LastCommitID, err = gitRepo.GetBranchCommitID(issues[i].PullRequest.HeadBranch) + if err != nil { + log.Error(4, "GetBranchCommitID[%d, %s]: %v", issues[i].PullRequest.ID, issues[i].PullRequest.HeadBranch, err) + continue + } + + cond = cond.Or(builder.Eq{ + "repo_id": issues[i].RepoID, + "sha": issues[i].PullRequest.LastCommitID, + }) + } + + var ids = make([]int64, 0, len(issues)) + err = x.Table("commit_status"). + Where(cond). + Select("max( id ) as id"). + GroupBy("repo_id, sha, context"). + OrderBy("max( id ) desc"). + Find(&ids) + if err != nil { + return nil, err + } + + var returns = make([][]*CommitStatus, len(issues)) + if len(ids) == 0 { + return returns, nil + } + + statuses := make(map[int64]*CommitStatus, len(ids)) + err = x.In("id", ids).Find(&statuses) + if err != nil { + return nil, err + } + + var repoIDsMap = make(map[string][]int64, len(issues)) + for _, status := range statuses { + key := fmt.Sprintf("%d-%s", status.RepoID, status.SHA) + repoIDsMap[key] = append(repoIDsMap[key], status.ID) + } + + for i := 0; i < len(issues); i++ { + key := fmt.Sprintf("%d-%s", issues[i].RepoID, issues[i].PullRequest.LastCommitID) + for _, id := range repoIDsMap[key] { + returns[i] = append(returns[i], statuses[id]) + } + } + return returns, nil +} + // GetCommitStatus populates a given status for a given commit. // NOTE: If ID or Index isn't given, and only Context, TargetURL and/or Description // is given, the CommitStatus created _last_ will be returned. diff --git a/routers/repo/issue.go b/routers/repo/issue.go index bcc648900a75a..2dfb99ff4a401 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -194,7 +194,7 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB } // Get posters. - for i := range issues { + for i := 0; i < len(issues); i++ { // Check read status if !ctx.IsSigned { issues[i].IsRead = true @@ -203,6 +203,22 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB return } } + + if isPullOption == util.OptionalBoolTrue && len(issues) > 0 { + commitStatuses, err := models.GetIssuesLatestCommitStatuses(issues) + if err != nil { + ctx.ServerError("GetIssuesLatestCommitStatuses", err) + return + } + + var issuesStates = make(map[int64]*models.CommitStatus, len(issues)) + for i, statuses := range commitStatuses { + issuesStates[issues[i].PullRequest.ID] = models.CalcCommitStatus(statuses) + } + + ctx.Data["IssuesStates"] = issuesStates + } + ctx.Data["Issues"] = issues // Get assignees. diff --git a/routers/user/home.go b/routers/user/home.go index 99c747e12cf9c..f66ffa16384bc 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -321,6 +321,20 @@ func Issues(ctx *context.Context) { for _, issue := range issues { issue.Repo = showReposMap[issue.RepoID] } + if isPullList && len(issues) > 0 { + commitStatuses, err := models.GetIssuesLatestCommitStatuses(issues) + if err != nil { + ctx.ServerError("GetIssuesLatestCommitStatuses", err) + return + } + + var issuesStates = make(map[int64]*models.CommitStatus, len(issues)) + for i, statuses := range commitStatuses { + issuesStates[issues[i].PullRequest.ID] = models.CalcCommitStatus(statuses) + } + + ctx.Data["IssuesStates"] = issuesStates + } issueStats, err := models.GetUserIssueStats(models.UserIssueStatsOptions{ UserID: ctxUser.ID, diff --git a/templates/repo/commit_status.tmpl b/templates/repo/commit_status.tmpl index f5bbbb02d61d2..19b6e804844fd 100644 --- a/templates/repo/commit_status.tmpl +++ b/templates/repo/commit_status.tmpl @@ -1,15 +1,17 @@ -{{if eq .State "pending"}} - -{{end}} -{{if eq .State "success"}} - -{{end}} -{{if eq .State "error"}} - -{{end}} -{{if eq .State "failure"}} - -{{end}} -{{if eq .State "warning"}} - +{{if .}} + {{if eq .State "pending"}} + + {{end}} + {{if eq .State "success"}} + + {{end}} + {{if eq .State "error"}} + + {{end}} + {{if eq .State "failure"}} + + {{end}} + {{if eq .State "warning"}} + + {{end}} {{end}} diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 10925a741d227..aed86d43644d8 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -188,7 +188,9 @@