Merge branch 'trunk' of github.com:cli/cli into interactive-gist-view
This commit is contained in:
commit
0833bdc6b4
14 changed files with 171 additions and 21 deletions
|
|
@ -163,12 +163,19 @@ func main() {
|
|||
|
||||
newRelease := <-updateMessageChan
|
||||
if newRelease != nil {
|
||||
ghExe, _ := os.Executable()
|
||||
isHomebrew := false
|
||||
if ghExe, err := os.Executable(); err == nil {
|
||||
isHomebrew = isUnderHomebrew(ghExe)
|
||||
}
|
||||
if isHomebrew && isRecentRelease(newRelease.PublishedAt) {
|
||||
// do not notify Homebrew users before the version bump had a chance to get merged into homebrew-core
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(stderr, "\n\n%s %s → %s\n",
|
||||
ansi.Color("A new release of gh is available:", "yellow"),
|
||||
ansi.Color(buildVersion, "cyan"),
|
||||
ansi.Color(newRelease.Version, "cyan"))
|
||||
if suggestBrewUpgrade(newRelease, ghExe) {
|
||||
if isHomebrew {
|
||||
fmt.Fprintf(stderr, "To upgrade, run: %s\n", "brew update && brew upgrade gh")
|
||||
}
|
||||
fmt.Fprintf(stderr, "%s\n\n",
|
||||
|
|
@ -265,13 +272,12 @@ func apiVerboseLog() api.ClientOption {
|
|||
return api.VerboseLog(colorable.NewColorable(os.Stderr), logTraffic, colorize)
|
||||
}
|
||||
|
||||
// Suggest to `brew upgrade gh` only if gh was found under homebrew prefix and when the release was
|
||||
// published over 24h ago, allowing homebrew-core ample time to merge the formula bump.
|
||||
func suggestBrewUpgrade(rel *update.ReleaseInfo, ghBinary string) bool {
|
||||
if rel.PublishedAt.IsZero() || time.Since(rel.PublishedAt) < time.Duration(time.Hour*24) {
|
||||
return false
|
||||
}
|
||||
func isRecentRelease(publishedAt time.Time) bool {
|
||||
return !publishedAt.IsZero() && time.Since(publishedAt) < time.Hour*24
|
||||
}
|
||||
|
||||
// Check whether the gh binary was found under the Homebrew prefix
|
||||
func isUnderHomebrew(ghBinary string) bool {
|
||||
brewExe, err := safeexec.LookPath("brew")
|
||||
if err != nil {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const tokenUser = "x-access-token"
|
||||
|
||||
type config interface {
|
||||
Get(string, string) (string, error)
|
||||
GetWithSource(string, string) (string, string, error)
|
||||
}
|
||||
|
||||
type CredentialOptions struct {
|
||||
|
|
@ -98,13 +100,19 @@ func helperRun(opts *CredentialOptions) error {
|
|||
return err
|
||||
}
|
||||
|
||||
gotUser, _ := cfg.Get(wants["host"], "user")
|
||||
gotToken, _ := cfg.Get(wants["host"], "oauth_token")
|
||||
var gotUser string
|
||||
gotToken, source, _ := cfg.GetWithSource(wants["host"], "oauth_token")
|
||||
if strings.HasSuffix(source, "_TOKEN") {
|
||||
gotUser = tokenUser
|
||||
} else {
|
||||
gotUser, _, _ = cfg.GetWithSource(wants["host"], "user")
|
||||
}
|
||||
|
||||
if gotUser == "" || gotToken == "" {
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
if wants["username"] != "" && !strings.EqualFold(wants["username"], gotUser) {
|
||||
if wants["username"] != "" && gotUser != tokenUser && !strings.EqualFold(wants["username"], gotUser) {
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import (
|
|||
|
||||
type tinyConfig map[string]string
|
||||
|
||||
func (c tinyConfig) Get(host, key string) (string, error) {
|
||||
return c[fmt.Sprintf("%s:%s", host, key)], nil
|
||||
func (c tinyConfig) GetWithSource(host, key string) (string, string, error) {
|
||||
return c[fmt.Sprintf("%s:%s", host, key)], c["_source"], nil
|
||||
}
|
||||
|
||||
func Test_helperRun(t *testing.T) {
|
||||
|
|
@ -29,6 +29,7 @@ func Test_helperRun(t *testing.T) {
|
|||
Operation: "get",
|
||||
Config: func() (config, error) {
|
||||
return tinyConfig{
|
||||
"_source": "/Users/monalisa/.config/gh/hosts.yml",
|
||||
"example.com:user": "monalisa",
|
||||
"example.com:oauth_token": "OTOKEN",
|
||||
}, nil
|
||||
|
|
@ -53,6 +54,7 @@ func Test_helperRun(t *testing.T) {
|
|||
Operation: "get",
|
||||
Config: func() (config, error) {
|
||||
return tinyConfig{
|
||||
"_source": "/Users/monalisa/.config/gh/hosts.yml",
|
||||
"example.com:user": "monalisa",
|
||||
"example.com:oauth_token": "OTOKEN",
|
||||
}, nil
|
||||
|
|
@ -78,6 +80,7 @@ func Test_helperRun(t *testing.T) {
|
|||
Operation: "get",
|
||||
Config: func() (config, error) {
|
||||
return tinyConfig{
|
||||
"_source": "/Users/monalisa/.config/gh/hosts.yml",
|
||||
"example.com:user": "monalisa",
|
||||
"example.com:oauth_token": "OTOKEN",
|
||||
}, nil
|
||||
|
|
@ -101,6 +104,7 @@ func Test_helperRun(t *testing.T) {
|
|||
Operation: "get",
|
||||
Config: func() (config, error) {
|
||||
return tinyConfig{
|
||||
"_source": "/Users/monalisa/.config/gh/hosts.yml",
|
||||
"example.com:user": "monalisa",
|
||||
}, nil
|
||||
},
|
||||
|
|
@ -119,6 +123,7 @@ func Test_helperRun(t *testing.T) {
|
|||
Operation: "get",
|
||||
Config: func() (config, error) {
|
||||
return tinyConfig{
|
||||
"_source": "/Users/monalisa/.config/gh/hosts.yml",
|
||||
"example.com:user": "monalisa",
|
||||
"example.com:oauth_token": "OTOKEN",
|
||||
}, nil
|
||||
|
|
@ -133,6 +138,31 @@ func Test_helperRun(t *testing.T) {
|
|||
wantStdout: "",
|
||||
wantStderr: "",
|
||||
},
|
||||
{
|
||||
name: "token from env",
|
||||
opts: CredentialOptions{
|
||||
Operation: "get",
|
||||
Config: func() (config, error) {
|
||||
return tinyConfig{
|
||||
"_source": "GITHUB_ENTERPRISE_TOKEN",
|
||||
"example.com:oauth_token": "OTOKEN",
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
input: heredoc.Doc(`
|
||||
protocol=https
|
||||
host=example.com
|
||||
username=hubot
|
||||
`),
|
||||
wantErr: false,
|
||||
wantStdout: heredoc.Doc(`
|
||||
protocol=https
|
||||
host=example.com
|
||||
username=x-access-token
|
||||
password=OTOKEN
|
||||
`),
|
||||
wantStderr: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/authflow"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/pkg/prompt"
|
||||
)
|
||||
|
|
@ -125,7 +126,7 @@ func Login(opts *LoginOptions) error {
|
|||
fmt.Fprint(opts.IO.ErrOut, heredoc.Docf(`
|
||||
Tip: you can generate a Personal Access Token here https://%s/settings/tokens
|
||||
The minimum required scopes are %s.
|
||||
`, hostname, scopesSentence(minimumScopes)))
|
||||
`, hostname, scopesSentence(minimumScopes, ghinstance.IsEnterprise(hostname))))
|
||||
|
||||
err := prompt.SurveyAskOne(&survey.Password{
|
||||
Message: "Paste your authentication token:",
|
||||
|
|
@ -193,11 +194,14 @@ func Login(opts *LoginOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func scopesSentence(scopes []string) string {
|
||||
func scopesSentence(scopes []string, isEnterprise bool) string {
|
||||
quoted := make([]string, len(scopes))
|
||||
for i, s := range scopes {
|
||||
quoted[i] = fmt.Sprintf("'%s'", s)
|
||||
if s == "workflow" && isEnterprise {
|
||||
// remove when GHE 2.x reaches EOL
|
||||
quoted[i] += " (GHE 3.0+)"
|
||||
}
|
||||
}
|
||||
// TODO: insert an "and" before the last element
|
||||
return strings.Join(quoted, ", ")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,3 +101,55 @@ func TestLogin_ssh(t *testing.T) {
|
|||
assert.Equal(t, "ATOKEN", cfg["example.com:oauth_token"])
|
||||
assert.Equal(t, "ssh", cfg["example.com:git_protocol"])
|
||||
}
|
||||
|
||||
func Test_scopesSentence(t *testing.T) {
|
||||
type args struct {
|
||||
scopes []string
|
||||
isEnterprise bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "basic scopes",
|
||||
args: args{
|
||||
scopes: []string{"repo", "read:org"},
|
||||
isEnterprise: false,
|
||||
},
|
||||
want: "'repo', 'read:org'",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
scopes: []string(nil),
|
||||
isEnterprise: false,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "workflow scope for dotcom",
|
||||
args: args{
|
||||
scopes: []string{"repo", "workflow"},
|
||||
isEnterprise: false,
|
||||
},
|
||||
want: "'repo', 'workflow'",
|
||||
},
|
||||
{
|
||||
name: "workflow scope for GHE",
|
||||
args: args{
|
||||
scopes: []string{"repo", "workflow"},
|
||||
isEnterprise: true,
|
||||
},
|
||||
want: "'repo', 'workflow' (GHE 3.0+)",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := scopesSentence(tt.args.scopes, tt.args.isEnterprise); got != tt.want {
|
||||
t.Errorf("scopesSentence() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func HasMinimumScopes(httpClient httpClient, hostname, authToken string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Autorization", "token "+authToken)
|
||||
req.Header.Set("Authorization", "token "+authToken)
|
||||
|
||||
res, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,9 @@ func Test_HasMinimumScopes(t *testing.T) {
|
|||
fakehttp := &httpmock.Registry{}
|
||||
defer fakehttp.Verify(t)
|
||||
|
||||
var gotAuthorization string
|
||||
fakehttp.Register(httpmock.REST("GET", ""), func(req *http.Request) (*http.Response, error) {
|
||||
gotAuthorization = req.Header.Get("authorization")
|
||||
return &http.Response{
|
||||
Request: req,
|
||||
StatusCode: 200,
|
||||
|
|
@ -70,6 +72,7 @@ func Test_HasMinimumScopes(t *testing.T) {
|
|||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, gotAuthorization, "token ATOKEN")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
|
|||
cmd := &cobra.Command{
|
||||
Use: "delete {<id> | <url>}",
|
||||
Short: "Delete a gist",
|
||||
Args: cmdutil.MinimumArgs(1, "cannot delete: gist argument required"),
|
||||
Args: cmdutil.ExactArgs(1, "cannot delete: gist argument required"),
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
opts.Selector = args[0]
|
||||
if runF != nil {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
cmd := &cobra.Command{
|
||||
Use: "edit {<id> | <url>}",
|
||||
Short: "Edit one of your gists",
|
||||
Args: cmdutil.MinimumArgs(1, "cannot edit: gist argument required"),
|
||||
Args: cmdutil.ExactArgs(1, "cannot edit: gist argument required"),
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
opts.Selector = args[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr
|
|||
cmd := &cobra.Command{
|
||||
Use: "checkout {<number> | <url> | <branch>}",
|
||||
Short: "Check out a pull request in git",
|
||||
Args: cmdutil.MinimumArgs(1, "argument required"),
|
||||
Args: cmdutil.ExactArgs(1, "argument required"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// support `-R, --repo` override
|
||||
opts.BaseRepo = f.BaseRepo
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
if flags.Changed("body") {
|
||||
opts.Editable.Body.Edited = true
|
||||
}
|
||||
if flags.Changed("base") {
|
||||
opts.Editable.Base.Edited = true
|
||||
}
|
||||
if flags.Changed("add-reviewer") || flags.Changed("remove-reviewer") {
|
||||
opts.Editable.Reviewers.Edited = true
|
||||
}
|
||||
|
|
@ -104,6 +107,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
|
|||
|
||||
cmd.Flags().StringVarP(&opts.Editable.Title.Value, "title", "t", "", "Set the new title.")
|
||||
cmd.Flags().StringVarP(&opts.Editable.Body.Value, "body", "b", "", "Set the new body.")
|
||||
cmd.Flags().StringVarP(&opts.Editable.Base.Value, "base", "B", "", "Change the base `branch` for this pull request")
|
||||
cmd.Flags().StringSliceVar(&opts.Editable.Reviewers.Add, "add-reviewer", nil, "Add reviewers by their `login`.")
|
||||
cmd.Flags().StringSliceVar(&opts.Editable.Reviewers.Remove, "remove-reviewer", nil, "Remove reviewers by their `login`.")
|
||||
cmd.Flags().StringSliceVar(&opts.Editable.Assignees.Add, "add-assignee", nil, "Add assigned users by their `login`. Use \"@me\" to assign yourself.")
|
||||
|
|
@ -133,6 +137,7 @@ func editRun(opts *EditOptions) error {
|
|||
editable.Reviewers.Allowed = true
|
||||
editable.Title.Default = pr.Title
|
||||
editable.Body.Default = pr.Body
|
||||
editable.Base.Default = pr.BaseRefName
|
||||
editable.Reviewers.Default = pr.ReviewRequests.Logins()
|
||||
editable.Assignees.Default = pr.Assignees.Logins()
|
||||
editable.Labels.Default = pr.Labels.Names()
|
||||
|
|
@ -203,6 +208,9 @@ func updatePullRequest(client *api.Client, repo ghrepo.Interface, id string, edi
|
|||
return err
|
||||
}
|
||||
params.MilestoneID = ghId(milestoneId)
|
||||
if editable.Base.Edited {
|
||||
params.BaseRefName = ghString(&editable.Base.Value)
|
||||
}
|
||||
err = api.UpdatePullRequest(client, repo, params)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -65,6 +65,20 @@ func TestNewCmdEdit(t *testing.T) {
|
|||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "base flag",
|
||||
input: "23 --base base-branch-name",
|
||||
output: EditOptions{
|
||||
SelectorArg: "23",
|
||||
Editable: shared.Editable{
|
||||
Base: shared.EditableString{
|
||||
Value: "base-branch-name",
|
||||
Edited: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantsErr: false,
|
||||
},
|
||||
{
|
||||
name: "add-reviewer flag",
|
||||
input: "23 --add-reviewer monalisa,owner/core",
|
||||
|
|
@ -254,6 +268,10 @@ func Test_editRun(t *testing.T) {
|
|||
Value: "new body",
|
||||
Edited: true,
|
||||
},
|
||||
Base: shared.EditableString{
|
||||
Value: "base-branch-name",
|
||||
Edited: true,
|
||||
},
|
||||
Reviewers: shared.EditableSlice{
|
||||
Add: []string{"OWNER/core", "OWNER/external", "monalisa", "hubot"},
|
||||
Remove: []string{"dependabot"},
|
||||
|
|
@ -303,6 +321,10 @@ func Test_editRun(t *testing.T) {
|
|||
Value: "new body",
|
||||
Edited: true,
|
||||
},
|
||||
Base: shared.EditableString{
|
||||
Value: "base-branch-name",
|
||||
Edited: true,
|
||||
},
|
||||
Assignees: shared.EditableSlice{
|
||||
Add: []string{"monalisa", "hubot"},
|
||||
Remove: []string{"octocat"},
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
type Editable struct {
|
||||
Title EditableString
|
||||
Body EditableString
|
||||
Base EditableString
|
||||
Reviewers EditableSlice
|
||||
Assignees EditableSlice
|
||||
Labels EditableSlice
|
||||
|
|
@ -42,6 +43,7 @@ type EditableSlice struct {
|
|||
func (e Editable) Dirty() bool {
|
||||
return e.Title.Edited ||
|
||||
e.Body.Edited ||
|
||||
e.Base.Edited ||
|
||||
e.Reviewers.Edited ||
|
||||
e.Assignees.Edited ||
|
||||
e.Labels.Edited ||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,21 @@ func MinimumArgs(n int, msg string) cobra.PositionalArgs {
|
|||
}
|
||||
}
|
||||
|
||||
func ExactArgs(n int, msg string) cobra.PositionalArgs {
|
||||
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > n {
|
||||
return &FlagError{Err: errors.New("too many arguments")}
|
||||
}
|
||||
|
||||
if len(args) < n {
|
||||
return &FlagError{Err: errors.New(msg)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func NoArgsQuoteReminder(cmd *cobra.Command, args []string) error {
|
||||
if len(args) < 1 {
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue