Merge remote-tracking branch 'origin' into api-template

This commit is contained in:
Mislav Marohnić 2021-03-04 14:53:08 +01:00
commit 07cb5e9e17
20 changed files with 181 additions and 38 deletions

View file

@ -13,6 +13,7 @@ import (
"time"
surveyCore "github.com/AlecAivazis/survey/v2/core"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/build"
"github.com/cli/cli/internal/config"
@ -32,7 +33,21 @@ import (
var updaterEnabled = ""
type exitCode int
const (
exitOK exitCode = 0
exitError exitCode = 1
exitCancel exitCode = 2
exitAuth exitCode = 4
)
func main() {
code := mainRun()
os.Exit(int(code))
}
func mainRun() exitCode {
buildDate := build.Date
buildVersion := build.Version
@ -78,7 +93,7 @@ func main() {
cfg, err := cmdFactory.Config()
if err != nil {
fmt.Fprintf(stderr, "failed to read configuration: %s\n", err)
os.Exit(2)
return exitError
}
if prompt, _ := cfg.Get("", "prompt"); prompt == "disabled" {
@ -102,7 +117,7 @@ func main() {
expandedArgs, isShell, err = expand.ExpandAlias(cfg, os.Args, nil)
if err != nil {
fmt.Fprintf(stderr, "failed to process aliases: %s\n", err)
os.Exit(2)
return exitError
}
if hasDebug {
@ -113,7 +128,7 @@ func main() {
exe, err := safeexec.LookPath(expandedArgs[0])
if err != nil {
fmt.Fprintf(stderr, "failed to run external command: %s", err)
os.Exit(3)
return exitError
}
externalCmd := exec.Command(exe, expandedArgs[1:]...)
@ -125,14 +140,14 @@ func main() {
err = preparedCmd.Run()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
os.Exit(ee.ExitCode())
return exitCode(ee.ExitCode())
}
fmt.Fprintf(stderr, "failed to run external command: %s", err)
os.Exit(3)
return exitError
}
os.Exit(0)
return exitOK
}
}
@ -142,34 +157,41 @@ func main() {
fmt.Fprintln(stderr, cs.Bold("Welcome to GitHub CLI!"))
fmt.Fprintln(stderr)
fmt.Fprintln(stderr, "To authenticate, please run `gh auth login`.")
os.Exit(4)
return exitAuth
}
rootCmd.SetArgs(expandedArgs)
if cmd, err := rootCmd.ExecuteC(); err != nil {
if err == cmdutil.SilentError {
return exitError
} else if cmdutil.IsUserCancellation(err) {
if errors.Is(err, terminal.InterruptErr) {
// ensure the next shell prompt will start on its own line
fmt.Fprint(stderr, "\n")
}
return exitCancel
}
printError(stderr, err, cmd, hasDebug)
var httpErr api.HTTPError
if errors.As(err, &httpErr) && httpErr.StatusCode == 401 {
fmt.Println("hint: try authenticating with `gh auth login`")
fmt.Fprintln(stderr, "hint: try authenticating with `gh auth login`")
}
os.Exit(1)
return exitError
}
if root.HasFailed() {
os.Exit(1)
return exitError
}
newRelease := <-updateMessageChan
if newRelease != nil {
isHomebrew := false
if ghExe, err := os.Executable(); err == nil {
isHomebrew = isUnderHomebrew(ghExe)
}
isHomebrew := isUnderHomebrew(cmdFactory.Executable)
if isHomebrew && isRecentRelease(newRelease.PublishedAt) {
// do not notify Homebrew users before the version bump had a chance to get merged into homebrew-core
return
return exitOK
}
fmt.Fprintf(stderr, "\n\n%s %s → %s\n",
ansi.Color("A new release of gh is available:", "yellow"),
@ -181,13 +203,11 @@ func main() {
fmt.Fprintf(stderr, "%s\n\n",
ansi.Color(newRelease.URL, "yellow"))
}
return exitOK
}
func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
if err == cmdutil.SilentError {
return
}
var dnsError *net.DNSError
if errors.As(err, &dnsError) {
fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name)

View file

@ -38,6 +38,7 @@ type ApiOptions struct {
MagicFields []string
RawFields []string
RequestHeaders []string
Previews []string
ShowResponseHeaders bool
Paginate bool
Silent bool
@ -119,7 +120,10 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
$ gh api -X GET search/issues -f q='repo:cli/cli is:open remote'
# set a custom HTTP header
$ gh api -H 'Accept: application/vnd.github.XYZ-preview+json' ...
$ gh api -H 'Accept: application/vnd.github.v3.raw+json' ...
# opt into GitHub API previews
$ gh api --preview baptiste,nebula ...
# use a template for the output
$ gh api repos/:owner/:repo/issues --template \
@ -192,6 +196,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
cmd.Flags().StringArrayVarP(&opts.MagicFields, "field", "F", nil, "Add a typed parameter in `key=value` format")
cmd.Flags().StringArrayVarP(&opts.RawFields, "raw-field", "f", nil, "Add a string parameter in `key=value` format")
cmd.Flags().StringArrayVarP(&opts.RequestHeaders, "header", "H", nil, "Add a HTTP request header in `key:value` format")
cmd.Flags().StringSliceVarP(&opts.Previews, "preview", "p", nil, "Opt into GitHub API previews")
cmd.Flags().BoolVarP(&opts.ShowResponseHeaders, "include", "i", false, "Include HTTP response headers in the output")
cmd.Flags().BoolVar(&opts.Paginate, "paginate", false, "Make additional HTTP requests to fetch all pages of results")
cmd.Flags().StringVar(&opts.RequestInputFile, "input", "", "The `file` to use as body for the HTTP request")
@ -237,6 +242,10 @@ func apiRun(opts *ApiOptions) error {
}
}
if len(opts.Previews) > 0 {
requestHeaders = append(requestHeaders, "Accept: "+previewNamesToMIMETypes(opts.Previews))
}
httpClient, err := opts.HttpClient()
if err != nil {
return err
@ -555,3 +564,11 @@ func parseErrorResponse(r io.Reader, statusCode int) (io.Reader, string, error)
return bodyCopy, "", nil
}
func previewNamesToMIMETypes(names []string) string {
types := []string{fmt.Sprintf("application/vnd.github.%s-preview+json", names[0])}
for _, p := range names[1:] {
types = append(types, fmt.Sprintf("application/vnd.github.%s-preview", p))
}
return strings.Join(types, ", ")
}

View file

@ -913,6 +913,32 @@ func Test_fillPlaceholders(t *testing.T) {
}
}
func Test_previewNamesToMIMETypes(t *testing.T) {
tests := []struct {
name string
previews []string
want string
}{
{
name: "single",
previews: []string{"nebula"},
want: "application/vnd.github.nebula-preview+json",
},
{
name: "multiple",
previews: []string{"nebula", "baptiste", "squirrel-girl"},
want: "application/vnd.github.nebula-preview+json, application/vnd.github.baptiste-preview, application/vnd.github.squirrel-girl-preview",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := previewNamesToMIMETypes(tt.previews); got != tt.want {
t.Errorf("previewNamesToMIMETypes() = %q, want %q", got, tt.want)
}
})
}
}
func Test_processResponse_template(t *testing.T) {
io, _, stdout, stderr := iostreams.Test()

View file

@ -23,6 +23,8 @@ type LoginOptions struct {
Config func() (config.Config, error)
HttpClient func() (*http.Client, error)
MainExecutable string
Interactive bool
Hostname string
@ -36,6 +38,8 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
IO: f.IOStreams,
Config: f.Config,
HttpClient: f.HttpClient,
MainExecutable: f.Executable,
}
var tokenStdin bool
@ -189,6 +193,7 @@ func loginRun(opts *LoginOptions) error {
Interactive: opts.Interactive,
Web: opts.Web,
Scopes: opts.Scopes,
Executable: opts.MainExecutable,
})
}

View file

@ -19,6 +19,8 @@ type RefreshOptions struct {
IO *iostreams.IOStreams
Config func() (config.Config, error)
MainExecutable string
Hostname string
Scopes []string
AuthFlow func(config.Config, *iostreams.IOStreams, string, []string) error
@ -34,6 +36,7 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
_, err := authflow.AuthFlowWithConfig(cfg, io, hostname, "", scopes)
return err
},
MainExecutable: f.Executable,
}
cmd := &cobra.Command{

View file

@ -15,6 +15,8 @@ import (
)
type GitCredentialFlow struct {
Executable string
shouldSetup bool
helper string
scopes []string
@ -50,13 +52,26 @@ func (flow *GitCredentialFlow) ShouldSetup() bool {
}
func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error {
return GitCredentialSetup(hostname, username, authToken, flow.helper)
return flow.gitCredentialSetup(hostname, username, authToken)
}
func GitCredentialSetup(hostname, username, password, helper string) error {
if helper == "" {
func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password string) error {
if flow.helper == "" {
// first use a blank value to indicate to git we want to sever the chain of credential helpers
preConfigureCmd, err := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname), "")
if err != nil {
return err
}
if err = run.PrepareCmd(preConfigureCmd).Run(); err != nil {
return err
}
// use GitHub CLI as a credential helper (for this host only)
configureCmd, err := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname), "!gh auth git-credential")
configureCmd, err := git.GitCommand(
"config", "--global", "--add",
gitCredentialHelperKey(hostname),
fmt.Sprintf("!%s auth git-credential", shellQuote(flow.Executable)),
)
if err != nil {
return err
}
@ -124,3 +139,10 @@ func isOurCredentialHelper(cmd string) bool {
return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh"
}
func shellQuote(s string) string {
if strings.ContainsAny(s, " $") {
return "'" + s + "'"
}
return s
}

View file

@ -12,7 +12,12 @@ func TestGitCredentialSetup_configureExisting(t *testing.T) {
cs.Register(`git credential reject`, 0, "")
cs.Register(`git credential approve`, 0, "")
if err := GitCredentialSetup("example.com", "monalisa", "PASSWD", "osxkeychain"); err != nil {
f := GitCredentialFlow{
Executable: "gh",
helper: "osxkeychain",
}
if err := f.gitCredentialSetup("example.com", "monalisa", "PASSWD"); err != nil {
t.Errorf("GitCredentialSetup() error = %v", err)
}
}
@ -20,13 +25,29 @@ func TestGitCredentialSetup_configureExisting(t *testing.T) {
func TestGitCredentialSetup_setOurs(t *testing.T) {
cs, restoreRun := run.Stub()
defer restoreRun(t)
cs.Register(`git config --global credential\.https://example\.com\.helper`, 0, "", func(args []string) {
if val := args[len(args)-1]; val != "!gh auth git-credential" {
cs.Register(`git config --global credential\.`, 0, "", func(args []string) {
if key := args[len(args)-2]; key != "credential.https://example.com.helper" {
t.Errorf("git config key was %q", key)
}
if val := args[len(args)-1]; val != "" {
t.Errorf("global credential helper configured to %q", val)
}
})
cs.Register(`git config --global --add credential\.`, 0, "", func(args []string) {
if key := args[len(args)-2]; key != "credential.https://example.com.helper" {
t.Errorf("git config key was %q", key)
}
if val := args[len(args)-1]; val != "!/path/to/gh auth git-credential" {
t.Errorf("global credential helper configured to %q", val)
}
})
if err := GitCredentialSetup("example.com", "monalisa", "PASSWD", ""); err != nil {
f := GitCredentialFlow{
Executable: "/path/to/gh",
helper: "",
}
if err := f.gitCredentialSetup("example.com", "monalisa", "PASSWD"); err != nil {
t.Errorf("GitCredentialSetup() error = %v", err)
}
}

View file

@ -28,6 +28,7 @@ type LoginOptions struct {
Interactive bool
Web bool
Scopes []string
Executable string
sshContext sshContext
}
@ -56,7 +57,7 @@ func Login(opts *LoginOptions) error {
var additionalScopes []string
credentialFlow := &GitCredentialFlow{}
credentialFlow := &GitCredentialFlow{Executable: opts.Executable}
if opts.Interactive && gitProtocol == "https" {
if err := credentialFlow.Prompt(hostname); err != nil {
return err

View file

@ -44,6 +44,11 @@ func New(appVersion string) *cmdutil.Factory {
}
remotesFunc := rr.Resolver(hostOverride)
ghExecutable := "gh"
if exe, err := os.Executable(); err == nil {
ghExecutable = exe
}
return &cmdutil.Factory{
IOStreams: io,
Config: configFunc,
@ -70,5 +75,6 @@ func New(appVersion string) *cmdutil.Factory {
}
return currentBranch, nil
},
Executable: ghExecutable,
}
}

View file

@ -195,7 +195,7 @@ func editRun(opts *EditOptions) error {
case "Submit":
stop = true
case "Cancel":
return cmdutil.SilentError
return cmdutil.CancelError
}
if stop {

View file

@ -194,7 +194,7 @@ func Test_editRun(t *testing.T) {
as.StubOne("unix.md")
as.StubOne("Cancel")
},
wantErr: "SilentError",
wantErr: "CancelError",
gist: &shared.Gist{
ID: "1234",
Files: map[string]*shared.GistFile{

View file

@ -260,6 +260,7 @@ func createRun(opts *CreateOptions) (err error) {
if action == prShared.CancelAction {
fmt.Fprintln(opts.IO.ErrOut, "Discarding.")
err = cmdutil.CancelError
return
}
} else {

View file

@ -311,7 +311,8 @@ func createRun(opts *CreateOptions) (err error) {
if action == shared.CancelAction {
fmt.Fprintln(opts.IO.ErrOut, "Discarding.")
return nil
err = cmdutil.CancelError
return
}
err = handlePush(*opts, *ctx)
@ -553,7 +554,7 @@ func NewCreateContext(opts *CreateOptions) (*CreateContext, error) {
} else if pushOptions[selectedOption] == "Skip pushing the branch" {
isPushEnabled = false
} else if pushOptions[selectedOption] == "Cancel" {
return nil, cmdutil.SilentError
return nil, cmdutil.CancelError
} else {
// "Create a fork of ..."
if baseRepo.IsPrivate {

View file

@ -227,7 +227,7 @@ func mergeRun(opts *MergeOptions) error {
}
if action == shared.CancelAction {
fmt.Fprintln(opts.IO.ErrOut, "Cancelled.")
return cmdutil.SilentError
return cmdutil.CancelError
}
}

View file

@ -895,7 +895,7 @@ func TestPRMerge_interactiveCancelled(t *testing.T) {
as.StubOne("Cancel") // Confirm submit survey
output, err := runCommand(http, "blueberries", true, "")
if !errors.Is(err, cmdutil.SilentError) {
if !errors.Is(err, cmdutil.CancelError) {
t.Fatalf("got error %v", err)
}

View file

@ -5,6 +5,7 @@ import (
"fmt"
"os"
"github.com/cli/cli/pkg/cmdutil"
"github.com/cli/cli/pkg/iostreams"
)
@ -18,6 +19,11 @@ func PreserveInput(io *iostreams.IOStreams, state *IssueMetadataState, createErr
return
}
if cmdutil.IsUserCancellation(*createErr) {
// these errors are user-initiated cancellations
return
}
out := io.ErrOut
// this extra newline guards against appending to the end of a survey line

View file

@ -256,7 +256,7 @@ func createRun(opts *CreateOptions) error {
case "Save as draft":
opts.Draft = true
case "Cancel":
return cmdutil.SilentError
return cmdutil.CancelError
default:
return fmt.Errorf("invalid action: %v", opts.SubmitAction)
}

View file

@ -78,7 +78,7 @@ func deleteRun(opts *DeleteOptions) error {
}
if !confirmed {
return cmdutil.SilentError
return cmdutil.CancelError
}
}

View file

@ -1,6 +1,10 @@
package cmdutil
import "errors"
import (
"errors"
"github.com/AlecAivazis/survey/v2/terminal"
)
// FlagError is the kind of error raised in flag processing
type FlagError struct {
@ -17,3 +21,10 @@ func (fe FlagError) Unwrap() error {
// SilentError is an error that triggers exit code 1 without any error messaging
var SilentError = errors.New("SilentError")
// CancelError signals user-initiated cancellation
var CancelError = errors.New("CancelError")
func IsUserCancellation(err error) bool {
return errors.Is(err, CancelError) || errors.Is(err, terminal.InterruptErr)
}

View file

@ -16,4 +16,7 @@ type Factory struct {
Remotes func() (context.Remotes, error)
Config func() (config.Config, error)
Branch func() (string, error)
// Executable is the path to the currently invoked gh binary
Executable string
}