Add commit statuses reports on pull request view (#6845)

* Add commit statuses reports on pull view

* Add some translations

* improve the UI

* fix fmt

* fix tests

* add a new test git repo to fix tests

* fix bug when headRepo or headBranch missing

* fix tests

* fix tests

* fix consistency

* fix tests

* fix tests

* change the test repo

* fix tests

* fix tests

* fix migration

* keep db size consistency

* fix translation

* change commit hash status table unique index

* remove unused table

* use char instead varchar

* make hashCommitStatusContext private

* split merge section with status check on pull view ui

* fix tests; fix arc-green theme on pull ui
release/v1.15
Lunny Xiao 2019-06-30 15:57:59 +08:00 committed by zeripath
parent 1e46eedce7
commit ff85dd3e12
23 changed files with 237 additions and 86 deletions

View File

@ -0,0 +1 @@
0abcb056019adb8336cf9db3ad9d9cf80cd4b141

View File

@ -159,7 +159,7 @@ func TestCantMergeWorkInProgress(t *testing.T) {
req := NewRequest(t, "GET", resp.Header().Get("Location")) req := NewRequest(t, "GET", resp.Header().Get("Location"))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
text := strings.TrimSpace(htmlDoc.doc.Find(".merge.segment > .text.grey").Text()) text := strings.TrimSpace(htmlDoc.doc.Find(".merge-section.segment > .text.grey").Text())
assert.NotEmpty(t, text, "Can't find WIP text") assert.NotEmpty(t, text, "Can't find WIP text")
// remove <strong /> from lang // remove <strong /> from lang

View File

@ -6,16 +6,14 @@ package models
import ( import (
"container/list" "container/list"
"crypto/sha1"
"fmt" "fmt"
"strings" "strings"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/go-xorm/xorm"
) )
// CommitStatusState holds the state of a Status // CommitStatusState holds the state of a Status
@ -61,6 +59,7 @@ type CommitStatus struct {
SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"` SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"`
TargetURL string `xorm:"TEXT"` TargetURL string `xorm:"TEXT"`
Description string `xorm:"TEXT"` Description string `xorm:"TEXT"`
ContextHash string `xorm:"char(40) index"`
Context string `xorm:"TEXT"` Context string `xorm:"TEXT"`
Creator *User `xorm:"-"` Creator *User `xorm:"-"`
CreatorID int64 CreatorID int64
@ -146,7 +145,7 @@ func GetLatestCommitStatus(repo *Repository, sha string, page int) ([]*CommitSta
Table(&CommitStatus{}). Table(&CommitStatus{}).
Where("repo_id = ?", repo.ID).And("sha = ?", sha). Where("repo_id = ?", repo.ID).And("sha = ?", sha).
Select("max( id ) as id"). Select("max( id ) as id").
GroupBy("context").OrderBy("max( id ) desc").Find(&ids) GroupBy("context_hash").OrderBy("max( id ) desc").Find(&ids)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -157,27 +156,6 @@ func GetLatestCommitStatus(repo *Repository, sha string, page int) ([]*CommitSta
return statuses, x.In("id", ids).Find(&statuses) return statuses, x.In("id", ids).Find(&statuses)
} }
// 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.
func GetCommitStatus(repo *Repository, sha string, status *CommitStatus) (*CommitStatus, error) {
conds := &CommitStatus{
Context: status.Context,
State: status.State,
TargetURL: status.TargetURL,
Description: status.Description,
}
has, err := x.Where("repo_id = ?", repo.ID).And("sha = ?", sha).Desc("created_unix").Get(conds)
if err != nil {
return nil, fmt.Errorf("GetCommitStatus[%s, %s]: %v", repo.RepoPath(), sha, err)
}
if !has {
return nil, fmt.Errorf("GetCommitStatus[%s, %s]: not found", repo.RepoPath(), sha)
}
return conds, nil
}
// NewCommitStatusOptions holds options for creating a CommitStatus // NewCommitStatusOptions holds options for creating a CommitStatus
type NewCommitStatusOptions struct { type NewCommitStatusOptions struct {
Repo *Repository Repo *Repository
@ -186,30 +164,30 @@ type NewCommitStatusOptions struct {
CommitStatus *CommitStatus CommitStatus *CommitStatus
} }
func newCommitStatus(sess *xorm.Session, opts NewCommitStatusOptions) error { // NewCommitStatus save commit statuses into database
func NewCommitStatus(opts NewCommitStatusOptions) error {
if opts.Repo == nil {
return fmt.Errorf("NewCommitStatus[nil, %s]: no repository specified", opts.SHA)
}
repoPath := opts.Repo.RepoPath()
if opts.Creator == nil {
return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA)
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v", opts.Repo.ID, opts.Creator.ID, opts.SHA, err)
}
opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description) opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description)
opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context) opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context)
opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL) opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL)
opts.CommitStatus.SHA = opts.SHA opts.CommitStatus.SHA = opts.SHA
opts.CommitStatus.CreatorID = opts.Creator.ID opts.CommitStatus.CreatorID = opts.Creator.ID
if opts.Repo == nil {
return fmt.Errorf("newCommitStatus[nil, %s]: no repository specified", opts.SHA)
}
opts.CommitStatus.RepoID = opts.Repo.ID opts.CommitStatus.RepoID = opts.Repo.ID
repoPath := opts.Repo.repoPath(sess)
if opts.Creator == nil {
return fmt.Errorf("newCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA)
}
gitRepo, err := git.OpenRepository(repoPath)
if err != nil {
return fmt.Errorf("OpenRepository[%s]: %v", repoPath, err)
}
if _, err := gitRepo.GetCommit(opts.SHA); err != nil {
return fmt.Errorf("GetCommit[%s]: %v", opts.SHA, err)
}
// Get the next Status Index // Get the next Status Index
var nextIndex int64 var nextIndex int64
@ -220,46 +198,25 @@ func newCommitStatus(sess *xorm.Session, opts NewCommitStatusOptions) error {
has, err := sess.Desc("index").Limit(1).Get(lastCommitStatus) has, err := sess.Desc("index").Limit(1).Get(lastCommitStatus)
if err != nil { if err != nil {
if err := sess.Rollback(); err != nil { if err := sess.Rollback(); err != nil {
log.Error("newCommitStatus: sess.Rollback: %v", err) log.Error("NewCommitStatus: sess.Rollback: %v", err)
} }
return fmt.Errorf("newCommitStatus[%s, %s]: %v", repoPath, opts.SHA, err) return fmt.Errorf("NewCommitStatus[%s, %s]: %v", repoPath, opts.SHA, err)
} }
if has { if has {
log.Debug("newCommitStatus[%s, %s]: found", repoPath, opts.SHA) log.Debug("NewCommitStatus[%s, %s]: found", repoPath, opts.SHA)
nextIndex = lastCommitStatus.Index nextIndex = lastCommitStatus.Index
} }
opts.CommitStatus.Index = nextIndex + 1 opts.CommitStatus.Index = nextIndex + 1
log.Debug("newCommitStatus[%s, %s]: %d", repoPath, opts.SHA, opts.CommitStatus.Index) log.Debug("NewCommitStatus[%s, %s]: %d", repoPath, opts.SHA, opts.CommitStatus.Index)
opts.CommitStatus.ContextHash = hashCommitStatusContext(opts.CommitStatus.Context)
// Insert new CommitStatus // Insert new CommitStatus
if _, err = sess.Insert(opts.CommitStatus); err != nil { if _, err = sess.Insert(opts.CommitStatus); err != nil {
if err := sess.Rollback(); err != nil { if err := sess.Rollback(); err != nil {
log.Error("newCommitStatus: sess.Rollback: %v", err) log.Error("Insert CommitStatus: sess.Rollback: %v", err)
} }
return fmt.Errorf("newCommitStatus[%s, %s]: %v", repoPath, opts.SHA, err) return fmt.Errorf("Insert CommitStatus[%s, %s]: %v", repoPath, opts.SHA, err)
}
return nil
}
// NewCommitStatus creates a new CommitStatus given a bunch of parameters
// NOTE: All text-values will be trimmed from whitespaces.
// Requires: Repo, Creator, SHA
func NewCommitStatus(repo *Repository, creator *User, sha string, status *CommitStatus) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v", repo.ID, creator.ID, sha, err)
}
if err := newCommitStatus(sess, NewCommitStatusOptions{
Repo: repo,
Creator: creator,
SHA: sha,
CommitStatus: status,
}); err != nil {
return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v", repo.ID, creator.ID, sha, err)
} }
return sess.Commit() return sess.Commit()
@ -295,3 +252,8 @@ func ParseCommitsWithStatus(oldCommits *list.List, repo *Repository) *list.List
} }
return newCommits return newCommits
} }
// hashCommitStatusContext hash context
func hashCommitStatusContext(context string) string {
return fmt.Sprintf("%x", sha1.Sum([]byte(context)))
}

View File

@ -86,3 +86,14 @@
created_unix: 946684830 created_unix: 946684830
updated_unix: 978307200 updated_unix: 978307200
-
id: 8
repo_id: 10
index: 1
poster_id: 11
name: pr2
content: a pull request
is_closed: false
is_pull: true
created_unix: 946684820
updated_unix: 978307180

View File

@ -26,3 +26,17 @@
base_branch: master base_branch: master
merge_base: fedcba9876543210 merge_base: fedcba9876543210
has_merged: false has_merged: false
-
id: 3
type: 0 # gitea pull request
status: 2 # mergable
issue_id: 8
index: 1
head_repo_id: 11
base_repo_id: 10
head_user_name: user13
head_branch: branch2
base_branch: master
merge_base: 0abcb056019adb83
has_merged: false

View File

@ -118,7 +118,7 @@
is_private: false is_private: false
num_issues: 0 num_issues: 0
num_closed_issues: 0 num_closed_issues: 0
num_pulls: 0 num_pulls: 1
num_closed_pulls: 0 num_closed_pulls: 0
is_mirror: false is_mirror: false
num_forks: 1 num_forks: 1

View File

@ -229,6 +229,8 @@ var migrations = []Migration{
NewMigration("add http method to webhook", addHTTPMethodToWebhook), NewMigration("add http method to webhook", addHTTPMethodToWebhook),
// v87 -> v88 // v87 -> v88
NewMigration("add avatar field to repository", addAvatarFieldToRepository), NewMigration("add avatar field to repository", addAvatarFieldToRepository),
// v88 -> v89
NewMigration("add commit status context field to commit_status", addCommitStatusContext),
} }
// Migrate database to current version // Migrate database to current version

View File

@ -1,4 +1,4 @@
// Copyright 2019 Gitea. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style // Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

66
models/migrations/v88.go Normal file
View File

@ -0,0 +1,66 @@
// 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 migrations
import (
"crypto/sha1"
"fmt"
"github.com/go-xorm/xorm"
)
func hashContext(context string) string {
return fmt.Sprintf("%x", sha1.Sum([]byte(context)))
}
func addCommitStatusContext(x *xorm.Engine) error {
type CommitStatus struct {
ID int64 `xorm:"pk autoincr"`
ContextHash string `xorm:"char(40) index"`
Context string `xorm:"TEXT"`
}
if err := x.Sync2(new(CommitStatus)); err != nil {
return err
}
sess := x.NewSession()
defer sess.Close()
var start = 0
for {
var statuses = make([]*CommitStatus, 0, 100)
err := sess.OrderBy("id").Limit(100, start).Find(&statuses)
if err != nil {
return err
}
if len(statuses) == 0 {
break
}
if err = sess.Begin(); err != nil {
return err
}
for _, status := range statuses {
status.ContextHash = hashContext(status.Context)
if _, err := sess.ID(status.ID).Cols("context_hash").Update(status); err != nil {
return err
}
}
if err := sess.Commit(); err != nil {
return err
}
if len(statuses) < 100 {
break
}
start += len(statuses)
}
return nil
}

View File

@ -0,0 +1,39 @@
// 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 repofiles
import (
"fmt"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
)
// CreateCommitStatus creates a new CommitStatus given a bunch of parameters
// NOTE: All text-values will be trimmed from whitespaces.
// Requires: Repo, Creator, SHA
func CreateCommitStatus(repo *models.Repository, creator *models.User, sha string, status *models.CommitStatus) error {
repoPath := repo.RepoPath()
// confirm that commit is exist
gitRepo, err := git.OpenRepository(repoPath)
if err != nil {
return fmt.Errorf("OpenRepository[%s]: %v", repoPath, err)
}
if _, err := gitRepo.GetCommit(sha); err != nil {
return fmt.Errorf("GetCommit[%s]: %v", sha, err)
}
if err := models.NewCommitStatus(models.NewCommitStatusOptions{
Repo: repo,
Creator: creator,
SHA: sha,
CommitStatus: status,
}); err != nil {
return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %v", repo.ID, creator.ID, sha, err)
}
return nil
}

View File

@ -981,6 +981,9 @@ pulls.rebase_merge_commit_pull_request = Rebase and Merge (--no-ff)
pulls.squash_merge_pull_request = Squash and Merge pulls.squash_merge_pull_request = Squash and Merge
pulls.invalid_merge_option = You cannot use this merge option for this pull request. pulls.invalid_merge_option = You cannot use this merge option for this pull request.
pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.` pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.`
pulls.status_checking = Some checks are pending
pulls.status_checks_success = All checks were successful
pulls.status_checks_error = Some checks failed
milestones.new = New Milestone milestones.new = New Milestone
milestones.open_tab = %d Open milestones.open_tab = %d Open

View File

@ -535,6 +535,7 @@ footer .ui.left,footer .ui.right{line-height:40px}
.repository.view.issue .comment-list .comment .content>.header:before{border-right-color:#d3d3d4;border-width:9px;margin-top:-9px} .repository.view.issue .comment-list .comment .content>.header:before{border-right-color:#d3d3d4;border-width:9px;margin-top:-9px}
.repository.view.issue .comment-list .comment .content>.header:after{border-right-color:#f7f7f7;border-width:8px;margin-top:-8px} .repository.view.issue .comment-list .comment .content>.header:after{border-right-color:#f7f7f7;border-width:8px;margin-top:-8px}
.repository.view.issue .comment-list .comment .content>.header .text{max-width:78%;padding-top:10px;padding-bottom:10px} .repository.view.issue .comment-list .comment .content>.header .text{max-width:78%;padding-top:10px;padding-bottom:10px}
.repository.view.issue .comment-list .comment .content>.merge-section{border-top:1px solid #d4d4d5;background-color:#f7f7f7}
.repository.view.issue .comment-list .comment .content .markdown{font-size:14px} .repository.view.issue .comment-list .comment .content .markdown{font-size:14px}
.repository.view.issue .comment-list .comment .content .no-content{color:#767676;font-style:italic} .repository.view.issue .comment-list .comment .content .no-content{color:#767676;font-style:italic}
.repository.view.issue .comment-list .comment .content>.bottom.segment{background:#f3f4f5} .repository.view.issue .comment-list .comment .content>.bottom.segment{background:#f3f4f5}

View File

@ -111,6 +111,7 @@ footer{background:#2e323e;border-top:1px solid #313131}
.ui.attached.segment{border:1px solid #404552} .ui.attached.segment{border:1px solid #404552}
.repository.view.issue .comment-list .comment .content>.bottom.segment{background:#353945} .repository.view.issue .comment-list .comment .content>.bottom.segment{background:#353945}
.repository.view.issue .comment-list .comment .content .header{color:#dbdbdb;background-color:#404552;border-bottom:1px solid #353944} .repository.view.issue .comment-list .comment .content .header{color:#dbdbdb;background-color:#404552;border-bottom:1px solid #353944}
.repository.view.issue .comment-list .comment .content .merge-section{background-color:#404552;border-top:1px solid #353944}
.ui .text.grey a{color:#dbdbdb!important} .ui .text.grey a{color:#dbdbdb!important}
.ui.comments .comment .actions a{color:#dbdbdb} .ui.comments .comment .actions a{color:#dbdbdb}
.repository.view.issue .comment-list .comment .content .header:after{border-right-color:#404552} .repository.view.issue .comment-list .comment .content .header:after{border-right-color:#404552}

View File

@ -813,6 +813,11 @@
} }
} }
> .merge-section {
border-top: 1px solid #d4d4d5;
background-color: #f7f7f7;
}
.markdown { .markdown {
font-size: 14px; font-size: 14px;
} }

View File

@ -590,6 +590,11 @@ a.ui.basic.green.label:hover {
border-bottom: 1px solid #353944; border-bottom: 1px solid #353944;
} }
.repository.view.issue .comment-list .comment .content .merge-section {
background-color: #404552;
border-top: 1px solid #353944;
}
.ui .text.grey a { .ui .text.grey a {
color: #dbdbdb !important; color: #dbdbdb !important;
} }

View File

@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/repofiles"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
) )
@ -57,17 +58,12 @@ func NewCommitStatus(ctx *context.APIContext, form api.CreateStatusOption) {
Description: form.Description, Description: form.Description,
Context: form.Context, Context: form.Context,
} }
if err := models.NewCommitStatus(ctx.Repo.Repository, ctx.User, sha, status); err != nil { if err := repofiles.CreateCommitStatus(ctx.Repo.Repository, ctx.User, sha, status); err != nil {
ctx.Error(500, "NewCommitStatus", err) ctx.Error(500, "CreateCommitStatus", err)
return return
} }
newStatus, err := models.GetCommitStatus(ctx.Repo.Repository, sha, status) ctx.JSON(201, status.APIFormat())
if err != nil {
ctx.Error(500, "GetCommitStatus", err)
return
}
ctx.JSON(201, newStatus.APIFormat())
} }
// GetCommitStatuses returns all statuses for any given commit hash // GetCommitStatuses returns all statuses for any given commit hash
@ -140,6 +136,7 @@ func getCommitStatuses(ctx *context.APIContext, sha string) {
statuses, err := models.GetCommitStatuses(repo, sha, page) statuses, err := models.GetCommitStatuses(repo, sha, page)
if err != nil { if err != nil {
ctx.Error(500, "GetCommitStatuses", fmt.Errorf("GetCommitStatuses[%s, %s, %d]: %v", repo.FullName(), sha, page, err)) ctx.Error(500, "GetCommitStatuses", fmt.Errorf("GetCommitStatuses[%s, %s, %d]: %v", repo.FullName(), sha, page, err))
return
} }
apiStatuses := make([]*api.Status, 0, len(statuses)) apiStatuses := make([]*api.Status, 0, len(statuses))

View File

@ -321,15 +321,37 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare
setMergeTarget(ctx, pull) setMergeTarget(ctx, pull)
var headGitRepo *git.Repository var headGitRepo *git.Repository
var headBranchExist bool
// HeadRepo may be missing
if pull.HeadRepo != nil { if pull.HeadRepo != nil {
headGitRepo, err = git.OpenRepository(pull.HeadRepo.RepoPath()) headGitRepo, err = git.OpenRepository(pull.HeadRepo.RepoPath())
if err != nil { if err != nil {
ctx.ServerError("OpenRepository", err) ctx.ServerError("OpenRepository", err)
return nil return nil
} }
headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
if headBranchExist {
sha, err := headGitRepo.GetBranchCommitID(pull.HeadBranch)
if err != nil {
ctx.ServerError("GetBranchCommitID", err)
return nil
}
commitStatuses, err := models.GetLatestCommitStatus(repo, sha, 0)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
}
if len(commitStatuses) > 0 {
ctx.Data["LatestCommitStatuses"] = commitStatuses
ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
}
}
} }
if pull.HeadRepo == nil || !headGitRepo.IsBranchExist(pull.HeadBranch) { if pull.HeadRepo == nil || !headBranchExist {
ctx.Data["IsPullRequestBroken"] = true ctx.Data["IsPullRequestBroken"] = true
ctx.Data["HeadTarget"] = "deleted" ctx.Data["HeadTarget"] = "deleted"
ctx.Data["NumCommits"] = 0 ctx.Data["NumCommits"] = 0

View File

@ -45,7 +45,8 @@
{{else if .Issue.PullRequest.CanAutoMerge}}green {{else if .Issue.PullRequest.CanAutoMerge}}green
{{else}}red{{end}}"><span class="mega-octicon octicon-git-merge"></span></a> {{else}}red{{end}}"><span class="mega-octicon octicon-git-merge"></span></a>
<div class="content"> <div class="content">
<div class="ui merge segment"> {{template "repo/pulls/status" .}}
<div class="ui attached merge-section segment">
{{if .Issue.PullRequest.HasMerged}} {{if .Issue.PullRequest.HasMerged}}
<div class="item text purple"> <div class="item text purple">
{{$.i18n.Tr "repo.pulls.has_merged"}} {{$.i18n.Tr "repo.pulls.has_merged"}}

View File

@ -0,0 +1,21 @@
{{if $.LatestCommitStatus}}
<div class="ui top attached header">
{{if eq .LatestCommitStatus.State "pending"}}
{{$.i18n.Tr "repo.pulls.status_checking"}}
{{else if eq .LatestCommitStatus.State "success"}}
{{$.i18n.Tr "repo.pulls.status_checks_success"}}
{{else if eq .LatestCommitStatus.State "error"}}
{{$.i18n.Tr "repo.pulls.status_checks_error"}}
{{else}}
{{$.i18n.Tr "repo.pulls.status_checking"}}
{{end}}
</div>
{{range $.LatestCommitStatuses}}
<div class="ui attached segment">
<span>{{template "repo/commit_status" .}}</span>
<span class="ui">{{.Context}} <span class="text grey">{{.Description}}</span></span>
<div class="ui right">{{if .TargetURL}}<a href="{{.TargetURL}}">Details</a>{{end}}</div>
</div>
{{end}}
{{end}}