cli/pkg/cmd/pr/checks/checks.go
Mislav Marohnić 111e8dbcf2 Pass web browser to each individual command
This removes sensitivity to the BROWSER environment variable in tests
and makes it easier to verify the URL that the browser was invoked with
without having to stub sub-processes.
2021-03-19 21:22:37 +01:00

248 lines
5.2 KiB
Go

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 browser interface {
Browse(string) error
}
type ChecksOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
Browser browser
BaseRepo func() (ghrepo.Interface, error)
Branch func() (string, error)
Remotes func() (context.Remotes, error)
WebMode bool
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,
Browser: f.Browser,
}
cmd := &cobra.Command{
Use: "checks [<number> | <url> | <branch>]",
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)
},
}
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the web browser to show details about checks")
return cmd
}
func checksRun(opts *ChecksOptions) error {
httpClient, err := opts.HttpClient()
if err != nil {
return err
}
apiClient := api.NewClientFromHTTP(httpClient)
pr, baseRepo, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
if err != nil {
return err
}
if len(pr.Commits.Nodes) == 0 {
return fmt.Errorf("no commit found on the pull request")
}
rollup := pr.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes
if len(rollup) == 0 {
return fmt.Errorf("no checks reported on the '%s' branch", pr.BaseRefName)
}
isTerminal := opts.IO.IsStdoutTTY()
if opts.WebMode {
openURL := ghrepo.GenerateRepoURL(baseRepo, "pull/%d/checks", pr.Number)
if isTerminal {
fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
}
return opts.Browser.Browse(openURL)
}
passing := 0
failing := 0
pending := 0
type output struct {
mark string
bucket string
name string
elapsed string
link string
markColor func(string) string
}
cs := opts.IO.ColorScheme()
outputs := []output{}
for _, c := range pr.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes {
mark := "✓"
bucket := "pass"
state := c.State
markColor := cs.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 = cs.Red
failing++
bucket = "fail"
case "EXPECTED", "REQUESTED", "QUEUED", "PENDING", "IN_PROGRESS", "STALE":
mark = "-"
markColor = cs.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
}
return n0 < n1
}
return (b0 == "fail") || (b0 == "pending" && b1 == "success")
})
tp := utils.NewTablePrinter(opts.IO)
for _, o := range outputs {
if isTerminal {
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)
} 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", cs.Bold(summary), tallies)
}
if isTerminal {
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
}