Merge remote-tracking branch 'origin' into fix-milestone
This commit is contained in:
commit
20ffa49d69
107 changed files with 7113 additions and 4697 deletions
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -166,9 +167,9 @@ func aliasList(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
stdout := colorableOut(cmd)
|
||||
|
||||
tp := utils.NewTablePrinter(stdout)
|
||||
tp := utils.NewTablePrinter(&iostreams.IOStreams{
|
||||
Out: cmd.OutOrStdout(),
|
||||
})
|
||||
|
||||
aliasMap := aliasCfg.All()
|
||||
keys := []string{}
|
||||
|
|
@ -193,7 +194,7 @@ func aliasList(cmd *cobra.Command, args []string) error {
|
|||
|
||||
var aliasDeleteCmd = &cobra.Command{
|
||||
Use: "delete <alias>",
|
||||
Short: "Delete an alias.",
|
||||
Short: "Delete an alias",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: aliasDelete,
|
||||
}
|
||||
|
|
|
|||
226
command/issue.go
226
command/issue.go
|
|
@ -3,7 +3,6 @@ package command
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -12,8 +11,11 @@ import (
|
|||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/git"
|
||||
"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/githubtemplate"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/text"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
|
@ -121,57 +123,6 @@ var issueReopenCmd = &cobra.Command{
|
|||
RunE: issueReopen,
|
||||
}
|
||||
|
||||
type filterOptions struct {
|
||||
entity string
|
||||
state string
|
||||
assignee string
|
||||
labels []string
|
||||
author string
|
||||
baseBranch string
|
||||
mention string
|
||||
milestone string
|
||||
}
|
||||
|
||||
func listURLWithQuery(listURL string, options filterOptions) (string, error) {
|
||||
u, err := url.Parse(listURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
query := fmt.Sprintf("is:%s ", options.entity)
|
||||
if options.state != "all" {
|
||||
query += fmt.Sprintf("is:%s ", options.state)
|
||||
}
|
||||
if options.assignee != "" {
|
||||
query += fmt.Sprintf("assignee:%s ", options.assignee)
|
||||
}
|
||||
for _, label := range options.labels {
|
||||
query += fmt.Sprintf("label:%s ", quoteValueForQuery(label))
|
||||
}
|
||||
if options.author != "" {
|
||||
query += fmt.Sprintf("author:%s ", options.author)
|
||||
}
|
||||
if options.baseBranch != "" {
|
||||
query += fmt.Sprintf("base:%s ", options.baseBranch)
|
||||
}
|
||||
if options.mention != "" {
|
||||
query += fmt.Sprintf("mentions:%s ", options.mention)
|
||||
}
|
||||
if options.milestone != "" {
|
||||
query += fmt.Sprintf("milestone:%s ", quoteValueForQuery(options.milestone))
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("q", strings.TrimSuffix(query, " "))
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func quoteValueForQuery(v string) string {
|
||||
if strings.ContainsAny(v, " \"\t\r\n") {
|
||||
return fmt.Sprintf("%q", v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func issueList(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
|
|
@ -229,14 +180,14 @@ func issueList(cmd *cobra.Command, args []string) error {
|
|||
|
||||
if web {
|
||||
issueListURL := ghrepo.GenerateRepoURL(baseRepo, "issues")
|
||||
openURL, err := listURLWithQuery(issueListURL, filterOptions{
|
||||
entity: "issue",
|
||||
state: state,
|
||||
assignee: assignee,
|
||||
labels: labels,
|
||||
author: author,
|
||||
mention: mention,
|
||||
milestone: milestone,
|
||||
openURL, err := shared.ListURLWithQuery(issueListURL, shared.FilterOptions{
|
||||
Entity: "issue",
|
||||
State: state,
|
||||
Assignee: assignee,
|
||||
Labels: labels,
|
||||
Author: author,
|
||||
Mention: mention,
|
||||
Milestone: milestone,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -258,7 +209,7 @@ func issueList(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
})
|
||||
|
||||
title := listHeader(ghrepo.FullName(baseRepo), "issue", len(listResult.Issues), listResult.TotalCount, hasFilters)
|
||||
title := shared.ListHeader(ghrepo.FullName(baseRepo), "issue", len(listResult.Issues), listResult.TotalCount, hasFilters)
|
||||
if connectedToTerminal(cmd) {
|
||||
fmt.Fprintf(colorableErr(cmd), "\n%s\n\n", title)
|
||||
}
|
||||
|
|
@ -282,7 +233,7 @@ func issueStatus(cmd *cobra.Command, args []string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
currentUser, err := api.CurrentLoginName(apiClient)
|
||||
currentUser, err := api.CurrentLoginName(apiClient, baseRepo.RepoHost())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -298,28 +249,28 @@ func issueStatus(cmd *cobra.Command, args []string) error {
|
|||
fmt.Fprintf(out, "Relevant issues in %s\n", ghrepo.FullName(baseRepo))
|
||||
fmt.Fprintln(out, "")
|
||||
|
||||
printHeader(out, "Issues assigned to you")
|
||||
shared.PrintHeader(out, "Issues assigned to you")
|
||||
if issuePayload.Assigned.TotalCount > 0 {
|
||||
printIssues(out, " ", issuePayload.Assigned.TotalCount, issuePayload.Assigned.Issues)
|
||||
} else {
|
||||
message := " There are no issues assigned to you"
|
||||
printMessage(out, message)
|
||||
shared.PrintMessage(out, message)
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
|
||||
printHeader(out, "Issues mentioning you")
|
||||
shared.PrintHeader(out, "Issues mentioning you")
|
||||
if issuePayload.Mentioned.TotalCount > 0 {
|
||||
printIssues(out, " ", issuePayload.Mentioned.TotalCount, issuePayload.Mentioned.Issues)
|
||||
} else {
|
||||
printMessage(out, " There are no issues mentioning you")
|
||||
shared.PrintMessage(out, " There are no issues mentioning you")
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
|
||||
printHeader(out, "Issues opened by you")
|
||||
shared.PrintHeader(out, "Issues opened by you")
|
||||
if issuePayload.Authored.TotalCount > 0 {
|
||||
printIssues(out, " ", issuePayload.Authored.TotalCount, issuePayload.Authored.Issues)
|
||||
} else {
|
||||
printMessage(out, " There are no issues opened by you")
|
||||
shared.PrintMessage(out, " There are no issues opened by you")
|
||||
}
|
||||
fmt.Fprintln(out)
|
||||
|
||||
|
|
@ -357,29 +308,10 @@ func issueView(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
func issueStateTitleWithColor(state string) string {
|
||||
colorFunc := colorFuncForState(state)
|
||||
colorFunc := shared.ColorFuncForState(state)
|
||||
return colorFunc(strings.Title(strings.ToLower(state)))
|
||||
}
|
||||
|
||||
func listHeader(repoName string, itemName string, matchCount int, totalMatchCount int, hasFilters bool) string {
|
||||
if totalMatchCount == 0 {
|
||||
if hasFilters {
|
||||
return fmt.Sprintf("No %ss match your search in %s", itemName, repoName)
|
||||
}
|
||||
return fmt.Sprintf("There are no open %ss in %s", itemName, repoName)
|
||||
}
|
||||
|
||||
if hasFilters {
|
||||
matchVerb := "match"
|
||||
if totalMatchCount == 1 {
|
||||
matchVerb = "matches"
|
||||
}
|
||||
return fmt.Sprintf("Showing %d of %s in %s that %s your search", matchCount, utils.Pluralize(totalMatchCount, itemName), repoName, matchVerb)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Showing %d of %s in %s", matchCount, utils.Pluralize(totalMatchCount, itemName), repoName)
|
||||
}
|
||||
|
||||
func printRawIssuePreview(out io.Writer, issue *api.Issue) error {
|
||||
assignees := issueAssigneeList(*issue)
|
||||
labels := issueLabelList(*issue)
|
||||
|
|
@ -507,11 +439,7 @@ func issueCreate(cmd *cobra.Command, args []string) error {
|
|||
if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb {
|
||||
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
|
||||
if title != "" || body != "" {
|
||||
milestone := ""
|
||||
if len(milestoneTitles) > 0 {
|
||||
milestone = milestoneTitles[0]
|
||||
}
|
||||
openURL, err = withPrAndIssueQueryParams(openURL, title, body, assignees, labelNames, projectNames, milestone)
|
||||
openURL, err = shared.WithPrAndIssueQueryParams(openURL, title, body, assignees, labelNames, projectNames, milestoneTitles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -534,9 +462,9 @@ func issueCreate(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(baseRepo))
|
||||
}
|
||||
|
||||
action := SubmitAction
|
||||
tb := issueMetadataState{
|
||||
Type: issueMetadata,
|
||||
action := shared.SubmitAction
|
||||
tb := shared.IssueMetadataState{
|
||||
Type: shared.IssueMetadata,
|
||||
Assignees: assignees,
|
||||
Labels: labelNames,
|
||||
Projects: projectNames,
|
||||
|
|
@ -557,14 +485,20 @@ func issueCreate(cmd *cobra.Command, args []string) error {
|
|||
legacyTemplateFile = githubtemplate.FindLegacy(rootDir, "ISSUE_TEMPLATE")
|
||||
}
|
||||
}
|
||||
err := titleBodySurvey(cmd, &tb, apiClient, baseRepo, title, body, defaults{}, nonLegacyTemplateFiles, legacyTemplateFile, false, repo.ViewerCanTriage())
|
||||
|
||||
editorCommand, err := cmdutil.DetermineEditor(ctx.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = shared.TitleBodySurvey(defaultStreams, editorCommand, &tb, apiClient, baseRepo, title, body, shared.Defaults{}, nonLegacyTemplateFiles, legacyTemplateFile, false, repo.ViewerCanTriage())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not collect title and/or body: %w", err)
|
||||
}
|
||||
|
||||
action = tb.Action
|
||||
|
||||
if tb.Action == CancelAction {
|
||||
if tb.Action == shared.CancelAction {
|
||||
fmt.Fprintln(cmd.ErrOrStderr(), "Discarding.")
|
||||
|
||||
return nil
|
||||
|
|
@ -582,26 +516,22 @@ func issueCreate(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
if action == PreviewAction {
|
||||
if action == shared.PreviewAction {
|
||||
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
|
||||
milestone := ""
|
||||
if len(milestoneTitles) > 0 {
|
||||
milestone = milestoneTitles[0]
|
||||
}
|
||||
openURL, err = withPrAndIssueQueryParams(openURL, title, body, assignees, labelNames, projectNames, milestone)
|
||||
openURL, err = shared.WithPrAndIssueQueryParams(openURL, title, body, assignees, labelNames, projectNames, milestoneTitles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO could exceed max url length for explorer
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
||||
return utils.OpenInBrowser(openURL)
|
||||
} else if action == SubmitAction {
|
||||
} else if action == shared.SubmitAction {
|
||||
params := map[string]interface{}{
|
||||
"title": title,
|
||||
"body": body,
|
||||
}
|
||||
|
||||
err = addMetadataToIssueParams(apiClient, baseRepo, params, &tb)
|
||||
err = shared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -619,84 +549,10 @@ func issueCreate(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func addMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *issueMetadataState) error {
|
||||
if !tb.HasMetadata() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tb.MetadataResult == nil {
|
||||
resolveInput := api.RepoResolveInput{
|
||||
Reviewers: tb.Reviewers,
|
||||
Assignees: tb.Assignees,
|
||||
Labels: tb.Labels,
|
||||
Projects: tb.Projects,
|
||||
Milestones: tb.Milestones,
|
||||
}
|
||||
|
||||
var err error
|
||||
tb.MetadataResult, err = api.RepoResolveMetadataIDs(client, baseRepo, resolveInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
assigneeIDs, err := tb.MetadataResult.MembersToIDs(tb.Assignees)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not assign user: %w", err)
|
||||
}
|
||||
params["assigneeIds"] = assigneeIDs
|
||||
|
||||
labelIDs, err := tb.MetadataResult.LabelsToIDs(tb.Labels)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add label: %w", err)
|
||||
}
|
||||
params["labelIds"] = labelIDs
|
||||
|
||||
projectIDs, err := tb.MetadataResult.ProjectsToIDs(tb.Projects)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add to project: %w", err)
|
||||
}
|
||||
params["projectIds"] = projectIDs
|
||||
|
||||
if len(tb.Milestones) > 0 {
|
||||
milestoneID, err := tb.MetadataResult.MilestoneToID(tb.Milestones[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add to milestone '%s': %w", tb.Milestones[0], err)
|
||||
}
|
||||
params["milestoneId"] = milestoneID
|
||||
}
|
||||
|
||||
if len(tb.Reviewers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var userReviewers []string
|
||||
var teamReviewers []string
|
||||
for _, r := range tb.Reviewers {
|
||||
if strings.ContainsRune(r, '/') {
|
||||
teamReviewers = append(teamReviewers, r)
|
||||
} else {
|
||||
userReviewers = append(userReviewers, r)
|
||||
}
|
||||
}
|
||||
|
||||
userReviewerIDs, err := tb.MetadataResult.MembersToIDs(userReviewers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not request reviewer: %w", err)
|
||||
}
|
||||
params["userReviewerIds"] = userReviewerIDs
|
||||
|
||||
teamReviewerIDs, err := tb.MetadataResult.TeamsToIDs(teamReviewers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not request reviewer: %w", err)
|
||||
}
|
||||
params["teamReviewerIds"] = teamReviewerIDs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printIssues(w io.Writer, prefix string, totalCount int, issues []api.Issue) {
|
||||
table := utils.NewTablePrinter(w)
|
||||
io := &iostreams.IOStreams{Out: w}
|
||||
io.SetStdoutTTY(utils.IsTerminal(w))
|
||||
table := utils.NewTablePrinter(io)
|
||||
for _, issue := range issues {
|
||||
issueNum := strconv.Itoa(issue.Number)
|
||||
if table.IsTTY() {
|
||||
|
|
@ -709,11 +565,11 @@ func printIssues(w io.Writer, prefix string, totalCount int, issues []api.Issue)
|
|||
}
|
||||
now := time.Now()
|
||||
ago := now.Sub(issue.UpdatedAt)
|
||||
table.AddField(issueNum, nil, colorFuncForState(issue.State))
|
||||
table.AddField(issueNum, nil, shared.ColorFuncForState(issue.State))
|
||||
if !table.IsTTY() {
|
||||
table.AddField(issue.State, nil, nil)
|
||||
}
|
||||
table.AddField(replaceExcessiveWhitespace(issue.Title), nil, nil)
|
||||
table.AddField(text.ReplaceExcessiveWhitespace(issue.Title), nil, nil)
|
||||
table.AddField(labels, nil, utils.Gray)
|
||||
if table.IsTTY() {
|
||||
table.AddField(utils.FuzzyAgo(ago), nil, utils.Gray)
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ func TestIssueList_tty(t *testing.T) {
|
|||
}
|
||||
|
||||
eq(t, output.Stderr(), `
|
||||
Showing 3 of 3 issues in OWNER/REPO
|
||||
Showing 3 of 3 open issues in OWNER/REPO
|
||||
|
||||
`)
|
||||
|
||||
|
|
@ -834,117 +834,6 @@ func TestIssueCreate_webTitleBody(t *testing.T) {
|
|||
eq(t, output.String(), "Opening github.com/OWNER/REPO/issues/new in your browser.\n")
|
||||
}
|
||||
|
||||
func Test_listHeader(t *testing.T) {
|
||||
type args struct {
|
||||
repoName string
|
||||
itemName string
|
||||
matchCount int
|
||||
totalMatchCount int
|
||||
hasFilters bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no results",
|
||||
args: args{
|
||||
repoName: "REPO",
|
||||
itemName: "table",
|
||||
matchCount: 0,
|
||||
totalMatchCount: 0,
|
||||
hasFilters: false,
|
||||
},
|
||||
want: "There are no open tables in REPO",
|
||||
},
|
||||
{
|
||||
name: "no matches after filters",
|
||||
args: args{
|
||||
repoName: "REPO",
|
||||
itemName: "Luftballon",
|
||||
matchCount: 0,
|
||||
totalMatchCount: 0,
|
||||
hasFilters: true,
|
||||
},
|
||||
want: "No Luftballons match your search in REPO",
|
||||
},
|
||||
{
|
||||
name: "one result",
|
||||
args: args{
|
||||
repoName: "REPO",
|
||||
itemName: "genie",
|
||||
matchCount: 1,
|
||||
totalMatchCount: 23,
|
||||
hasFilters: false,
|
||||
},
|
||||
want: "Showing 1 of 23 genies in REPO",
|
||||
},
|
||||
{
|
||||
name: "one result after filters",
|
||||
args: args{
|
||||
repoName: "REPO",
|
||||
itemName: "tiny cup",
|
||||
matchCount: 1,
|
||||
totalMatchCount: 23,
|
||||
hasFilters: true,
|
||||
},
|
||||
want: "Showing 1 of 23 tiny cups in REPO that match your search",
|
||||
},
|
||||
{
|
||||
name: "one result in total",
|
||||
args: args{
|
||||
repoName: "REPO",
|
||||
itemName: "chip",
|
||||
matchCount: 1,
|
||||
totalMatchCount: 1,
|
||||
hasFilters: false,
|
||||
},
|
||||
want: "Showing 1 of 1 chip in REPO",
|
||||
},
|
||||
{
|
||||
name: "one result in total after filters",
|
||||
args: args{
|
||||
repoName: "REPO",
|
||||
itemName: "spicy noodle",
|
||||
matchCount: 1,
|
||||
totalMatchCount: 1,
|
||||
hasFilters: true,
|
||||
},
|
||||
want: "Showing 1 of 1 spicy noodle in REPO that matches your search",
|
||||
},
|
||||
{
|
||||
name: "multiple results",
|
||||
args: args{
|
||||
repoName: "REPO",
|
||||
itemName: "plant",
|
||||
matchCount: 4,
|
||||
totalMatchCount: 23,
|
||||
hasFilters: false,
|
||||
},
|
||||
want: "Showing 4 of 23 plants in REPO",
|
||||
},
|
||||
{
|
||||
name: "multiple results after filters",
|
||||
args: args{
|
||||
repoName: "REPO",
|
||||
itemName: "boomerang",
|
||||
matchCount: 4,
|
||||
totalMatchCount: 23,
|
||||
hasFilters: true,
|
||||
},
|
||||
want: "Showing 4 of 23 boomerangs in REPO that match your search",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := listHeader(tt.args.repoName, tt.args.itemName, tt.args.matchCount, tt.args.totalMatchCount, tt.args.hasFilters); got != tt.want {
|
||||
t.Errorf("listHeader() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueStateTitleWithColor(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
state string
|
||||
|
|
@ -1102,71 +991,3 @@ func TestIssueReopen_issuesDisabled(t *testing.T) {
|
|||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_listURLWithQuery(t *testing.T) {
|
||||
type args struct {
|
||||
listURL string
|
||||
options filterOptions
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "blank",
|
||||
args: args{
|
||||
listURL: "https://example.com/path?a=b",
|
||||
options: filterOptions{
|
||||
entity: "issue",
|
||||
state: "open",
|
||||
},
|
||||
},
|
||||
want: "https://example.com/path?a=b&q=is%3Aissue+is%3Aopen",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "all",
|
||||
args: args{
|
||||
listURL: "https://example.com/path",
|
||||
options: filterOptions{
|
||||
entity: "issue",
|
||||
state: "open",
|
||||
assignee: "bo",
|
||||
author: "ka",
|
||||
baseBranch: "trunk",
|
||||
mention: "nu",
|
||||
},
|
||||
},
|
||||
want: "https://example.com/path?q=is%3Aissue+is%3Aopen+assignee%3Abo+author%3Aka+base%3Atrunk+mentions%3Anu",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "spaces in values",
|
||||
args: args{
|
||||
listURL: "https://example.com/path",
|
||||
options: filterOptions{
|
||||
entity: "pr",
|
||||
state: "open",
|
||||
labels: []string{"docs", "help wanted"},
|
||||
milestone: `Codename "What Was Missing"`,
|
||||
},
|
||||
},
|
||||
want: "https://example.com/path?q=is%3Apr+is%3Aopen+label%3Adocs+label%3A%22help+wanted%22+milestone%3A%22Codename+%5C%22What+Was+Missing%5C%22%22",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := listURLWithQuery(tt.args.listURL, tt.args.options)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("listURLWithQuery() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("listURLWithQuery() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1047
command/pr.go
1047
command/pr.go
File diff suppressed because it is too large
Load diff
|
|
@ -1,129 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
)
|
||||
|
||||
func prCheckout(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
currentBranch, _ := ctx.Branch()
|
||||
remotes, err := ctx.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr, baseRepo, err := prFromArgs(ctx, apiClient, cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseRemote, _ := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName())
|
||||
// baseRemoteSpec is a repository URL or a remote name to be used in git fetch
|
||||
baseURLOrName := formatRemoteURL(cmd, baseRepo)
|
||||
if baseRemote != nil {
|
||||
baseURLOrName = baseRemote.Name
|
||||
}
|
||||
|
||||
headRemote := baseRemote
|
||||
if pr.IsCrossRepository {
|
||||
headRemote, _ = remotes.FindByRepo(pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name)
|
||||
}
|
||||
|
||||
var cmdQueue [][]string
|
||||
newBranchName := pr.HeadRefName
|
||||
if strings.HasPrefix(newBranchName, "-") {
|
||||
return fmt.Errorf("invalid branch name: %q", newBranchName)
|
||||
}
|
||||
|
||||
if headRemote != nil {
|
||||
// there is an existing git remote for PR head
|
||||
remoteBranch := fmt.Sprintf("%s/%s", headRemote.Name, pr.HeadRefName)
|
||||
refSpec := fmt.Sprintf("+refs/heads/%s:refs/remotes/%s", pr.HeadRefName, remoteBranch)
|
||||
|
||||
cmdQueue = append(cmdQueue, []string{"git", "fetch", headRemote.Name, refSpec})
|
||||
|
||||
// local branch already exists
|
||||
if _, err := git.ShowRefs("refs/heads/" + newBranchName); err == nil {
|
||||
cmdQueue = append(cmdQueue, []string{"git", "checkout", newBranchName})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "merge", "--ff-only", fmt.Sprintf("refs/remotes/%s", remoteBranch)})
|
||||
} else {
|
||||
cmdQueue = append(cmdQueue, []string{"git", "checkout", "-b", newBranchName, "--no-track", remoteBranch})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), headRemote.Name})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), "refs/heads/" + pr.HeadRefName})
|
||||
}
|
||||
} else {
|
||||
// no git remote for PR head
|
||||
|
||||
defaultBranchName, err := api.RepoDefaultBranch(apiClient, baseRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// avoid naming the new branch the same as the default branch
|
||||
if newBranchName == defaultBranchName {
|
||||
newBranchName = fmt.Sprintf("%s/%s", pr.HeadRepositoryOwner.Login, newBranchName)
|
||||
}
|
||||
|
||||
ref := fmt.Sprintf("refs/pull/%d/head", pr.Number)
|
||||
if newBranchName == currentBranch {
|
||||
// PR head matches currently checked out branch
|
||||
cmdQueue = append(cmdQueue, []string{"git", "fetch", baseURLOrName, ref})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "merge", "--ff-only", "FETCH_HEAD"})
|
||||
} else {
|
||||
// create a new branch
|
||||
cmdQueue = append(cmdQueue, []string{"git", "fetch", baseURLOrName, fmt.Sprintf("%s:%s", ref, newBranchName)})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "checkout", newBranchName})
|
||||
}
|
||||
|
||||
remote := baseURLOrName
|
||||
mergeRef := ref
|
||||
if pr.MaintainerCanModify {
|
||||
headRepo := ghrepo.NewWithHost(pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name, baseRepo.RepoHost())
|
||||
remote = formatRemoteURL(cmd, headRepo)
|
||||
mergeRef = fmt.Sprintf("refs/heads/%s", pr.HeadRefName)
|
||||
}
|
||||
if mc, err := git.Config(fmt.Sprintf("branch.%s.merge", newBranchName)); err != nil || mc == "" {
|
||||
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), remote})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), mergeRef})
|
||||
}
|
||||
}
|
||||
|
||||
for _, args := range cmdQueue {
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := run.PrepareCmd(cmd).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var prCheckoutCmd = &cobra.Command{
|
||||
Use: "checkout {<number> | <url> | <branch>}",
|
||||
Short: "Check out a pull request in Git",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("requires a pull request number as an argument")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: prCheckout,
|
||||
}
|
||||
|
|
@ -1,567 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/internal/run"
|
||||
"github.com/cli/cli/pkg/httpmock"
|
||||
"github.com/cli/cli/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPRCheckout_sameRepo(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
return &errorStub{"exit status: 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := RunCommand(`pr checkout 123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin +refs/heads/feature:refs/remotes/origin/feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout -b feature --no-track origin/feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git config branch.feature.remote origin")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.merge refs/heads/feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_urlArg(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
return &errorStub{"exit status: 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := RunCommand(`pr checkout https://github.com/OWNER/REPO/pull/123/files`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout -b feature --no-track origin/feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_urlArg_differentBase(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "POE"
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
http.Register(httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": {
|
||||
"defaultBranchRef": {"name": "master"}
|
||||
} } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
return &errorStub{"exit status: 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := RunCommand(`pr checkout https://github.com/OTHER/POE/pull/123/files`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Owner string
|
||||
Repo string
|
||||
}
|
||||
}{}
|
||||
_ = json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.Owner, "OTHER")
|
||||
eq(t, reqBody.Variables.Repo, "POE")
|
||||
|
||||
eq(t, len(ranCommands), 5)
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git fetch https://github.com/OTHER/POE.git refs/pull/123/head:feature")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.remote https://github.com/OTHER/POE.git")
|
||||
}
|
||||
|
||||
func TestPRCheckout_branchArg(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestForBranch\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false }
|
||||
] } } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
return &errorStub{"exit status: 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := RunCommand(`pr checkout hubot:feature`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, len(ranCommands), 5)
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git fetch origin refs/pull/123/head:feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_existingBranch(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
return &test.OutputStub{}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := RunCommand(`pr checkout 123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, len(ranCommands), 3)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin +refs/heads/feature:refs/remotes/origin/feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git merge --ff-only refs/remotes/origin/feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
"robot-fork": "hubot/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify -- refs/heads/feature":
|
||||
return &errorStub{"exit status: 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := RunCommand(`pr checkout 123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch robot-fork +refs/heads/feature:refs/remotes/robot-fork/feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout -b feature --no-track robot-fork/feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git config branch.feature.remote robot-fork")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.merge refs/heads/feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
return &errorStub{"exit status 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := RunCommand(`pr checkout 123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin refs/pull/123/head:feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git config branch.feature.remote origin")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.merge refs/pull/123/head")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
return &test.OutputStub{Out: []byte("refs/heads/feature\n")}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := RunCommand(`pr checkout 123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, len(ranCommands), 2)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin refs/pull/123/head:feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("feature")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
return &test.OutputStub{Out: []byte("refs/heads/feature\n")}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := RunCommand(`pr checkout 123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, len(ranCommands), 2)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin refs/pull/123/head")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git merge --ff-only FETCH_HEAD")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_invalidBranchName(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("feature")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "-foo",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
t.Errorf("unexpected external invocation: %v", cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := RunCommand(`pr checkout 123`)
|
||||
if assert.Errorf(t, err, "expected command to fail") {
|
||||
assert.Equal(t, `invalid branch name: "-foo"`, err.Error())
|
||||
}
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
}
|
||||
|
||||
func TestPRCheckout_maintainerCanModify(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
defer http.Verify(t)
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
|
||||
http.Register(httpmock.GraphQL(`query PullRequestByNumber\b`), httpmock.StringResponse(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO"
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": true
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
return &errorStub{"exit status 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &test.OutputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
output, err := RunCommand(`pr checkout 123`)
|
||||
eq(t, err, nil)
|
||||
eq(t, output.String(), "")
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin refs/pull/123/head:feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git config branch.feature.remote https://github.com/hubot/REPO.git")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.merge refs/heads/feature")
|
||||
}
|
||||
|
|
@ -1,490 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/githubtemplate"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type defaults struct {
|
||||
Title string
|
||||
Body string
|
||||
}
|
||||
|
||||
func computeDefaults(baseRef, headRef string) (defaults, error) {
|
||||
commits, err := git.Commits(baseRef, headRef)
|
||||
if err != nil {
|
||||
return defaults{}, err
|
||||
}
|
||||
|
||||
out := defaults{}
|
||||
|
||||
if len(commits) == 1 {
|
||||
out.Title = commits[0].Title
|
||||
body, err := git.CommitBody(commits[0].Sha)
|
||||
if err != nil {
|
||||
return defaults{}, err
|
||||
}
|
||||
out.Body = body
|
||||
} else {
|
||||
out.Title = utils.Humanize(headRef)
|
||||
|
||||
body := ""
|
||||
for i := len(commits) - 1; i >= 0; i-- {
|
||||
body += fmt.Sprintf("- %s\n", commits[i].Title)
|
||||
}
|
||||
out.Body = body
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func prCreate(cmd *cobra.Command, _ []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
remotes, err := ctx.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not initialize API client: %w", err)
|
||||
}
|
||||
|
||||
baseRepoOverride, _ := cmd.Flags().GetString("repo")
|
||||
repoContext, err := context.ResolveRemotesToRepos(remotes, client, baseRepoOverride)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseRepo, err := repoContext.BaseRepo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not determine base repository: %w", err)
|
||||
}
|
||||
|
||||
headBranch, err := ctx.Branch()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not determine the current branch: %w", err)
|
||||
}
|
||||
|
||||
var headRepo ghrepo.Interface
|
||||
var headRemote *context.Remote
|
||||
|
||||
// determine whether the head branch is already pushed to a remote
|
||||
headBranchPushedTo := determineTrackingBranch(remotes, headBranch)
|
||||
if headBranchPushedTo != nil {
|
||||
for _, r := range remotes {
|
||||
if r.Name != headBranchPushedTo.RemoteName {
|
||||
continue
|
||||
}
|
||||
headRepo = r
|
||||
headRemote = r
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, determine the head repository with info obtained from the API
|
||||
if headRepo == nil {
|
||||
if r, err := repoContext.HeadRepo(); err == nil {
|
||||
headRepo = r
|
||||
}
|
||||
}
|
||||
|
||||
baseBranch, err := cmd.Flags().GetString("base")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if baseBranch == "" {
|
||||
baseBranch = baseRepo.DefaultBranchRef.Name
|
||||
}
|
||||
if headBranch == baseBranch && headRepo != nil && ghrepo.IsSame(baseRepo, headRepo) {
|
||||
return fmt.Errorf("must be on a branch named differently than %q", baseBranch)
|
||||
}
|
||||
|
||||
if ucc, err := git.UncommittedChangeCount(); err == nil && ucc > 0 {
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: %s\n", utils.Pluralize(ucc, "uncommitted change"))
|
||||
}
|
||||
|
||||
title, err := cmd.Flags().GetString("title")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse title: %w", err)
|
||||
}
|
||||
body, err := cmd.Flags().GetString("body")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse body: %w", err)
|
||||
}
|
||||
|
||||
reviewers, err := cmd.Flags().GetStringSlice("reviewer")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse reviewers: %w", err)
|
||||
}
|
||||
assignees, err := cmd.Flags().GetStringSlice("assignee")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse assignees: %w", err)
|
||||
}
|
||||
labelNames, err := cmd.Flags().GetStringSlice("label")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse labels: %w", err)
|
||||
}
|
||||
projectNames, err := cmd.Flags().GetStringSlice("project")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse projects: %w", err)
|
||||
}
|
||||
var milestoneTitles []string
|
||||
if milestoneTitle, err := cmd.Flags().GetString("milestone"); err != nil {
|
||||
return fmt.Errorf("could not parse milestone: %w", err)
|
||||
} else if milestoneTitle != "" {
|
||||
milestoneTitles = append(milestoneTitles, milestoneTitle)
|
||||
}
|
||||
|
||||
baseTrackingBranch := baseBranch
|
||||
if baseRemote, err := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName()); err == nil {
|
||||
baseTrackingBranch = fmt.Sprintf("%s/%s", baseRemote.Name, baseBranch)
|
||||
}
|
||||
defs, defaultsErr := computeDefaults(baseTrackingBranch, headBranch)
|
||||
|
||||
isWeb, err := cmd.Flags().GetBool("web")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse web: %q", err)
|
||||
}
|
||||
|
||||
autofill, err := cmd.Flags().GetBool("fill")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse fill: %q", err)
|
||||
}
|
||||
|
||||
action := SubmitAction
|
||||
if isWeb {
|
||||
action = PreviewAction
|
||||
if (title == "" || body == "") && defaultsErr != nil {
|
||||
return fmt.Errorf("could not compute title or body defaults: %w", defaultsErr)
|
||||
}
|
||||
} else if autofill {
|
||||
if defaultsErr != nil {
|
||||
return fmt.Errorf("could not compute title or body defaults: %w", defaultsErr)
|
||||
}
|
||||
title = defs.Title
|
||||
body = defs.Body
|
||||
}
|
||||
|
||||
if !isWeb {
|
||||
headBranchLabel := headBranch
|
||||
if headRepo != nil && !ghrepo.IsSame(baseRepo, headRepo) {
|
||||
headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), headBranch)
|
||||
}
|
||||
existingPR, err := api.PullRequestForBranch(client, baseRepo, baseBranch, headBranchLabel)
|
||||
var notFound *api.NotFoundError
|
||||
if err != nil && !errors.As(err, ¬Found) {
|
||||
return fmt.Errorf("error checking for existing pull request: %w", err)
|
||||
}
|
||||
if err == nil {
|
||||
return fmt.Errorf("a pull request for branch %q into branch %q already exists:\n%s", headBranchLabel, baseBranch, existingPR.URL)
|
||||
}
|
||||
}
|
||||
|
||||
isDraft, err := cmd.Flags().GetBool("draft")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse draft: %w", err)
|
||||
}
|
||||
|
||||
if !isWeb && !autofill {
|
||||
message := "\nCreating pull request for %s into %s in %s\n\n"
|
||||
if isDraft {
|
||||
message = "\nCreating draft pull request for %s into %s in %s\n\n"
|
||||
}
|
||||
|
||||
if connectedToTerminal(cmd) {
|
||||
fmt.Fprintf(colorableErr(cmd), message,
|
||||
utils.Cyan(headBranch),
|
||||
utils.Cyan(baseBranch),
|
||||
ghrepo.FullName(baseRepo))
|
||||
if (title == "" || body == "") && defaultsErr != nil {
|
||||
fmt.Fprintf(colorableErr(cmd), "%s warning: could not compute title or body defaults: %s\n", utils.Yellow("!"), defaultsErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tb := issueMetadataState{
|
||||
Type: prMetadata,
|
||||
Reviewers: reviewers,
|
||||
Assignees: assignees,
|
||||
Labels: labelNames,
|
||||
Projects: projectNames,
|
||||
Milestones: milestoneTitles,
|
||||
}
|
||||
|
||||
if !connectedToTerminal(cmd) {
|
||||
if !isWeb && (!cmd.Flags().Changed("title") && !autofill) {
|
||||
return errors.New("--title or --fill required when not attached to a tty")
|
||||
}
|
||||
}
|
||||
|
||||
interactive := connectedToTerminal(cmd) && !(cmd.Flags().Changed("title") && cmd.Flags().Changed("body"))
|
||||
|
||||
if !isWeb && !autofill && interactive {
|
||||
var nonLegacyTemplateFiles []string
|
||||
var legacyTemplateFile *string
|
||||
if rootDir, err := git.ToplevelDir(); err == nil {
|
||||
// TODO: figure out how to stub this in tests
|
||||
nonLegacyTemplateFiles = githubtemplate.FindNonLegacy(rootDir, "PULL_REQUEST_TEMPLATE")
|
||||
legacyTemplateFile = githubtemplate.FindLegacy(rootDir, "PULL_REQUEST_TEMPLATE")
|
||||
}
|
||||
err := titleBodySurvey(cmd, &tb, client, baseRepo, title, body, defs, nonLegacyTemplateFiles, legacyTemplateFile, true, baseRepo.ViewerCanTriage())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not collect title and/or body: %w", err)
|
||||
}
|
||||
|
||||
action = tb.Action
|
||||
|
||||
if action == CancelAction {
|
||||
fmt.Fprintln(cmd.ErrOrStderr(), "Discarding.")
|
||||
return nil
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
title = tb.Title
|
||||
}
|
||||
if body == "" {
|
||||
body = tb.Body
|
||||
}
|
||||
}
|
||||
|
||||
if action == SubmitAction && title == "" {
|
||||
return errors.New("pull request title must not be blank")
|
||||
}
|
||||
|
||||
if isDraft && isWeb {
|
||||
return errors.New("the --draft flag is not supported with --web")
|
||||
}
|
||||
if len(reviewers) > 0 && isWeb {
|
||||
return errors.New("the --reviewer flag is not supported with --web")
|
||||
}
|
||||
|
||||
didForkRepo := false
|
||||
// if a head repository could not be determined so far, automatically create
|
||||
// one by forking the base repository
|
||||
if headRepo == nil {
|
||||
if baseRepo.IsPrivate {
|
||||
return fmt.Errorf("cannot fork private repository '%s'", ghrepo.FullName(baseRepo))
|
||||
}
|
||||
headRepo, err = api.ForkRepo(client, baseRepo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error forking repo: %w", err)
|
||||
}
|
||||
didForkRepo = true
|
||||
}
|
||||
|
||||
headBranchLabel := headBranch
|
||||
if !ghrepo.IsSame(baseRepo, headRepo) {
|
||||
headBranchLabel = fmt.Sprintf("%s:%s", headRepo.RepoOwner(), headBranch)
|
||||
}
|
||||
|
||||
if headRemote == nil {
|
||||
headRemote, _ = repoContext.RemoteForRepo(headRepo)
|
||||
}
|
||||
|
||||
// There are two cases when an existing remote for the head repo will be
|
||||
// missing:
|
||||
// 1. the head repo was just created by auto-forking;
|
||||
// 2. an existing fork was discovered by quering the API.
|
||||
//
|
||||
// In either case, we want to add the head repo as a new git remote so we
|
||||
// can push to it.
|
||||
if headRemote == nil {
|
||||
headRepoURL := formatRemoteURL(cmd, headRepo)
|
||||
|
||||
// TODO: prevent clashes with another remote of a same name
|
||||
gitRemote, err := git.AddRemote("fork", headRepoURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error adding remote: %w", err)
|
||||
}
|
||||
headRemote = &context.Remote{
|
||||
Remote: gitRemote,
|
||||
Repo: headRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// automatically push the branch if it hasn't been pushed anywhere yet
|
||||
if headBranchPushedTo == nil {
|
||||
pushTries := 0
|
||||
maxPushTries := 3
|
||||
for {
|
||||
if err := git.Push(headRemote.Name, fmt.Sprintf("HEAD:%s", headBranch)); err != nil {
|
||||
if didForkRepo && pushTries < maxPushTries {
|
||||
pushTries++
|
||||
// first wait 2 seconds after forking, then 4s, then 6s
|
||||
waitSeconds := 2 * pushTries
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "waiting %s before retrying...\n", utils.Pluralize(waitSeconds, "second"))
|
||||
time.Sleep(time.Duration(waitSeconds) * time.Second)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if action == SubmitAction {
|
||||
params := map[string]interface{}{
|
||||
"title": title,
|
||||
"body": body,
|
||||
"draft": isDraft,
|
||||
"baseRefName": baseBranch,
|
||||
"headRefName": headBranchLabel,
|
||||
}
|
||||
|
||||
err = addMetadataToIssueParams(client, baseRepo, params, &tb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr, err := api.CreatePullRequest(client, baseRepo, params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create pull request: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(cmd.OutOrStdout(), pr.URL)
|
||||
} else if action == PreviewAction {
|
||||
milestone := ""
|
||||
if len(milestoneTitles) > 0 {
|
||||
milestone = milestoneTitles[0]
|
||||
}
|
||||
openURL, err := generateCompareURL(baseRepo, baseBranch, headBranchLabel, title, body, assignees, labelNames, projectNames, milestone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if connectedToTerminal(cmd) {
|
||||
// TODO could exceed max url length for explorer
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "Opening %s in your browser.\n", utils.DisplayURL(openURL))
|
||||
}
|
||||
return utils.OpenInBrowser(openURL)
|
||||
} else {
|
||||
panic("Unreachable state")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func determineTrackingBranch(remotes context.Remotes, headBranch string) *git.TrackingRef {
|
||||
refsForLookup := []string{"HEAD"}
|
||||
var trackingRefs []git.TrackingRef
|
||||
|
||||
headBranchConfig := git.ReadBranchConfig(headBranch)
|
||||
if headBranchConfig.RemoteName != "" {
|
||||
tr := git.TrackingRef{
|
||||
RemoteName: headBranchConfig.RemoteName,
|
||||
BranchName: strings.TrimPrefix(headBranchConfig.MergeRef, "refs/heads/"),
|
||||
}
|
||||
trackingRefs = append(trackingRefs, tr)
|
||||
refsForLookup = append(refsForLookup, tr.String())
|
||||
}
|
||||
|
||||
for _, remote := range remotes {
|
||||
tr := git.TrackingRef{
|
||||
RemoteName: remote.Name,
|
||||
BranchName: headBranch,
|
||||
}
|
||||
trackingRefs = append(trackingRefs, tr)
|
||||
refsForLookup = append(refsForLookup, tr.String())
|
||||
}
|
||||
|
||||
resolvedRefs, _ := git.ShowRefs(refsForLookup...)
|
||||
if len(resolvedRefs) > 1 {
|
||||
for _, r := range resolvedRefs[1:] {
|
||||
if r.Hash != resolvedRefs[0].Hash {
|
||||
continue
|
||||
}
|
||||
for _, tr := range trackingRefs {
|
||||
if tr.String() != r.Name {
|
||||
continue
|
||||
}
|
||||
return &tr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func withPrAndIssueQueryParams(baseURL, title, body string, assignees, labels, projects []string, milestone string) (string, error) {
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
q := u.Query()
|
||||
if title != "" {
|
||||
q.Set("title", title)
|
||||
}
|
||||
if body != "" {
|
||||
q.Set("body", body)
|
||||
}
|
||||
if len(assignees) > 0 {
|
||||
q.Set("assignees", strings.Join(assignees, ","))
|
||||
}
|
||||
if len(labels) > 0 {
|
||||
q.Set("labels", strings.Join(labels, ","))
|
||||
}
|
||||
if len(projects) > 0 {
|
||||
q.Set("projects", strings.Join(projects, ","))
|
||||
}
|
||||
if milestone != "" {
|
||||
q.Set("milestone", milestone)
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func generateCompareURL(r ghrepo.Interface, base, head, title, body string, assignees, labels, projects []string, milestone string) (string, error) {
|
||||
u := ghrepo.GenerateRepoURL(r, "compare/%s...%s?expand=1", base, head)
|
||||
url, err := withPrAndIssueQueryParams(u, title, body, assignees, labels, projects, milestone)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return url, nil
|
||||
}
|
||||
|
||||
var prCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a pull request",
|
||||
Args: cmdutil.NoArgsQuoteReminder,
|
||||
RunE: prCreate,
|
||||
Example: heredoc.Doc(`
|
||||
$ gh pr create --title "The bug is fixed" --body "Everything works again"
|
||||
$ gh issue create --label "bug,help wanted"
|
||||
$ gh issue create --label bug --label "help wanted"
|
||||
$ gh pr create --reviewer monalisa,hubot
|
||||
$ gh pr create --project "Roadmap"
|
||||
$ gh pr create --base develop
|
||||
`),
|
||||
}
|
||||
|
||||
func init() {
|
||||
prCreateCmd.Flags().BoolP("draft", "d", false,
|
||||
"Mark pull request as a draft")
|
||||
prCreateCmd.Flags().StringP("title", "t", "",
|
||||
"Supply a title. Will prompt for one otherwise.")
|
||||
prCreateCmd.Flags().StringP("body", "b", "",
|
||||
"Supply a body. Will prompt for one otherwise.")
|
||||
prCreateCmd.Flags().StringP("base", "B", "",
|
||||
"The branch into which you want your code merged")
|
||||
prCreateCmd.Flags().BoolP("web", "w", false, "Open the web browser to create a pull request")
|
||||
prCreateCmd.Flags().BoolP("fill", "f", false, "Do not prompt for title/body and just use commit info")
|
||||
|
||||
prCreateCmd.Flags().StringSliceP("reviewer", "r", nil, "Request reviews from people by their `login`")
|
||||
prCreateCmd.Flags().StringSliceP("assignee", "a", nil, "Assign people by their `login`")
|
||||
prCreateCmd.Flags().StringSliceP("label", "l", nil, "Add labels by `name`")
|
||||
prCreateCmd.Flags().StringSliceP("project", "p", nil, "Add the pull request to projects by `name`")
|
||||
prCreateCmd.Flags().StringP("milestone", "m", "", "Add the pull request to a milestone by `name`")
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,104 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var prDiffCmd = &cobra.Command{
|
||||
Use: "diff {<number> | <url>}",
|
||||
Short: "View a pull request's changes.",
|
||||
RunE: prDiff,
|
||||
}
|
||||
|
||||
func init() {
|
||||
prDiffCmd.Flags().StringP("color", "c", "auto", "Whether or not to output color: {always|never|auto}")
|
||||
|
||||
prCmd.AddCommand(prDiffCmd)
|
||||
}
|
||||
|
||||
func prDiff(cmd *cobra.Command, args []string) error {
|
||||
color, err := cmd.Flags().GetString("color")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !validColorFlag(color) {
|
||||
return fmt.Errorf("did not understand color: %q. Expected one of always, never, or auto", color)
|
||||
}
|
||||
|
||||
ctx := contextForCommand(cmd)
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr, baseRepo, err := prFromArgs(ctx, apiClient, cmd, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not find pull request: %w", err)
|
||||
}
|
||||
|
||||
diff, err := apiClient.PullRequestDiff(baseRepo, pr.Number)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not find pull request diff: %w", err)
|
||||
}
|
||||
|
||||
out := cmd.OutOrStdout()
|
||||
if color == "auto" {
|
||||
color = "never"
|
||||
isTTY := false
|
||||
if outFile, isFile := out.(*os.File); isFile {
|
||||
isTTY = utils.IsTerminal(outFile)
|
||||
if isTTY {
|
||||
color = "always"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if color == "never" {
|
||||
fmt.Fprint(out, diff)
|
||||
return nil
|
||||
}
|
||||
|
||||
out = colorableOut(cmd)
|
||||
for _, diffLine := range strings.Split(diff, "\n") {
|
||||
output := diffLine
|
||||
switch {
|
||||
case isHeaderLine(diffLine):
|
||||
output = utils.Bold(diffLine)
|
||||
case isAdditionLine(diffLine):
|
||||
output = utils.Green(diffLine)
|
||||
case isRemovalLine(diffLine):
|
||||
output = utils.Red(diffLine)
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, output)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isHeaderLine(dl string) bool {
|
||||
prefixes := []string{"+++", "---", "diff", "index"}
|
||||
for _, p := range prefixes {
|
||||
if strings.HasPrefix(dl, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isAdditionLine(dl string) bool {
|
||||
return strings.HasPrefix(dl, "+")
|
||||
}
|
||||
|
||||
func isRemovalLine(dl string) bool {
|
||||
return strings.HasPrefix(dl, "-")
|
||||
}
|
||||
|
||||
func validColorFlag(c string) bool {
|
||||
return c == "auto" || c == "always" || c == "never"
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPRDiff_validation(t *testing.T) {
|
||||
_, err := RunCommand("pr diff --color=doublerainbow")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
eq(t, err.Error(), `did not understand color: "doublerainbow". Expected one of always, never, or auto`)
|
||||
}
|
||||
|
||||
func TestPRDiff_no_current_pr(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }`))
|
||||
http.StubResponse(200, bytes.NewBufferString(testDiff))
|
||||
_, err := RunCommand("pr diff")
|
||||
if err == nil {
|
||||
t.Fatal("expected error", err)
|
||||
}
|
||||
eq(t, err.Error(), `could not find pull request: no open pull requests found for branch "master"`)
|
||||
}
|
||||
|
||||
func TestPRDiff_argument_not_found(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 123 }
|
||||
} } }
|
||||
`))
|
||||
http.StubResponse(404, bytes.NewBufferString(""))
|
||||
_, err := RunCommand("pr diff 123")
|
||||
if err == nil {
|
||||
t.Fatal("expected error", err)
|
||||
}
|
||||
eq(t, err.Error(), `could not find pull request diff: pull request not found`)
|
||||
}
|
||||
|
||||
func TestPRDiff(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }`))
|
||||
http.StubResponse(200, bytes.NewBufferString(testDiff))
|
||||
output, err := RunCommand("pr diff")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
eq(t, output.String(), testDiff)
|
||||
}
|
||||
|
||||
const testDiff = `diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml
|
||||
index 73974448..b7fc0154 100644
|
||||
--- a/.github/workflows/releases.yml
|
||||
+++ b/.github/workflows/releases.yml
|
||||
@@ -44,6 +44,11 @@ jobs:
|
||||
token: ${{secrets.SITE_GITHUB_TOKEN}}
|
||||
- name: Publish documentation site
|
||||
if: "!contains(github.ref, '-')" # skip prereleases
|
||||
+ env:
|
||||
+ GIT_COMMITTER_NAME: cli automation
|
||||
+ GIT_AUTHOR_NAME: cli automation
|
||||
+ GIT_COMMITTER_EMAIL: noreply@github.com
|
||||
+ GIT_AUTHOR_EMAIL: noreply@github.com
|
||||
run: make site-publish
|
||||
- name: Move project cards
|
||||
if: "!contains(github.ref, '-')" # skip prereleases
|
||||
diff --git a/Makefile b/Makefile
|
||||
index f2b4805c..3d7bd0f9 100644
|
||||
--- a/Makefile
|
||||
+++ b/Makefile
|
||||
@@ -22,8 +22,8 @@ test:
|
||||
go test ./...
|
||||
.PHONY: test
|
||||
|
||||
-site:
|
||||
- git clone https://github.com/github/cli.github.com.git "$@"
|
||||
+site: bin/gh
|
||||
+ bin/gh repo clone github/cli.github.com "$@"
|
||||
|
||||
site-docs: site
|
||||
git -C site pull
|
||||
`
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func prFromArgs(ctx context.Context, apiClient *api.Client, cmd *cobra.Command, args []string) (*api.PullRequest, ghrepo.Interface, error) {
|
||||
if len(args) == 1 {
|
||||
// First check to see if the prString is a url, return repo from url if found. This
|
||||
// is run first because we don't need to run determineBaseRepo for this path
|
||||
prString := args[0]
|
||||
pr, r, err := prFromURL(ctx, apiClient, prString)
|
||||
if pr != nil || err != nil {
|
||||
return pr, r, err
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := determineBaseRepo(apiClient, cmd, ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not determine base repo: %w", err)
|
||||
}
|
||||
|
||||
// If there are no args see if we can guess the PR from the current branch
|
||||
if len(args) == 0 {
|
||||
pr, err := prForCurrentBranch(ctx, apiClient, repo)
|
||||
return pr, repo, err
|
||||
} else {
|
||||
prString := args[0]
|
||||
// Next see if the prString is a number and use that to look up the url
|
||||
pr, err := prFromNumberString(ctx, apiClient, repo, prString)
|
||||
if pr != nil || err != nil {
|
||||
return pr, repo, err
|
||||
}
|
||||
|
||||
// Last see if it is a branch name
|
||||
pr, err = api.PullRequestForBranch(apiClient, repo, "", prString)
|
||||
return pr, repo, err
|
||||
}
|
||||
}
|
||||
|
||||
func prFromNumberString(ctx context.Context, apiClient *api.Client, repo ghrepo.Interface, s string) (*api.PullRequest, error) {
|
||||
if prNumber, err := strconv.Atoi(strings.TrimPrefix(s, "#")); err == nil {
|
||||
return api.PullRequestByNumber(apiClient, repo, prNumber)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var pullURLRE = regexp.MustCompile(`^/([^/]+)/([^/]+)/pull/(\d+)`)
|
||||
|
||||
func prFromURL(ctx context.Context, apiClient *api.Client, s string) (*api.PullRequest, ghrepo.Interface, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
if u.Scheme != "https" && u.Scheme != "http" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
m := pullURLRE.FindStringSubmatch(u.Path)
|
||||
if m == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
repo := ghrepo.NewWithHost(m[1], m[2], u.Hostname())
|
||||
prNumberString := m[3]
|
||||
pr, err := prFromNumberString(ctx, apiClient, repo, prNumberString)
|
||||
return pr, repo, err
|
||||
}
|
||||
|
||||
func prForCurrentBranch(ctx context.Context, apiClient *api.Client, repo ghrepo.Interface) (*api.PullRequest, error) {
|
||||
prHeadRef, err := ctx.Branch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
branchConfig := git.ReadBranchConfig(prHeadRef)
|
||||
|
||||
// the branch is configured to merge a special PR head ref
|
||||
prHeadRE := regexp.MustCompile(`^refs/pull/(\d+)/head$`)
|
||||
if m := prHeadRE.FindStringSubmatch(branchConfig.MergeRef); m != nil {
|
||||
return prFromNumberString(ctx, apiClient, repo, m[1])
|
||||
}
|
||||
|
||||
var branchOwner string
|
||||
if branchConfig.RemoteURL != nil {
|
||||
// the branch merges from a remote specified by URL
|
||||
if r, err := ghrepo.FromURL(branchConfig.RemoteURL); err == nil {
|
||||
branchOwner = r.RepoOwner()
|
||||
}
|
||||
} else if branchConfig.RemoteName != "" {
|
||||
// the branch merges from a remote specified by name
|
||||
rem, _ := ctx.Remotes()
|
||||
if r, err := rem.FindByName(branchConfig.RemoteName); err == nil {
|
||||
branchOwner = r.RepoOwner()
|
||||
}
|
||||
}
|
||||
|
||||
if branchOwner != "" {
|
||||
if strings.HasPrefix(branchConfig.MergeRef, "refs/heads/") {
|
||||
prHeadRef = strings.TrimPrefix(branchConfig.MergeRef, "refs/heads/")
|
||||
}
|
||||
// prepend `OWNER:` if this branch is pushed to a fork
|
||||
if !strings.EqualFold(branchOwner, repo.RepoOwner()) {
|
||||
prHeadRef = fmt.Sprintf("%s:%s", branchOwner, prHeadRef)
|
||||
}
|
||||
}
|
||||
|
||||
return api.PullRequestForBranch(apiClient, repo, "", prHeadRef)
|
||||
}
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/pkg/prompt"
|
||||
"github.com/cli/cli/pkg/surveyext"
|
||||
"github.com/cli/cli/utils"
|
||||
)
|
||||
|
||||
func init() {
|
||||
prCmd.AddCommand(prReviewCmd)
|
||||
|
||||
prReviewCmd.Flags().BoolP("approve", "a", false, "Approve pull request")
|
||||
prReviewCmd.Flags().BoolP("request-changes", "r", false, "Request changes on a pull request")
|
||||
prReviewCmd.Flags().BoolP("comment", "c", false, "Comment on a pull request")
|
||||
prReviewCmd.Flags().StringP("body", "b", "", "Specify the body of a review")
|
||||
}
|
||||
|
||||
var prReviewCmd = &cobra.Command{
|
||||
Use: "review [<number> | <url> | <branch>]",
|
||||
Short: "Add a review to a pull request",
|
||||
Long: `Add a review to a pull request.
|
||||
|
||||
Without an argument, the pull request that belongs to the current branch is reviewed.`,
|
||||
Example: heredoc.Doc(`
|
||||
# approve the pull request of the current branch
|
||||
$ gh pr review --approve
|
||||
|
||||
# leave a review comment for the current branch
|
||||
$ gh pr review --comment -b "interesting"
|
||||
|
||||
# add a review for a specific pull request
|
||||
$ gh pr review 123
|
||||
|
||||
# request changes on a specific pull request
|
||||
$ gh pr review 123 -r -b "needs more ASCII art"
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: prReview,
|
||||
}
|
||||
|
||||
func processReviewOpt(cmd *cobra.Command) (*api.PullRequestReviewInput, error) {
|
||||
found := 0
|
||||
flag := ""
|
||||
var state api.PullRequestReviewState
|
||||
|
||||
if cmd.Flags().Changed("approve") {
|
||||
found++
|
||||
flag = "approve"
|
||||
state = api.ReviewApprove
|
||||
}
|
||||
if cmd.Flags().Changed("request-changes") {
|
||||
found++
|
||||
flag = "request-changes"
|
||||
state = api.ReviewRequestChanges
|
||||
}
|
||||
if cmd.Flags().Changed("comment") {
|
||||
found++
|
||||
flag = "comment"
|
||||
state = api.ReviewComment
|
||||
}
|
||||
|
||||
body, err := cmd.Flags().GetString("body")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if found == 0 && body == "" {
|
||||
if connectedToTerminal(cmd) {
|
||||
return nil, nil // signal interactive mode
|
||||
}
|
||||
return nil, errors.New("--approve, --request-changes, or --comment required when not attached to a tty")
|
||||
} else if found == 0 && body != "" {
|
||||
return nil, errors.New("--body unsupported without --approve, --request-changes, or --comment")
|
||||
} else if found > 1 {
|
||||
return nil, errors.New("need exactly one of --approve, --request-changes, or --comment")
|
||||
}
|
||||
|
||||
if (flag == "request-changes" || flag == "comment") && body == "" {
|
||||
return nil, fmt.Errorf("body cannot be blank for %s review", flag)
|
||||
}
|
||||
|
||||
return &api.PullRequestReviewInput{
|
||||
Body: body,
|
||||
State: state,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func prReview(cmd *cobra.Command, args []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr, _, err := prFromArgs(ctx, apiClient, cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reviewData, err := processReviewOpt(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("did not understand desired review action: %w", err)
|
||||
}
|
||||
|
||||
stderr := colorableErr(cmd)
|
||||
|
||||
if reviewData == nil {
|
||||
reviewData, err = reviewSurvey(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if reviewData == nil && err == nil {
|
||||
fmt.Fprint(stderr, "Discarding.\n")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err = api.AddReview(apiClient, pr, reviewData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create review: %w", err)
|
||||
}
|
||||
|
||||
if !connectedToTerminal(cmd) {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch reviewData.State {
|
||||
case api.ReviewComment:
|
||||
fmt.Fprintf(stderr, "%s Reviewed pull request #%d\n", utils.Gray("-"), pr.Number)
|
||||
case api.ReviewApprove:
|
||||
fmt.Fprintf(stderr, "%s Approved pull request #%d\n", utils.Green("✓"), pr.Number)
|
||||
case api.ReviewRequestChanges:
|
||||
fmt.Fprintf(stderr, "%s Requested changes to pull request #%d\n", utils.Red("+"), pr.Number)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func reviewSurvey(cmd *cobra.Command) (*api.PullRequestReviewInput, error) {
|
||||
editorCommand, err := determineEditor(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
typeAnswers := struct {
|
||||
ReviewType string
|
||||
}{}
|
||||
typeQs := []*survey.Question{
|
||||
{
|
||||
Name: "reviewType",
|
||||
Prompt: &survey.Select{
|
||||
Message: "What kind of review do you want to give?",
|
||||
Options: []string{
|
||||
"Comment",
|
||||
"Approve",
|
||||
"Request changes",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = prompt.SurveyAsk(typeQs, &typeAnswers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var reviewState api.PullRequestReviewState
|
||||
|
||||
switch typeAnswers.ReviewType {
|
||||
case "Approve":
|
||||
reviewState = api.ReviewApprove
|
||||
case "Request changes":
|
||||
reviewState = api.ReviewRequestChanges
|
||||
case "Comment":
|
||||
reviewState = api.ReviewComment
|
||||
default:
|
||||
panic("unreachable state")
|
||||
}
|
||||
|
||||
bodyAnswers := struct {
|
||||
Body string
|
||||
}{}
|
||||
|
||||
blankAllowed := false
|
||||
if reviewState == api.ReviewApprove {
|
||||
blankAllowed = true
|
||||
}
|
||||
|
||||
bodyQs := []*survey.Question{
|
||||
{
|
||||
Name: "body",
|
||||
Prompt: &surveyext.GhEditor{
|
||||
BlankAllowed: blankAllowed,
|
||||
EditorCommand: editorCommand,
|
||||
Editor: &survey.Editor{
|
||||
Message: "Review body",
|
||||
FileName: "*.md",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = prompt.SurveyAsk(bodyQs, &bodyAnswers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if bodyAnswers.Body == "" && (reviewState == api.ReviewComment || reviewState == api.ReviewRequestChanges) {
|
||||
return nil, errors.New("this type of review cannot be blank")
|
||||
}
|
||||
|
||||
if len(bodyAnswers.Body) > 0 {
|
||||
out := colorableOut(cmd)
|
||||
renderedBody, err := utils.RenderMarkdown(bodyAnswers.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "Got:\n%s", renderedBody)
|
||||
}
|
||||
|
||||
confirm := false
|
||||
confirmQs := []*survey.Question{
|
||||
{
|
||||
Name: "confirm",
|
||||
Prompt: &survey.Confirm{
|
||||
Message: "Submit?",
|
||||
Default: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = prompt.SurveyAsk(confirmQs, &confirm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !confirm {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &api.PullRequestReviewInput{
|
||||
Body: bodyAnswers.Body,
|
||||
State: reviewState,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,495 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/pkg/prompt"
|
||||
"github.com/cli/cli/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPRReview_validation(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
for _, cmd := range []string{
|
||||
`pr review --approve --comment 123`,
|
||||
`pr review --approve --comment -b"hey" 123`,
|
||||
} {
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 123 }
|
||||
} } }
|
||||
`))
|
||||
_, err := RunCommand(cmd)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
eq(t, err.Error(), "did not understand desired review action: need exactly one of --approve, --request-changes, or --comment")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRReview_bad_body(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 123 }
|
||||
} } }
|
||||
`))
|
||||
_, err := RunCommand(`pr review 123 -b "radical"`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
eq(t, err.Error(), "did not understand desired review action: --body unsupported without --approve, --request-changes, or --comment")
|
||||
}
|
||||
|
||||
func TestPRReview_url_arg(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
defer stubTerminal(true)()
|
||||
http := initFakeHTTP()
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "foobar123",
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } } `))
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
|
||||
|
||||
output, err := RunCommand("pr review --approve https://github.com/OWNER/REPO/pull/123")
|
||||
if err != nil {
|
||||
t.Fatalf("error running pr review: %s", err)
|
||||
}
|
||||
|
||||
test.ExpectLines(t, output.Stderr(), "Approved pull request #123")
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Input struct {
|
||||
PullRequestID string
|
||||
Event string
|
||||
Body string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
_ = json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.Input.PullRequestID, "foobar123")
|
||||
eq(t, reqBody.Variables.Input.Event, "APPROVE")
|
||||
eq(t, reqBody.Variables.Input.Body, "")
|
||||
}
|
||||
|
||||
func TestPRReview_number_arg(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
defer stubTerminal(true)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"id": "foobar123",
|
||||
"number": 123,
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } } `))
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
|
||||
|
||||
output, err := RunCommand("pr review --approve 123")
|
||||
if err != nil {
|
||||
t.Fatalf("error running pr review: %s", err)
|
||||
}
|
||||
|
||||
test.ExpectLines(t, output.Stderr(), "Approved pull request #123")
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Input struct {
|
||||
PullRequestID string
|
||||
Event string
|
||||
Body string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
_ = json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.Input.PullRequestID, "foobar123")
|
||||
eq(t, reqBody.Variables.Input.Event, "APPROVE")
|
||||
eq(t, reqBody.Variables.Input.Body, "")
|
||||
}
|
||||
|
||||
func TestPRReview_no_arg(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
defer stubTerminal(true)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }`))
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
|
||||
|
||||
output, err := RunCommand(`pr review --comment -b "cool story"`)
|
||||
if err != nil {
|
||||
t.Fatalf("error running pr review: %s", err)
|
||||
}
|
||||
|
||||
test.ExpectLines(t, output.Stderr(), "Reviewed pull request #123")
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Input struct {
|
||||
PullRequestID string
|
||||
Event string
|
||||
Body string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
_ = json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.Input.PullRequestID, "foobar123")
|
||||
eq(t, reqBody.Variables.Input.Event, "COMMENT")
|
||||
eq(t, reqBody.Variables.Input.Body, "cool story")
|
||||
}
|
||||
|
||||
func TestPRReview_blank_comment(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 123 }
|
||||
} } }
|
||||
`))
|
||||
|
||||
_, err := RunCommand(`pr review --comment 123`)
|
||||
eq(t, err.Error(), "did not understand desired review action: body cannot be blank for comment review")
|
||||
}
|
||||
|
||||
func TestPRReview_blank_request_changes(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"pullRequest": { "number": 123 }
|
||||
} } }
|
||||
`))
|
||||
|
||||
_, err := RunCommand(`pr review -r 123`)
|
||||
eq(t, err.Error(), "did not understand desired review action: body cannot be blank for request-changes review")
|
||||
}
|
||||
|
||||
func TestPRReview(t *testing.T) {
|
||||
type c struct {
|
||||
Cmd string
|
||||
ExpectedEvent string
|
||||
ExpectedBody string
|
||||
}
|
||||
cases := []c{
|
||||
{`pr review --request-changes -b"bad"`, "REQUEST_CHANGES", "bad"},
|
||||
{`pr review --approve`, "APPROVE", ""},
|
||||
{`pr review --approve -b"hot damn"`, "APPROVE", "hot damn"},
|
||||
{`pr review --comment --body "i donno"`, "COMMENT", "i donno"},
|
||||
}
|
||||
|
||||
for _, kase := range cases {
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }
|
||||
`))
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
|
||||
|
||||
_, err := RunCommand(kase.Cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error running %s: %s", kase.Cmd, err)
|
||||
}
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Input struct {
|
||||
Event string
|
||||
Body string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
_ = json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.Input.Event, kase.ExpectedEvent)
|
||||
eq(t, reqBody.Variables.Input.Body, kase.ExpectedBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRReview_nontty_insufficient_flags(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
defer stubTerminal(false)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }
|
||||
`))
|
||||
|
||||
output, err := RunCommand("pr review")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
|
||||
assert.Equal(t, "did not understand desired review action: --approve, --request-changes, or --comment required when not attached to a tty", err.Error())
|
||||
}
|
||||
|
||||
func TestPRReview_nontty(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
defer stubTerminal(false)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }
|
||||
`))
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
|
||||
output, err := RunCommand("pr review -c -bcool")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error running command: %s", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "", output.String())
|
||||
assert.Equal(t, "", output.Stderr())
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Input struct {
|
||||
Event string
|
||||
Body string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
_ = json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
assert.Equal(t, "COMMENT", reqBody.Variables.Input.Event)
|
||||
assert.Equal(t, "cool", reqBody.Variables.Input.Body)
|
||||
}
|
||||
|
||||
func TestPRReview_interactive(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
defer stubTerminal(true)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }
|
||||
`))
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
|
||||
as, teardown := prompt.InitAskStubber()
|
||||
defer teardown()
|
||||
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "reviewType",
|
||||
Value: "Approve",
|
||||
},
|
||||
})
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "body",
|
||||
Value: "cool story",
|
||||
},
|
||||
})
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "confirm",
|
||||
Value: true,
|
||||
},
|
||||
})
|
||||
|
||||
output, err := RunCommand(`pr review`)
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error running pr review: %s", err)
|
||||
}
|
||||
|
||||
test.ExpectLines(t, output.Stderr(), "Approved pull request #123")
|
||||
|
||||
test.ExpectLines(t, output.String(),
|
||||
"Got:",
|
||||
"cool.*story")
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Input struct {
|
||||
Event string
|
||||
Body string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
_ = json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.Input.Event, "APPROVE")
|
||||
eq(t, reqBody.Variables.Input.Body, "cool story")
|
||||
}
|
||||
|
||||
func TestPRReview_interactive_no_body(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
defer stubTerminal(true)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }
|
||||
`))
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
|
||||
as, teardown := prompt.InitAskStubber()
|
||||
defer teardown()
|
||||
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "reviewType",
|
||||
Value: "Request changes",
|
||||
},
|
||||
})
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "body",
|
||||
Default: true,
|
||||
},
|
||||
})
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "confirm",
|
||||
Value: true,
|
||||
},
|
||||
})
|
||||
|
||||
_, err := RunCommand(`pr review`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
eq(t, err.Error(), "this type of review cannot be blank")
|
||||
}
|
||||
|
||||
func TestPRReview_interactive_blank_approve(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "feature")
|
||||
defer stubTerminal(true)()
|
||||
http := initFakeHTTP()
|
||||
http.StubRepoResponse("OWNER", "REPO")
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequests": { "nodes": [
|
||||
{ "url": "https://github.com/OWNER/REPO/pull/123",
|
||||
"number": 123,
|
||||
"id": "foobar123",
|
||||
"headRefName": "feature",
|
||||
"baseRefName": "master" }
|
||||
] } } } }
|
||||
`))
|
||||
http.StubResponse(200, bytes.NewBufferString(`{"data": {} }`))
|
||||
as, teardown := prompt.InitAskStubber()
|
||||
defer teardown()
|
||||
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "reviewType",
|
||||
Value: "Approve",
|
||||
},
|
||||
})
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "body",
|
||||
Default: true,
|
||||
},
|
||||
})
|
||||
as.Stub([]*prompt.QuestionStub{
|
||||
{
|
||||
Name: "confirm",
|
||||
Value: true,
|
||||
},
|
||||
})
|
||||
|
||||
output, err := RunCommand(`pr review`)
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error running pr review: %s", err)
|
||||
}
|
||||
|
||||
unexpect := regexp.MustCompile("Got:")
|
||||
if unexpect.MatchString(output.String()) {
|
||||
t.Errorf("did not expect to see body printed in %s", output.String())
|
||||
}
|
||||
|
||||
test.ExpectLines(t, output.Stderr(), "Approved pull request #123")
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[2].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Input struct {
|
||||
Event string
|
||||
Body string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
_ = json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.Input.Event, "APPROVE")
|
||||
eq(t, reqBody.Variables.Input.Body, "")
|
||||
|
||||
}
|
||||
1731
command/pr_test.go
1731
command/pr_test.go
File diff suppressed because it is too large
Load diff
189
command/root.go
189
command/root.go
|
|
@ -16,11 +16,17 @@ import (
|
|||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
"github.com/cli/cli/git"
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/internal/run"
|
||||
apiCmd "github.com/cli/cli/pkg/cmd/api"
|
||||
authCmd "github.com/cli/cli/pkg/cmd/auth"
|
||||
authLoginCmd "github.com/cli/cli/pkg/cmd/auth/login"
|
||||
authLogoutCmd "github.com/cli/cli/pkg/cmd/auth/logout"
|
||||
gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create"
|
||||
prCmd "github.com/cli/cli/pkg/cmd/pr"
|
||||
repoCmd "github.com/cli/cli/pkg/cmd/repo"
|
||||
repoCloneCmd "github.com/cli/cli/pkg/cmd/repo/clone"
|
||||
repoCreateCmd "github.com/cli/cli/pkg/cmd/repo/create"
|
||||
|
|
@ -36,9 +42,6 @@ import (
|
|||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// TODO these are sprinkled across command, context, config, and ghrepo
|
||||
const defaultHostname = "github.com"
|
||||
|
||||
// Version is dynamically set by the toolchain or overridden by the Makefile.
|
||||
var Version = "DEV"
|
||||
|
||||
|
|
@ -47,6 +50,8 @@ var BuildDate = "" // YYYY-MM-DD
|
|||
|
||||
var versionOutput = ""
|
||||
|
||||
var defaultStreams *iostreams.IOStreams
|
||||
|
||||
func init() {
|
||||
if Version == "DEV" {
|
||||
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" {
|
||||
|
|
@ -78,22 +83,21 @@ func init() {
|
|||
return &cmdutil.FlagError{Err: err}
|
||||
})
|
||||
|
||||
defaultStreams = iostreams.System()
|
||||
|
||||
// TODO: iron out how a factory incorporates context
|
||||
cmdFactory := &cmdutil.Factory{
|
||||
IOStreams: iostreams.System(),
|
||||
IOStreams: defaultStreams,
|
||||
HttpClient: func() (*http.Client, error) {
|
||||
token := os.Getenv("GITHUB_TOKEN")
|
||||
if len(token) == 0 {
|
||||
// TODO: decouple from `context`
|
||||
ctx := context.New()
|
||||
var err error
|
||||
// TODO: pass IOStreams to this so that the auth flow knows if it's interactive or not
|
||||
token, err = ctx.AuthToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO: decouple from `context`
|
||||
ctx := context.New()
|
||||
cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return httpClient(token), nil
|
||||
|
||||
// TODO: avoid setting Accept header for `api` command
|
||||
return httpClient(defaultStreams, cfg, true), nil
|
||||
},
|
||||
BaseRepo: func() (ghrepo.Interface, error) {
|
||||
// TODO: decouple from `context`
|
||||
|
|
@ -113,6 +117,13 @@ func init() {
|
|||
}
|
||||
return cfg, nil
|
||||
},
|
||||
Branch: func() (string, error) {
|
||||
currentBranch, err := git.CurrentBranch()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not determine current branch: %w", err)
|
||||
}
|
||||
return currentBranch, nil
|
||||
},
|
||||
}
|
||||
RootCmd.AddCommand(apiCmd.NewCmdApi(cmdFactory, nil))
|
||||
|
||||
|
|
@ -124,6 +135,10 @@ func init() {
|
|||
RootCmd.AddCommand(gistCmd)
|
||||
gistCmd.AddCommand(gistCreateCmd.NewCmdCreate(cmdFactory, nil))
|
||||
|
||||
RootCmd.AddCommand(authCmd.Cmd)
|
||||
authCmd.Cmd.AddCommand(authLoginCmd.NewCmdLogin(cmdFactory, nil))
|
||||
authCmd.Cmd.AddCommand(authLogoutCmd.NewCmdLogout(cmdFactory, nil))
|
||||
|
||||
resolvedBaseRepo := func() (ghrepo.Interface, error) {
|
||||
httpClient, err := cmdFactory.HttpClient()
|
||||
if err != nil {
|
||||
|
|
@ -160,6 +175,7 @@ func init() {
|
|||
repoCmd.Cmd.AddCommand(repoCreateCmd.NewCmdCreate(cmdFactory, nil))
|
||||
repoCmd.Cmd.AddCommand(creditsCmd.NewCmdRepoCredits(&repoResolvingCmdFactory, nil))
|
||||
|
||||
RootCmd.AddCommand(prCmd.NewCmdPR(&repoResolvingCmdFactory))
|
||||
RootCmd.AddCommand(creditsCmd.NewCmdCredits(cmdFactory, nil))
|
||||
}
|
||||
|
||||
|
|
@ -218,14 +234,11 @@ var initContext = func() context.Context {
|
|||
if repo := os.Getenv("GH_REPO"); repo != "" {
|
||||
ctx.SetBaseRepo(repo)
|
||||
}
|
||||
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
|
||||
ctx.SetAuthToken(token)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
// BasicClient returns an API client that borrows from but does not depend on
|
||||
// user configuration
|
||||
// BasicClient returns an API client for github.com only that borrows from but
|
||||
// does not depend on user configuration
|
||||
func BasicClient() (*api.Client, error) {
|
||||
var opts []api.ClientOption
|
||||
if verbose := os.Getenv("DEBUG"); verbose != "" {
|
||||
|
|
@ -236,7 +249,7 @@ func BasicClient() (*api.Client, error) {
|
|||
token := os.Getenv("GITHUB_TOKEN")
|
||||
if token == "" {
|
||||
if c, err := config.ParseDefaultConfig(); err == nil {
|
||||
token, _ = c.Get(defaultHostname, "oauth_token")
|
||||
token, _ = c.Get(ghinstance.Default(), "oauth_token")
|
||||
}
|
||||
}
|
||||
if token != "" {
|
||||
|
|
@ -253,72 +266,71 @@ func contextForCommand(cmd *cobra.Command) context.Context {
|
|||
return ctx
|
||||
}
|
||||
|
||||
// for cmdutil-powered commands
|
||||
func httpClient(token string) *http.Client {
|
||||
// generic authenticated HTTP client for commands
|
||||
func httpClient(io *iostreams.IOStreams, cfg config.Config, setAccept bool) *http.Client {
|
||||
var opts []api.ClientOption
|
||||
if verbose := os.Getenv("DEBUG"); verbose != "" {
|
||||
opts = append(opts, apiVerboseLog())
|
||||
}
|
||||
|
||||
opts = append(opts,
|
||||
api.AddHeader("Authorization", fmt.Sprintf("token %s", token)),
|
||||
api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", Version)),
|
||||
// antiope-preview: Checks
|
||||
// FIXME: avoid setting this header for `api` command
|
||||
api.AddHeader("Accept", "application/vnd.github.antiope-preview+json"),
|
||||
api.AddHeaderFunc("Authorization", func(req *http.Request) (string, error) {
|
||||
if token := os.Getenv("GITHUB_TOKEN"); token != "" {
|
||||
return fmt.Sprintf("token %s", token), nil
|
||||
}
|
||||
|
||||
hostname := ghinstance.NormalizeHostname(req.URL.Hostname())
|
||||
token, err := cfg.Get(hostname, "oauth_token")
|
||||
if token == "" {
|
||||
var notFound *config.NotFoundError
|
||||
// TODO: check if stdout is TTY too
|
||||
if errors.As(err, ¬Found) && io.IsStdinTTY() {
|
||||
// interactive OAuth flow
|
||||
token, err = config.AuthFlowWithConfig(cfg, hostname, "Notice: authentication required")
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if token == "" {
|
||||
// TODO: instruct user how to manually authenticate
|
||||
return "", fmt.Errorf("authentication required for %s", hostname)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("token %s", token), nil
|
||||
}),
|
||||
)
|
||||
|
||||
if setAccept {
|
||||
opts = append(opts,
|
||||
api.AddHeaderFunc("Accept", func(req *http.Request) (string, error) {
|
||||
// antiope-preview: Checks
|
||||
accept := "application/vnd.github.antiope-preview+json"
|
||||
if ghinstance.IsEnterprise(req.URL.Hostname()) {
|
||||
// shadow-cat-preview: Draft pull requests
|
||||
accept += ", application/vnd.github.shadow-cat-preview"
|
||||
}
|
||||
return accept, nil
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return api.NewHTTPClient(opts...)
|
||||
}
|
||||
|
||||
// overridden in tests
|
||||
// LEGACY; overridden in tests
|
||||
var apiClientForContext = func(ctx context.Context) (*api.Client, error) {
|
||||
token, err := ctx.AuthToken()
|
||||
cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var opts []api.ClientOption
|
||||
if verbose := os.Getenv("DEBUG"); verbose != "" {
|
||||
opts = append(opts, apiVerboseLog())
|
||||
}
|
||||
|
||||
getAuthValue := func() string {
|
||||
return fmt.Sprintf("token %s", token)
|
||||
}
|
||||
|
||||
tokenFromEnv := func() bool {
|
||||
return os.Getenv("GITHUB_TOKEN") == token
|
||||
}
|
||||
|
||||
checkScopesFunc := func(appID string) error {
|
||||
if config.IsGitHubApp(appID) && !tokenFromEnv() && utils.IsTerminal(os.Stdin) && utils.IsTerminal(os.Stderr) {
|
||||
cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newToken, err := config.AuthFlowWithConfig(cfg, defaultHostname, "Notice: additional authorization required")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// update configuration in memory
|
||||
token = newToken
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "Warning: gh now requires the `read:org` OAuth scope.")
|
||||
fmt.Fprintln(os.Stderr, "Visit https://github.com/settings/tokens and edit your token to enable `read:org`")
|
||||
if tokenFromEnv() {
|
||||
fmt.Fprintln(os.Stderr, "or generate a new token for the GITHUB_TOKEN environment variable")
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "or generate a new token and paste it via `gh config set -h github.com oauth_token MYTOKEN`")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
opts = append(opts,
|
||||
api.CheckScopes("read:org", checkScopesFunc),
|
||||
api.AddHeaderFunc("Authorization", getAuthValue),
|
||||
api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", Version)),
|
||||
// antiope-preview: Checks
|
||||
api.AddHeader("Accept", "application/vnd.github.antiope-preview+json"),
|
||||
)
|
||||
|
||||
return api.NewClient(opts...), nil
|
||||
http := httpClient(defaultStreams, cfg, true)
|
||||
return api.NewClientFromHTTP(http), nil
|
||||
}
|
||||
|
||||
func apiVerboseLog() api.ClientOption {
|
||||
|
|
@ -382,39 +394,6 @@ func determineBaseRepo(apiClient *api.Client, cmd *cobra.Command, ctx context.Co
|
|||
return baseRepo, nil
|
||||
}
|
||||
|
||||
// TODO there is a parallel implementation for isolated commands
|
||||
func formatRemoteURL(cmd *cobra.Command, repo ghrepo.Interface) string {
|
||||
ctx := contextForCommand(cmd)
|
||||
|
||||
var protocol string
|
||||
cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
fmt.Fprintf(colorableErr(cmd), "%s failed to load config: %s. using defaults\n", utils.Yellow("!"), err)
|
||||
} else {
|
||||
protocol, _ = cfg.Get(repo.RepoHost(), "git_protocol")
|
||||
}
|
||||
|
||||
if protocol == "ssh" {
|
||||
return fmt.Sprintf("git@%s:%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s/%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName())
|
||||
}
|
||||
|
||||
func determineEditor(cmd *cobra.Command) (string, error) {
|
||||
editorCommand := os.Getenv("GH_EDITOR")
|
||||
if editorCommand == "" {
|
||||
ctx := contextForCommand(cmd)
|
||||
cfg, err := ctx.Config()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read config: %w", err)
|
||||
}
|
||||
editorCommand, _ = cfg.Get(defaultHostname, "editor")
|
||||
}
|
||||
|
||||
return editorCommand, nil
|
||||
}
|
||||
|
||||
func ExecuteShellAlias(args []string) error {
|
||||
externalCmd := exec.Command(args[0], args[1:]...)
|
||||
externalCmd.Stderr = os.Stderr
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ package command
|
|||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
)
|
||||
|
||||
func TestChangelogURL(t *testing.T) {
|
||||
|
|
@ -42,22 +40,3 @@ func TestChangelogURL(t *testing.T) {
|
|||
t.Errorf("expected %s to create url %s but got %s", tag, url, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteURLFormatting_no_config(t *testing.T) {
|
||||
initBlankContext("", "OWNER/REPO", "master")
|
||||
result := formatRemoteURL(prCheckoutCmd, ghrepo.New("OWNER", "REPO"))
|
||||
eq(t, result, "https://github.com/OWNER/REPO.git")
|
||||
}
|
||||
|
||||
func TestRemoteURLFormatting_ssh_config(t *testing.T) {
|
||||
cfg := `---
|
||||
hosts:
|
||||
github.com:
|
||||
user: OWNER
|
||||
oauth_token: MUSTBEHIGHCUZIMATOKEN
|
||||
git_protocol: ssh
|
||||
`
|
||||
initBlankContext(cfg, "OWNER/REPO", "master")
|
||||
result := formatRemoteURL(prCheckoutCmd, ghrepo.New("OWNER", "REPO"))
|
||||
eq(t, result, "git@github.com:OWNER/REPO.git")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ package command
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/context"
|
||||
|
|
@ -14,6 +15,13 @@ import (
|
|||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func eq(t *testing.T, got interface{}, expected interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected: %v, got: %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
const defaultTestConfig = `hosts:
|
||||
github.com:
|
||||
user: OWNER
|
||||
|
|
@ -102,18 +110,6 @@ func RunCommand(args string) (*cmdOut, error) {
|
|||
return &cmdOut{&outBuf, &errBuf}, err
|
||||
}
|
||||
|
||||
type errorStub struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (s errorStub) Output() ([]byte, error) {
|
||||
return nil, errors.New(s.message)
|
||||
}
|
||||
|
||||
func (s errorStub) Run() error {
|
||||
return errors.New(s.message)
|
||||
}
|
||||
|
||||
func stubTerminal(connected bool) func() {
|
||||
isTerminal := utils.IsTerminal
|
||||
utils.IsTerminal = func(_ interface{}) bool {
|
||||
|
|
|
|||
|
|
@ -1,386 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghrepo"
|
||||
"github.com/cli/cli/pkg/githubtemplate"
|
||||
"github.com/cli/cli/pkg/prompt"
|
||||
"github.com/cli/cli/pkg/surveyext"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type Action int
|
||||
type metadataStateType int
|
||||
|
||||
const (
|
||||
issueMetadata metadataStateType = iota
|
||||
prMetadata
|
||||
)
|
||||
|
||||
type issueMetadataState struct {
|
||||
Type metadataStateType
|
||||
|
||||
Body string
|
||||
Title string
|
||||
Action Action
|
||||
|
||||
Metadata []string
|
||||
Reviewers []string
|
||||
Assignees []string
|
||||
Labels []string
|
||||
Projects []string
|
||||
Milestones []string
|
||||
|
||||
MetadataResult *api.RepoMetadataResult
|
||||
}
|
||||
|
||||
func (tb *issueMetadataState) HasMetadata() bool {
|
||||
return len(tb.Reviewers) > 0 ||
|
||||
len(tb.Assignees) > 0 ||
|
||||
len(tb.Labels) > 0 ||
|
||||
len(tb.Projects) > 0 ||
|
||||
len(tb.Milestones) > 0
|
||||
}
|
||||
|
||||
const (
|
||||
SubmitAction Action = iota
|
||||
PreviewAction
|
||||
CancelAction
|
||||
MetadataAction
|
||||
|
||||
noMilestone = "(none)"
|
||||
)
|
||||
|
||||
func confirmSubmission(allowPreview bool, allowMetadata bool) (Action, error) {
|
||||
const (
|
||||
submitLabel = "Submit"
|
||||
previewLabel = "Continue in browser"
|
||||
metadataLabel = "Add metadata"
|
||||
cancelLabel = "Cancel"
|
||||
)
|
||||
|
||||
options := []string{submitLabel}
|
||||
if allowPreview {
|
||||
options = append(options, previewLabel)
|
||||
}
|
||||
if allowMetadata {
|
||||
options = append(options, metadataLabel)
|
||||
}
|
||||
options = append(options, cancelLabel)
|
||||
|
||||
confirmAnswers := struct {
|
||||
Confirmation int
|
||||
}{}
|
||||
confirmQs := []*survey.Question{
|
||||
{
|
||||
Name: "confirmation",
|
||||
Prompt: &survey.Select{
|
||||
Message: "What's next?",
|
||||
Options: options,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := prompt.SurveyAsk(confirmQs, &confirmAnswers)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
switch options[confirmAnswers.Confirmation] {
|
||||
case submitLabel:
|
||||
return SubmitAction, nil
|
||||
case previewLabel:
|
||||
return PreviewAction, nil
|
||||
case metadataLabel:
|
||||
return MetadataAction, nil
|
||||
case cancelLabel:
|
||||
return CancelAction, nil
|
||||
default:
|
||||
return -1, fmt.Errorf("invalid index: %d", confirmAnswers.Confirmation)
|
||||
}
|
||||
}
|
||||
|
||||
func selectTemplate(nonLegacyTemplatePaths []string, legacyTemplatePath *string, metadataType metadataStateType) (string, error) {
|
||||
templateResponse := struct {
|
||||
Index int
|
||||
}{}
|
||||
templateNames := make([]string, 0, len(nonLegacyTemplatePaths))
|
||||
for _, p := range nonLegacyTemplatePaths {
|
||||
templateNames = append(templateNames, githubtemplate.ExtractName(p))
|
||||
}
|
||||
if metadataType == issueMetadata {
|
||||
templateNames = append(templateNames, "Open a blank issue")
|
||||
} else if metadataType == prMetadata {
|
||||
templateNames = append(templateNames, "Open a blank pull request")
|
||||
}
|
||||
|
||||
selectQs := []*survey.Question{
|
||||
{
|
||||
Name: "index",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Choose a template",
|
||||
Options: templateNames,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := prompt.SurveyAsk(selectQs, &templateResponse); err != nil {
|
||||
return "", fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
if templateResponse.Index == len(nonLegacyTemplatePaths) { // the user has selected the blank template
|
||||
if legacyTemplatePath != nil {
|
||||
templateContents := githubtemplate.ExtractContents(*legacyTemplatePath)
|
||||
return string(templateContents), nil
|
||||
} else {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
templateContents := githubtemplate.ExtractContents(nonLegacyTemplatePaths[templateResponse.Index])
|
||||
return string(templateContents), nil
|
||||
}
|
||||
|
||||
func titleBodySurvey(cmd *cobra.Command, issueState *issueMetadataState, apiClient *api.Client, repo ghrepo.Interface, providedTitle, providedBody string, defs defaults, nonLegacyTemplatePaths []string, legacyTemplatePath *string, allowReviewers, allowMetadata bool) error {
|
||||
editorCommand, err := determineEditor(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issueState.Title = defs.Title
|
||||
templateContents := ""
|
||||
|
||||
if providedBody == "" {
|
||||
if len(nonLegacyTemplatePaths) > 0 {
|
||||
var err error
|
||||
templateContents, err = selectTemplate(nonLegacyTemplatePaths, legacyTemplatePath, issueState.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
issueState.Body = templateContents
|
||||
} else if legacyTemplatePath != nil {
|
||||
templateContents = string(githubtemplate.ExtractContents(*legacyTemplatePath))
|
||||
issueState.Body = templateContents
|
||||
} else {
|
||||
issueState.Body = defs.Body
|
||||
}
|
||||
}
|
||||
|
||||
titleQuestion := &survey.Question{
|
||||
Name: "title",
|
||||
Prompt: &survey.Input{
|
||||
Message: "Title",
|
||||
Default: issueState.Title,
|
||||
},
|
||||
}
|
||||
bodyQuestion := &survey.Question{
|
||||
Name: "body",
|
||||
Prompt: &surveyext.GhEditor{
|
||||
BlankAllowed: true,
|
||||
EditorCommand: editorCommand,
|
||||
Editor: &survey.Editor{
|
||||
Message: "Body",
|
||||
FileName: "*.md",
|
||||
Default: issueState.Body,
|
||||
HideDefault: true,
|
||||
AppendDefault: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var qs []*survey.Question
|
||||
if providedTitle == "" {
|
||||
qs = append(qs, titleQuestion)
|
||||
}
|
||||
if providedBody == "" {
|
||||
qs = append(qs, bodyQuestion)
|
||||
}
|
||||
|
||||
err = prompt.SurveyAsk(qs, issueState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
if issueState.Body == "" {
|
||||
issueState.Body = templateContents
|
||||
}
|
||||
|
||||
allowPreview := !issueState.HasMetadata()
|
||||
confirmA, err := confirmSubmission(allowPreview, allowMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to confirm: %w", err)
|
||||
}
|
||||
|
||||
if confirmA == MetadataAction {
|
||||
isChosen := func(m string) bool {
|
||||
for _, c := range issueState.Metadata {
|
||||
if m == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
extraFieldsOptions := []string{}
|
||||
if allowReviewers {
|
||||
extraFieldsOptions = append(extraFieldsOptions, "Reviewers")
|
||||
}
|
||||
extraFieldsOptions = append(extraFieldsOptions, "Assignees", "Labels", "Projects", "Milestone")
|
||||
|
||||
err = prompt.SurveyAsk([]*survey.Question{
|
||||
{
|
||||
Name: "metadata",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "What would you like to add?",
|
||||
Options: extraFieldsOptions,
|
||||
},
|
||||
},
|
||||
}, issueState)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
|
||||
metadataInput := api.RepoMetadataInput{
|
||||
Reviewers: isChosen("Reviewers"),
|
||||
Assignees: isChosen("Assignees"),
|
||||
Labels: isChosen("Labels"),
|
||||
Projects: isChosen("Projects"),
|
||||
Milestones: isChosen("Milestone"),
|
||||
}
|
||||
s := utils.Spinner(cmd.OutOrStderr())
|
||||
utils.StartSpinner(s)
|
||||
issueState.MetadataResult, err = api.RepoMetadata(apiClient, repo, metadataInput)
|
||||
utils.StopSpinner(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching metadata options: %w", err)
|
||||
}
|
||||
|
||||
var users []string
|
||||
for _, u := range issueState.MetadataResult.AssignableUsers {
|
||||
users = append(users, u.Login)
|
||||
}
|
||||
var teams []string
|
||||
for _, t := range issueState.MetadataResult.Teams {
|
||||
teams = append(teams, fmt.Sprintf("%s/%s", repo.RepoOwner(), t.Slug))
|
||||
}
|
||||
var labels []string
|
||||
for _, l := range issueState.MetadataResult.Labels {
|
||||
labels = append(labels, l.Name)
|
||||
}
|
||||
var projects []string
|
||||
for _, l := range issueState.MetadataResult.Projects {
|
||||
projects = append(projects, l.Name)
|
||||
}
|
||||
milestones := []string{noMilestone}
|
||||
for _, m := range issueState.MetadataResult.Milestones {
|
||||
milestones = append(milestones, m.Title)
|
||||
}
|
||||
|
||||
type metadataValues struct {
|
||||
Reviewers []string
|
||||
Assignees []string
|
||||
Labels []string
|
||||
Projects []string
|
||||
Milestone string
|
||||
}
|
||||
var mqs []*survey.Question
|
||||
if isChosen("Reviewers") {
|
||||
if len(users) > 0 || len(teams) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "reviewers",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Reviewers",
|
||||
Options: append(users, teams...),
|
||||
Default: issueState.Reviewers,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
cmd.PrintErrln("warning: no available reviewers")
|
||||
}
|
||||
}
|
||||
if isChosen("Assignees") {
|
||||
if len(users) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "assignees",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Assignees",
|
||||
Options: users,
|
||||
Default: issueState.Assignees,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
cmd.PrintErrln("warning: no assignable users")
|
||||
}
|
||||
}
|
||||
if isChosen("Labels") {
|
||||
if len(labels) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "labels",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Labels",
|
||||
Options: labels,
|
||||
Default: issueState.Labels,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
cmd.PrintErrln("warning: no labels in the repository")
|
||||
}
|
||||
}
|
||||
if isChosen("Projects") {
|
||||
if len(projects) > 0 {
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "projects",
|
||||
Prompt: &survey.MultiSelect{
|
||||
Message: "Projects",
|
||||
Options: projects,
|
||||
Default: issueState.Projects,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
cmd.PrintErrln("warning: no projects to choose from")
|
||||
}
|
||||
}
|
||||
if isChosen("Milestone") {
|
||||
if len(milestones) > 1 {
|
||||
var milestoneDefault string
|
||||
if len(issueState.Milestones) > 0 {
|
||||
milestoneDefault = issueState.Milestones[0]
|
||||
}
|
||||
mqs = append(mqs, &survey.Question{
|
||||
Name: "milestone",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Milestone",
|
||||
Options: milestones,
|
||||
Default: milestoneDefault,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
cmd.PrintErrln("warning: no milestones in the repository")
|
||||
}
|
||||
}
|
||||
values := metadataValues{}
|
||||
err = prompt.SurveyAsk(mqs, &values, survey.WithKeepFilter(true))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prompt: %w", err)
|
||||
}
|
||||
issueState.Reviewers = values.Reviewers
|
||||
issueState.Assignees = values.Assignees
|
||||
issueState.Labels = values.Labels
|
||||
issueState.Projects = values.Projects
|
||||
if values.Milestone != "" && values.Milestone != noMilestone {
|
||||
issueState.Milestones = []string{values.Milestone}
|
||||
}
|
||||
|
||||
allowPreview = !issueState.HasMetadata()
|
||||
allowMetadata = false
|
||||
confirmA, err = confirmSubmission(allowPreview, allowMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to confirm: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
issueState.Action = confirmA
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue