github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/pr/status/status.go (about) 1 package status 2 3 import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "regexp" 8 "strconv" 9 "strings" 10 11 "github.com/cli/cli/api" 12 "github.com/cli/cli/context" 13 "github.com/cli/cli/git" 14 "github.com/cli/cli/internal/config" 15 "github.com/cli/cli/internal/ghrepo" 16 "github.com/cli/cli/pkg/cmd/pr/shared" 17 "github.com/cli/cli/pkg/cmdutil" 18 "github.com/cli/cli/pkg/iostreams" 19 "github.com/cli/cli/pkg/text" 20 "github.com/spf13/cobra" 21 ) 22 23 type StatusOptions struct { 24 HttpClient func() (*http.Client, error) 25 Config func() (config.Config, error) 26 IO *iostreams.IOStreams 27 BaseRepo func() (ghrepo.Interface, error) 28 Remotes func() (context.Remotes, error) 29 Branch func() (string, error) 30 31 HasRepoOverride bool 32 Exporter cmdutil.Exporter 33 } 34 35 func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Command { 36 opts := &StatusOptions{ 37 IO: f.IOStreams, 38 HttpClient: f.HttpClient, 39 Config: f.Config, 40 Remotes: f.Remotes, 41 Branch: f.Branch, 42 } 43 44 cmd := &cobra.Command{ 45 Use: "status", 46 Short: "Show status of relevant pull requests", 47 Args: cmdutil.NoArgsQuoteReminder, 48 RunE: func(cmd *cobra.Command, args []string) error { 49 // support `-R, --repo` override 50 opts.BaseRepo = f.BaseRepo 51 opts.HasRepoOverride = cmd.Flags().Changed("repo") 52 53 if runF != nil { 54 return runF(opts) 55 } 56 return statusRun(opts) 57 }, 58 } 59 60 cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.PullRequestFields) 61 62 return cmd 63 } 64 65 func statusRun(opts *StatusOptions) error { 66 httpClient, err := opts.HttpClient() 67 if err != nil { 68 return err 69 } 70 apiClient := api.NewClientFromHTTP(httpClient) 71 72 baseRepo, err := opts.BaseRepo() 73 if err != nil { 74 return err 75 } 76 77 var currentBranch string 78 var currentPRNumber int 79 var currentPRHeadRef string 80 81 if !opts.HasRepoOverride { 82 currentBranch, err = opts.Branch() 83 if err != nil && !errors.Is(err, git.ErrNotOnAnyBranch) { 84 return fmt.Errorf("could not query for pull request for current branch: %w", err) 85 } 86 87 remotes, _ := opts.Remotes() 88 currentPRNumber, currentPRHeadRef, err = prSelectorForCurrentBranch(baseRepo, currentBranch, remotes) 89 if err != nil { 90 return fmt.Errorf("could not query for pull request for current branch: %w", err) 91 } 92 } 93 94 options := api.StatusOptions{ 95 Username: "@me", 96 CurrentPR: currentPRNumber, 97 HeadRef: currentPRHeadRef, 98 } 99 if opts.Exporter != nil { 100 options.Fields = opts.Exporter.Fields() 101 } 102 prPayload, err := api.PullRequestStatus(apiClient, baseRepo, options) 103 if err != nil { 104 return err 105 } 106 107 err = opts.IO.StartPager() 108 if err != nil { 109 fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err) 110 } 111 defer opts.IO.StopPager() 112 113 if opts.Exporter != nil { 114 data := map[string]interface{}{ 115 "currentBranch": nil, 116 "createdBy": prPayload.ViewerCreated.PullRequests, 117 "needsReview": prPayload.ReviewRequested.PullRequests, 118 } 119 if prPayload.CurrentPR != nil { 120 data["currentBranch"] = prPayload.CurrentPR 121 } 122 return opts.Exporter.Write(opts.IO, data) 123 } 124 125 out := opts.IO.Out 126 cs := opts.IO.ColorScheme() 127 128 fmt.Fprintln(out, "") 129 fmt.Fprintf(out, "Relevant pull requests in %s\n", ghrepo.FullName(baseRepo)) 130 fmt.Fprintln(out, "") 131 132 shared.PrintHeader(opts.IO, "Current branch") 133 currentPR := prPayload.CurrentPR 134 if currentPR != nil && currentPR.State != "OPEN" && prPayload.DefaultBranch == currentBranch { 135 currentPR = nil 136 } 137 if currentPR != nil { 138 printPrs(opts.IO, 1, *currentPR) 139 } else if currentPRHeadRef == "" { 140 shared.PrintMessage(opts.IO, " There is no current branch") 141 } else { 142 shared.PrintMessage(opts.IO, fmt.Sprintf(" There is no pull request associated with %s", cs.Cyan("["+currentPRHeadRef+"]"))) 143 } 144 fmt.Fprintln(out) 145 146 shared.PrintHeader(opts.IO, "Created by you") 147 if prPayload.ViewerCreated.TotalCount > 0 { 148 printPrs(opts.IO, prPayload.ViewerCreated.TotalCount, prPayload.ViewerCreated.PullRequests...) 149 } else { 150 shared.PrintMessage(opts.IO, " You have no open pull requests") 151 } 152 fmt.Fprintln(out) 153 154 shared.PrintHeader(opts.IO, "Requesting a code review from you") 155 if prPayload.ReviewRequested.TotalCount > 0 { 156 printPrs(opts.IO, prPayload.ReviewRequested.TotalCount, prPayload.ReviewRequested.PullRequests...) 157 } else { 158 shared.PrintMessage(opts.IO, " You have no pull requests to review") 159 } 160 fmt.Fprintln(out) 161 162 return nil 163 } 164 165 func prSelectorForCurrentBranch(baseRepo ghrepo.Interface, prHeadRef string, rem context.Remotes) (prNumber int, selector string, err error) { 166 selector = prHeadRef 167 branchConfig := git.ReadBranchConfig(prHeadRef) 168 169 // the branch is configured to merge a special PR head ref 170 prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`) 171 if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil { 172 prNumber, _ = strconv.Atoi(m[1]) 173 return 174 } 175 176 var branchOwner string 177 if branchConfig.RemoteURL != nil { 178 // the branch merges from a remote specified by URL 179 if r, err := ghrepo.FromURL(branchConfig.RemoteURL); err == nil { 180 branchOwner = r.RepoOwner() 181 } 182 } else if branchConfig.RemoteName != "" { 183 // the branch merges from a remote specified by name 184 if r, err := rem.FindByName(branchConfig.RemoteName); err == nil { 185 branchOwner = r.RepoOwner() 186 } 187 } 188 189 if branchOwner != "" { 190 if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") { 191 selector = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/") 192 } 193 // prepend `OWNER:` if this branch is pushed to a fork 194 if !strings.EqualFold(branchOwner, baseRepo.RepoOwner()) { 195 selector = fmt.Sprintf("%s:%s", branchOwner, prHeadRef) 196 } 197 } 198 199 return 200 } 201 202 func printPrs(io *iostreams.IOStreams, totalCount int, prs ...api.PullRequest) { 203 w := io.Out 204 cs := io.ColorScheme() 205 206 for _, pr := range prs { 207 prNumber := fmt.Sprintf("#%d", pr.Number) 208 209 prStateColorFunc := cs.ColorFromString(shared.ColorForPR(pr)) 210 211 fmt.Fprintf(w, " %s %s %s", prStateColorFunc(prNumber), text.Truncate(50, text.ReplaceExcessiveWhitespace(pr.Title)), cs.Cyan("["+pr.HeadLabel()+"]")) 212 213 checks := pr.ChecksStatus() 214 reviews := pr.ReviewStatus() 215 216 if pr.State == "OPEN" { 217 reviewStatus := reviews.ChangesRequested || reviews.Approved || reviews.ReviewRequired 218 if checks.Total > 0 || reviewStatus { 219 // show checks & reviews on their own line 220 fmt.Fprintf(w, "\n ") 221 } 222 223 if checks.Total > 0 { 224 var summary string 225 if checks.Failing > 0 { 226 if checks.Failing == checks.Total { 227 summary = cs.Red("× All checks failing") 228 } else { 229 summary = cs.Redf("× %d/%d checks failing", checks.Failing, checks.Total) 230 } 231 } else if checks.Pending > 0 { 232 summary = cs.Yellow("- Checks pending") 233 } else if checks.Passing == checks.Total { 234 summary = cs.Green("✓ Checks passing") 235 } 236 fmt.Fprint(w, summary) 237 } 238 239 if checks.Total > 0 && reviewStatus { 240 // add padding between checks & reviews 241 fmt.Fprint(w, " ") 242 } 243 244 if reviews.ChangesRequested { 245 fmt.Fprint(w, cs.Red("+ Changes requested")) 246 } else if reviews.ReviewRequired { 247 fmt.Fprint(w, cs.Yellow("- Review required")) 248 } else if reviews.Approved { 249 fmt.Fprint(w, cs.Green("✓ Approved")) 250 } 251 252 if pr.BaseRef.BranchProtectionRule.RequiresStrictStatusChecks { 253 switch pr.MergeStateStatus { 254 case "BEHIND": 255 fmt.Fprintf(w, " %s", cs.Yellow("- Not up to date")) 256 case "UNKNOWN", "DIRTY": 257 // do not print anything 258 default: 259 fmt.Fprintf(w, " %s", cs.Green("✓ Up to date")) 260 } 261 } 262 263 } else { 264 fmt.Fprintf(w, " - %s", shared.StateTitleWithColor(cs, pr)) 265 } 266 267 fmt.Fprint(w, "\n") 268 } 269 remaining := totalCount - len(prs) 270 if remaining > 0 { 271 fmt.Fprintf(w, cs.Gray(" And %d more\n"), remaining) 272 } 273 }