package commands import ( "fmt" "os" "sort" "github.com/github/hub/git" "github.com/github/hub/github" "github.com/github/hub/ui" "github.com/github/hub/utils" ) var cmdCiStatus = &Command{ Run: ciStatus, Usage: "ci-status [-v] []", Long: `Display status of GitHub checks for a commit. ## Options: -v, --verbose Print detailed report of all status checks and their URLs. -f, --format Pretty print all status checks using (implies '--verbose'). See the "PRETTY FORMATS" section of git-log(1) for some additional details on how placeholders are used in format. The available placeholders for issues are: %U: the URL of this status check %S: check state (e.g. "success", "failure") %sC: set color to red, green, or yellow, depending on state %t: name of the status check --color[=] Enable colored output even if stdout is not a terminal. can be one of "always" (default for '--color'), "never", or "auto" (default). A commit SHA or branch name (default: "HEAD"). Possible outputs and exit statuses: - success, neutral: 0 - failure, error, action_required, cancelled, timed_out: 1 - pending: 2 ## See also: hub-pull-request(1), hub(1) `, } var severityList []string func init() { CmdRunner.Use(cmdCiStatus) severityList = []string{ "neutral", "success", "pending", "cancelled", "timed_out", "action_required", "failure", "error", } } func checkSeverity(targetState string) int { for i, state := range severityList { if state == targetState { return i } } return -1 } func ciStatus(cmd *Command, args *Args) { ref := "HEAD" if !args.IsParamsEmpty() { ref = args.RemoveParam(0) } localRepo, err := github.LocalRepo() utils.Check(err) project, err := localRepo.MainProject() utils.Check(err) sha, err := git.Ref(ref) if err != nil { err = fmt.Errorf("Aborted: no revision could be determined from '%s'", ref) } utils.Check(err) if args.Noop { ui.Printf("Would request CI status for %s\n", sha) } else { gh := github.NewClient(project.Host) response, err := gh.FetchCIStatus(project, sha) utils.Check(err) state := "" if len(response.Statuses) > 0 { for _, status := range response.Statuses { if checkSeverity(status.State) > checkSeverity(state) { state = status.State } } } var exitCode int switch state { case "success", "neutral": exitCode = 0 case "failure", "error", "action_required", "cancelled", "timed_out": exitCode = 1 case "pending": exitCode = 2 default: exitCode = 3 } verbose := args.Flag.Bool("--verbose") || args.Flag.HasReceived("--format") if verbose && len(response.Statuses) > 0 { colorize := colorizeOutput(args.Flag.HasReceived("--color"), args.Flag.Value("--color")) ciVerboseFormat(response.Statuses, args.Flag.Value("--format"), colorize) } else { if state != "" { ui.Println(state) } else { ui.Println("no status") } } os.Exit(exitCode) } } func ciVerboseFormat(statuses []github.CIStatus, formatString string, colorize bool) { contextWidth := 0 for _, status := range statuses { if len(status.Context) > contextWidth { contextWidth = len(status.Context) } } sort.SliceStable(statuses, func(a, b int) bool { return stateRank(statuses[a].State) < stateRank(statuses[b].State) }) for _, status := range statuses { var color int var stateMarker string switch status.State { case "success": stateMarker = "✔︎" color = 32 case "failure", "error", "action_required", "cancelled", "timed_out": stateMarker = "✖︎" color = 31 case "neutral": stateMarker = "◦" color = 30 case "pending": stateMarker = "●" color = 33 } placeholders := map[string]string{ "S": status.State, "sC": "", "t": status.Context, "U": status.TargetUrl, } if colorize { placeholders["sC"] = fmt.Sprintf("\033[%dm", color) } format := formatString if format == "" { if status.TargetUrl == "" { format = fmt.Sprintf("%%sC%s%%Creset\t%%t\n", stateMarker) } else { format = fmt.Sprintf("%%sC%s%%Creset\t%%<(%d)%%t\t%%U\n", stateMarker, contextWidth) } } ui.Print(ui.Expand(format, placeholders, colorize)) } } func stateRank(state string) uint32 { switch state { case "failure", "error", "action_required", "cancelled", "timed_out": return 1 case "success", "neutral": return 3 default: return 2 } }