diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 2dab75d5b..40ab1acb4 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -23,4 +23,4 @@ jobs: - name: Build run: | go test ./... - go build -v . + go build -v ./cmd/gh diff --git a/.goreleaser.yml b/.goreleaser.yml index b3ee2336c..c8ffa8997 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -8,6 +8,7 @@ before: - go mod tidy builds: - binary: bin/gh + main: ./cmd/gh ldflags: - -s -w -X github.com/github/gh-cli/command.Version={{.Version}} -X github.com/github/gh-cli/command.BuildDate={{time "2006-01-02"}} - -X github.com/github/gh-cli/context.oauthClientID={{.Env.GH_OAUTH_CLIENT_ID}} diff --git a/Makefile b/Makefile index d17f749bf..d3cb03449 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ ifdef GH_OAUTH_CLIENT_SECRET endif bin/gh: $(BUILD_FILES) - @go build -ldflags "$(LDFLAGS)" -o "$@" + @go build -ldflags "$(LDFLAGS)" -o "$@" ./cmd/gh test: go test ./... diff --git a/main.go b/cmd/gh/main.go similarity index 66% rename from main.go rename to cmd/gh/main.go index 726b50784..15925a6a9 100644 --- a/main.go +++ b/cmd/gh/main.go @@ -1,7 +1,10 @@ package main import ( + "errors" "fmt" + "io" + "net" "os" "path" "strings" @@ -12,6 +15,7 @@ import ( "github.com/github/gh-cli/utils" "github.com/mattn/go-isatty" "github.com/mgutz/ansi" + "github.com/spf13/cobra" ) var updaterEnabled = "" @@ -24,12 +28,10 @@ func main() { updateMessageChan <- rel }() + hasDebug := os.Getenv("DEBUG") != "" + if cmd, err := command.RootCmd.ExecuteC(); err != nil { - fmt.Fprintln(os.Stderr, err) - _, isFlagError := err.(command.FlagError) - if isFlagError || strings.HasPrefix(err.Error(), "unknown command ") { - fmt.Fprintln(os.Stderr, cmd.UsageString()) - } + printError(os.Stderr, err, cmd, hasDebug) os.Exit(1) } @@ -46,6 +48,28 @@ func main() { } } +func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) { + var dnsError *net.DNSError + if errors.As(err, &dnsError) { + fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name) + if debug { + fmt.Fprintln(out, dnsError) + } + fmt.Fprintln(out, "check your internet connection or githubstatus.com") + return + } + + fmt.Fprintln(out, err) + + var flagError *command.FlagError + if errors.As(err, &flagError) || strings.HasPrefix(err.Error(), "unknown command ") { + if !strings.HasSuffix(err.Error(), "\n") { + fmt.Fprintln(out) + } + fmt.Fprintln(out, cmd.UsageString()) + } +} + func shouldCheckForUpdate() bool { errFd := os.Stderr.Fd() return updaterEnabled != "" && (isatty.IsTerminal(errFd) || isatty.IsCygwinTerminal(errFd)) diff --git a/cmd/gh/main_test.go b/cmd/gh/main_test.go new file mode 100644 index 000000000..3a62f6470 --- /dev/null +++ b/cmd/gh/main_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "net" + "testing" + + "github.com/github/gh-cli/command" + "github.com/spf13/cobra" +) + +func Test_printError(t *testing.T) { + cmd := &cobra.Command{} + + type args struct { + err error + cmd *cobra.Command + debug bool + } + tests := []struct { + name string + args args + wantOut string + }{ + { + name: "generic error", + args: args{ + err: errors.New("the app exploded"), + cmd: nil, + debug: false, + }, + wantOut: "the app exploded\n", + }, + { + name: "DNS error", + args: args{ + err: fmt.Errorf("DNS oopsie: %w", &net.DNSError{ + Name: "api.github.com", + }), + cmd: nil, + debug: false, + }, + wantOut: `error connecting to api.github.com +check your internet connection or githubstatus.com +`, + }, + { + name: "Cobra flag error", + args: args{ + err: &command.FlagError{Err: errors.New("unknown flag --foo")}, + cmd: cmd, + debug: false, + }, + wantOut: "unknown flag --foo\n\nUsage:\n\n", + }, + { + name: "unknown Cobra command error", + args: args{ + err: errors.New("unknown command foo"), + cmd: cmd, + debug: false, + }, + wantOut: "unknown command foo\n\nUsage:\n\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out := &bytes.Buffer{} + printError(out, tt.args.err, tt.args.cmd, tt.args.debug) + if gotOut := out.String(); gotOut != tt.wantOut { + t.Errorf("printError() = %q, want %q", gotOut, tt.wantOut) + } + }) + } +} diff --git a/command/issue.go b/command/issue.go index 52e441419..faf1418fa 100644 --- a/command/issue.go +++ b/command/issue.go @@ -47,7 +47,7 @@ var issueCmd = &cobra.Command{ An issue can be supplied as argument in any of the following formats: - by number, e.g. "123"; or -- by URL, e.g. "https://github.com///issues/123".`, +- by URL, e.g. "https://github.com/OWNER/REPO/issues/123".`, } var issueCreateCmd = &cobra.Command{ Use: "create", @@ -68,7 +68,7 @@ var issueViewCmd = &cobra.Command{ Use: "view { | | }", Args: func(cmd *cobra.Command, args []string) error { if len(args) < 1 { - return errors.New("requires an issue number as an argument") + return FlagError{errors.New("issue required as argument")} } return nil }, diff --git a/command/pr.go b/command/pr.go index bcb773b67..02e67814f 100644 --- a/command/pr.go +++ b/command/pr.go @@ -43,8 +43,8 @@ var prCmd = &cobra.Command{ A pull request can be supplied as argument in any of the following formats: - by number, e.g. "123"; -- by URL, e.g. "https://github.com///pull/123"; or -- by the name of its head branch, e.g. "patch-1" or ":patch-1".`, +- by URL, e.g. "https://github.com/OWNER/REPO/pull/123"; or +- by the name of its head branch, e.g. "patch-1" or "OWNER:patch-1".`, } var prCheckoutCmd = &cobra.Command{ Use: "checkout { | | }", @@ -68,9 +68,13 @@ var prStatusCmd = &cobra.Command{ RunE: prStatus, } var prViewCmd = &cobra.Command{ - Use: "view { | | }", + Use: "view [{ | | }]", Short: "View a pull request in the browser", - RunE: prView, + Long: `View a pull request specified by the argument in the browser. + +Without an argument, the pull request that belongs to the current +branch is opened.`, + RunE: prView, } func prStatus(cmd *cobra.Command, args []string) error { @@ -286,10 +290,6 @@ func prView(cmd *cobra.Command, args []string) error { } else { pr, err = api.PullRequestForBranch(apiClient, baseRepo, branchWithOwner) if err != nil { - var notFoundErr *api.NotFoundError - if errors.As(err, ¬FoundErr) { - return fmt.Errorf("%s. To open a specific pull request use the pull request's number as an argument", err) - } return err } diff --git a/command/pr_test.go b/command/pr_test.go index 5a324cab1..9c17c3d4f 100644 --- a/command/pr_test.go +++ b/command/pr_test.go @@ -359,7 +359,7 @@ func TestPRView_noResultsForBranch(t *testing.T) { defer restoreCmd() _, err := RunCommand(prViewCmd, "pr view") - if err == nil || err.Error() != `no open pull requests found for branch "blueberries". To open a specific pull request use the pull request's number as an argument` { + if err == nil || err.Error() != `no open pull requests found for branch "blueberries"` { t.Errorf("error running command `pr view`: %v", err) } diff --git a/command/root.go b/command/root.go index c945d02e8..be94146d8 100644 --- a/command/root.go +++ b/command/root.go @@ -35,13 +35,21 @@ func init() { // RootCmd.PersistentFlags().BoolP("verbose", "V", false, "enable verbose output") RootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { - return FlagError{err} + return &FlagError{Err: err} }) } // FlagError is the kind of error raised in flag processing type FlagError struct { - error + Err error +} + +func (fe FlagError) Error() string { + return fe.Err.Error() +} + +func (fe FlagError) Unwrap() error { + return fe.Err } // RootCmd is the entry point of command-line execution