Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 62 additions & 6 deletions api/queries_pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"net/http"
"strings"
"time"

"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/internal/ghrepo"
Expand Down Expand Up @@ -72,12 +73,19 @@ type PullRequest struct {
TotalCount int
Nodes []struct {
Commit struct {
Oid string
StatusCheckRollup struct {
Contexts struct {
Nodes []struct {
State string
Status string
Conclusion string
Name string
Context string
State string
Status string
Conclusion string
StartedAt time.Time
CompletedAt time.Time
DetailsURL string
TargetURL string
}
}
}
Expand Down Expand Up @@ -272,11 +280,11 @@ func PullRequests(client *Client, repo ghrepo.Interface, currentPRNumber int, cu
contexts(last: 100) {
nodes {
...on StatusContext {
context
state
}
...on CheckRun {
status
conclusion
}
}
}
Expand Down Expand Up @@ -418,8 +426,32 @@ func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*Pu
author {
login
}
commits {
commits(last: 1) {
totalCount
nodes {
commit {
oid
statusCheckRollup {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My slight concern with expanding the PullRequestByNumber is that the Checks+Statuses data is unnecessarily fetched every time a user does any operation on a pull request, such as gh pr view 123 --web. However, I do understand that this approach is the most feasible to implement, since any other course of action would require us to either duplicate the logic of shared.PRFromArgs() to fetch extra fields, or to come up with a shippable solution to #1081. I'm only bringing this up as a note that this is a good candidate to revisit in a few weeks 👍

contexts(last: 100) {
nodes {
...on StatusContext {
context
state
targetUrl
}
...on CheckRun {
name
status
conclusion
startedAt
completedAt
detailsUrl
}
}
}
}
}
}
}
baseRefName
headRefName
Expand Down Expand Up @@ -524,8 +556,32 @@ func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, hea
author {
login
}
commits {
commits(last: 1) {
totalCount
nodes {
commit {
oid
statusCheckRollup {
contexts(last: 100) {
nodes {
...on StatusContext {
context
state
targetUrl
}
...on CheckRun {
name
status
conclusion
startedAt
completedAt
detailsUrl
}
}
}
}
}
}
}
url
baseRefName
Expand Down
227 changes: 227 additions & 0 deletions pkg/cmd/pr/checks/checks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
package checks

import (
"errors"
"fmt"
"net/http"
"sort"
"time"

"github.com/cli/cli/api"
"github.com/cli/cli/context"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/cmd/pr/shared"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
"github.com/cli/cli/utils"
"github.com/spf13/cobra"
)

type ChecksOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
Branch func() (string, error)
Remotes func() (context.Remotes, error)

SelectorArg string
}

func NewCmdChecks(f *cmdutil.Factory, runF func(*ChecksOptions) error) *cobra.Command {
opts := &ChecksOptions{
IO: f.IOStreams,
HttpClient: f.HttpClient,
Branch: f.Branch,
Remotes: f.Remotes,
BaseRepo: f.BaseRepo,
}

cmd := &cobra.Command{
Use: "checks",
Short: "Show CI status for a single pull request",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// support `-R, --repo` override
opts.BaseRepo = f.BaseRepo

if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
}

if len(args) > 0 {
opts.SelectorArg = args[0]
}

if runF != nil {
return runF(opts)
}

return checksRun(opts)
},
}

return cmd
}

func checksRun(opts *ChecksOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
apiClient := api.NewClientFromHTTP(httpClient)

pr, _, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
if err != nil {
return err
}

if len(pr.Commits.Nodes) == 0 {
return nil
}

rollup := pr.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes
if len(rollup) == 0 {
return nil
}

passing := 0
failing := 0
pending := 0

type output struct {
mark string
bucket string
name string
elapsed string
link string
markColor func(string) string
}

outputs := []output{}

for _, c := range pr.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes {
mark := "✓"
bucket := "pass"
state := c.State
markColor := utils.Green
if state == "" {
if c.Status == "COMPLETED" {
state = c.Conclusion
} else {
state = c.Status
}
}
switch state {
case "SUCCESS", "NEUTRAL", "SKIPPED":
passing++
case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
mark = "X"
markColor = utils.Red
failing++
bucket = "fail"
case "EXPECTED", "REQUESTED", "QUEUED", "PENDING", "IN_PROGRESS", "STALE":
mark = "-"
markColor = utils.Yellow
pending++
bucket = "pending"
default:
panic(fmt.Errorf("unsupported status: %q", state))
}

elapsed := ""
zeroTime := time.Time{}

if c.StartedAt != zeroTime && c.CompletedAt != zeroTime {
e := c.CompletedAt.Sub(c.StartedAt)
if e > 0 {
elapsed = e.String()
}
}

link := c.DetailsURL
if link == "" {
link = c.TargetURL
}

name := c.Name
if name == "" {
name = c.Context
}

outputs = append(outputs, output{mark, bucket, name, elapsed, link, markColor})
}

sort.Slice(outputs, func(i, j int) bool {
b0 := outputs[i].bucket
n0 := outputs[i].name
l0 := outputs[i].link
b1 := outputs[j].bucket
n1 := outputs[j].name
l1 := outputs[j].link

if b0 == b1 {
if n0 == n1 {
return l0 < l1
} else {
return n0 < n1
}
}

return (b0 == "fail") || (b0 == "pending" && b1 == "success")
})

tp := utils.NewTablePrinter(opts.IO)

for _, o := range outputs {
if opts.IO.IsStdoutTTY() {
tp.AddField(o.mark, nil, o.markColor)
tp.AddField(o.name, nil, nil)
tp.AddField(o.elapsed, nil, nil)
tp.AddField(o.link, nil, nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is tricky! We render the URL in this column, but the way I designed TablePrinter is to automatically truncate long columns in narrow terminals. That means that the URL column will be the first one put up on the chopping block, and URLs become completely useless after they are truncated.

A good followup candidate would be to add a feature to TablePrinter to mark some columns as exempt from elastic resizing/truncation.

} else {
tp.AddField(o.name, nil, nil)
tp.AddField(o.bucket, nil, nil)
if o.elapsed == "" {
tp.AddField("0", nil, nil)
} else {
tp.AddField(o.elapsed, nil, nil)
}
tp.AddField(o.link, nil, nil)
}

tp.EndRow()
}

summary := ""
if failing+passing+pending > 0 {
if failing > 0 {
summary = "Some checks were not successful"
} else if pending > 0 {
summary = "Some checks are still pending"
} else {
summary = "All checks were successful"
}

tallies := fmt.Sprintf(
"%d failing, %d successful, and %d pending checks",
failing, passing, pending)

summary = fmt.Sprintf("%s\n%s", utils.Bold(summary), tallies)
}

if opts.IO.IsStdoutTTY() {
fmt.Fprintln(opts.IO.Out, summary)
fmt.Fprintln(opts.IO.Out)
}

err = tp.Render()
if err != nil {
return err
}

if failing+pending > 0 {
return cmdutil.SilentError
}

return nil
}
Loading