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  }