X Tutup
Skip to content

Commit 07cb5e9

Browse files
committed
Merge remote-tracking branch 'origin' into api-template
2 parents bf97c6e + aa5cf6c commit 07cb5e9

File tree

20 files changed

+181
-38
lines changed

20 files changed

+181
-38
lines changed

cmd/gh/main.go

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"time"
1414

1515
surveyCore "github.com/AlecAivazis/survey/v2/core"
16+
"github.com/AlecAivazis/survey/v2/terminal"
1617
"github.com/cli/cli/api"
1718
"github.com/cli/cli/internal/build"
1819
"github.com/cli/cli/internal/config"
@@ -32,7 +33,21 @@ import (
3233

3334
var updaterEnabled = ""
3435

36+
type exitCode int
37+
38+
const (
39+
exitOK exitCode = 0
40+
exitError exitCode = 1
41+
exitCancel exitCode = 2
42+
exitAuth exitCode = 4
43+
)
44+
3545
func main() {
46+
code := mainRun()
47+
os.Exit(int(code))
48+
}
49+
50+
func mainRun() exitCode {
3651
buildDate := build.Date
3752
buildVersion := build.Version
3853

@@ -78,7 +93,7 @@ func main() {
7893
cfg, err := cmdFactory.Config()
7994
if err != nil {
8095
fmt.Fprintf(stderr, "failed to read configuration: %s\n", err)
81-
os.Exit(2)
96+
return exitError
8297
}
8398

8499
if prompt, _ := cfg.Get("", "prompt"); prompt == "disabled" {
@@ -102,7 +117,7 @@ func main() {
102117
expandedArgs, isShell, err = expand.ExpandAlias(cfg, os.Args, nil)
103118
if err != nil {
104119
fmt.Fprintf(stderr, "failed to process aliases: %s\n", err)
105-
os.Exit(2)
120+
return exitError
106121
}
107122

108123
if hasDebug {
@@ -113,7 +128,7 @@ func main() {
113128
exe, err := safeexec.LookPath(expandedArgs[0])
114129
if err != nil {
115130
fmt.Fprintf(stderr, "failed to run external command: %s", err)
116-
os.Exit(3)
131+
return exitError
117132
}
118133

119134
externalCmd := exec.Command(exe, expandedArgs[1:]...)
@@ -125,14 +140,14 @@ func main() {
125140
err = preparedCmd.Run()
126141
if err != nil {
127142
if ee, ok := err.(*exec.ExitError); ok {
128-
os.Exit(ee.ExitCode())
143+
return exitCode(ee.ExitCode())
129144
}
130145

131146
fmt.Fprintf(stderr, "failed to run external command: %s", err)
132-
os.Exit(3)
147+
return exitError
133148
}
134149

135-
os.Exit(0)
150+
return exitOK
136151
}
137152
}
138153

@@ -142,34 +157,41 @@ func main() {
142157
fmt.Fprintln(stderr, cs.Bold("Welcome to GitHub CLI!"))
143158
fmt.Fprintln(stderr)
144159
fmt.Fprintln(stderr, "To authenticate, please run `gh auth login`.")
145-
os.Exit(4)
160+
return exitAuth
146161
}
147162

148163
rootCmd.SetArgs(expandedArgs)
149164

150165
if cmd, err := rootCmd.ExecuteC(); err != nil {
166+
if err == cmdutil.SilentError {
167+
return exitError
168+
} else if cmdutil.IsUserCancellation(err) {
169+
if errors.Is(err, terminal.InterruptErr) {
170+
// ensure the next shell prompt will start on its own line
171+
fmt.Fprint(stderr, "\n")
172+
}
173+
return exitCancel
174+
}
175+
151176
printError(stderr, err, cmd, hasDebug)
152177

153178
var httpErr api.HTTPError
154179
if errors.As(err, &httpErr) && httpErr.StatusCode == 401 {
155-
fmt.Println("hint: try authenticating with `gh auth login`")
180+
fmt.Fprintln(stderr, "hint: try authenticating with `gh auth login`")
156181
}
157182

158-
os.Exit(1)
183+
return exitError
159184
}
160185
if root.HasFailed() {
161-
os.Exit(1)
186+
return exitError
162187
}
163188

164189
newRelease := <-updateMessageChan
165190
if newRelease != nil {
166-
isHomebrew := false
167-
if ghExe, err := os.Executable(); err == nil {
168-
isHomebrew = isUnderHomebrew(ghExe)
169-
}
191+
isHomebrew := isUnderHomebrew(cmdFactory.Executable)
170192
if isHomebrew && isRecentRelease(newRelease.PublishedAt) {
171193
// do not notify Homebrew users before the version bump had a chance to get merged into homebrew-core
172-
return
194+
return exitOK
173195
}
174196
fmt.Fprintf(stderr, "\n\n%s %s → %s\n",
175197
ansi.Color("A new release of gh is available:", "yellow"),
@@ -181,13 +203,11 @@ func main() {
181203
fmt.Fprintf(stderr, "%s\n\n",
182204
ansi.Color(newRelease.URL, "yellow"))
183205
}
206+
207+
return exitOK
184208
}
185209

186210
func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
187-
if err == cmdutil.SilentError {
188-
return
189-
}
190-
191211
var dnsError *net.DNSError
192212
if errors.As(err, &dnsError) {
193213
fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name)

pkg/cmd/api/api.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type ApiOptions struct {
3838
MagicFields []string
3939
RawFields []string
4040
RequestHeaders []string
41+
Previews []string
4142
ShowResponseHeaders bool
4243
Paginate bool
4344
Silent bool
@@ -119,7 +120,10 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
119120
$ gh api -X GET search/issues -f q='repo:cli/cli is:open remote'
120121
121122
# set a custom HTTP header
122-
$ gh api -H 'Accept: application/vnd.github.XYZ-preview+json' ...
123+
$ gh api -H 'Accept: application/vnd.github.v3.raw+json' ...
124+
125+
# opt into GitHub API previews
126+
$ gh api --preview baptiste,nebula ...
123127
124128
# use a template for the output
125129
$ gh api repos/:owner/:repo/issues --template \
@@ -192,6 +196,7 @@ func NewCmdApi(f *cmdutil.Factory, runF func(*ApiOptions) error) *cobra.Command
192196
cmd.Flags().StringArrayVarP(&opts.MagicFields, "field", "F", nil, "Add a typed parameter in `key=value` format")
193197
cmd.Flags().StringArrayVarP(&opts.RawFields, "raw-field", "f", nil, "Add a string parameter in `key=value` format")
194198
cmd.Flags().StringArrayVarP(&opts.RequestHeaders, "header", "H", nil, "Add a HTTP request header in `key:value` format")
199+
cmd.Flags().StringSliceVarP(&opts.Previews, "preview", "p", nil, "Opt into GitHub API previews")
195200
cmd.Flags().BoolVarP(&opts.ShowResponseHeaders, "include", "i", false, "Include HTTP response headers in the output")
196201
cmd.Flags().BoolVar(&opts.Paginate, "paginate", false, "Make additional HTTP requests to fetch all pages of results")
197202
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 {
237242
}
238243
}
239244

245+
if len(opts.Previews) > 0 {
246+
requestHeaders = append(requestHeaders, "Accept: "+previewNamesToMIMETypes(opts.Previews))
247+
}
248+
240249
httpClient, err := opts.HttpClient()
241250
if err != nil {
242251
return err
@@ -555,3 +564,11 @@ func parseErrorResponse(r io.Reader, statusCode int) (io.Reader, string, error)
555564

556565
return bodyCopy, "", nil
557566
}
567+
568+
func previewNamesToMIMETypes(names []string) string {
569+
types := []string{fmt.Sprintf("application/vnd.github.%s-preview+json", names[0])}
570+
for _, p := range names[1:] {
571+
types = append(types, fmt.Sprintf("application/vnd.github.%s-preview", p))
572+
}
573+
return strings.Join(types, ", ")
574+
}

pkg/cmd/api/api_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,32 @@ func Test_fillPlaceholders(t *testing.T) {
913913
}
914914
}
915915

916+
func Test_previewNamesToMIMETypes(t *testing.T) {
917+
tests := []struct {
918+
name string
919+
previews []string
920+
want string
921+
}{
922+
{
923+
name: "single",
924+
previews: []string{"nebula"},
925+
want: "application/vnd.github.nebula-preview+json",
926+
},
927+
{
928+
name: "multiple",
929+
previews: []string{"nebula", "baptiste", "squirrel-girl"},
930+
want: "application/vnd.github.nebula-preview+json, application/vnd.github.baptiste-preview, application/vnd.github.squirrel-girl-preview",
931+
},
932+
}
933+
for _, tt := range tests {
934+
t.Run(tt.name, func(t *testing.T) {
935+
if got := previewNamesToMIMETypes(tt.previews); got != tt.want {
936+
t.Errorf("previewNamesToMIMETypes() = %q, want %q", got, tt.want)
937+
}
938+
})
939+
}
940+
}
941+
916942
func Test_processResponse_template(t *testing.T) {
917943
io, _, stdout, stderr := iostreams.Test()
918944

pkg/cmd/auth/login/login.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ type LoginOptions struct {
2323
Config func() (config.Config, error)
2424
HttpClient func() (*http.Client, error)
2525

26+
MainExecutable string
27+
2628
Interactive bool
2729

2830
Hostname string
@@ -36,6 +38,8 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
3638
IO: f.IOStreams,
3739
Config: f.Config,
3840
HttpClient: f.HttpClient,
41+
42+
MainExecutable: f.Executable,
3943
}
4044

4145
var tokenStdin bool
@@ -189,6 +193,7 @@ func loginRun(opts *LoginOptions) error {
189193
Interactive: opts.Interactive,
190194
Web: opts.Web,
191195
Scopes: opts.Scopes,
196+
Executable: opts.MainExecutable,
192197
})
193198
}
194199

pkg/cmd/auth/refresh/refresh.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type RefreshOptions struct {
1919
IO *iostreams.IOStreams
2020
Config func() (config.Config, error)
2121

22+
MainExecutable string
23+
2224
Hostname string
2325
Scopes []string
2426
AuthFlow func(config.Config, *iostreams.IOStreams, string, []string) error
@@ -34,6 +36,7 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
3436
_, err := authflow.AuthFlowWithConfig(cfg, io, hostname, "", scopes)
3537
return err
3638
},
39+
MainExecutable: f.Executable,
3740
}
3841

3942
cmd := &cobra.Command{

pkg/cmd/auth/shared/git_credential.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
)
1616

1717
type GitCredentialFlow struct {
18+
Executable string
19+
1820
shouldSetup bool
1921
helper string
2022
scopes []string
@@ -50,13 +52,26 @@ func (flow *GitCredentialFlow) ShouldSetup() bool {
5052
}
5153

5254
func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error {
53-
return GitCredentialSetup(hostname, username, authToken, flow.helper)
55+
return flow.gitCredentialSetup(hostname, username, authToken)
5456
}
5557

56-
func GitCredentialSetup(hostname, username, password, helper string) error {
57-
if helper == "" {
58+
func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password string) error {
59+
if flow.helper == "" {
60+
// first use a blank value to indicate to git we want to sever the chain of credential helpers
61+
preConfigureCmd, err := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname), "")
62+
if err != nil {
63+
return err
64+
}
65+
if err = run.PrepareCmd(preConfigureCmd).Run(); err != nil {
66+
return err
67+
}
68+
5869
// use GitHub CLI as a credential helper (for this host only)
59-
configureCmd, err := git.GitCommand("config", "--global", gitCredentialHelperKey(hostname), "!gh auth git-credential")
70+
configureCmd, err := git.GitCommand(
71+
"config", "--global", "--add",
72+
gitCredentialHelperKey(hostname),
73+
fmt.Sprintf("!%s auth git-credential", shellQuote(flow.Executable)),
74+
)
6075
if err != nil {
6176
return err
6277
}
@@ -124,3 +139,10 @@ func isOurCredentialHelper(cmd string) bool {
124139

125140
return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh"
126141
}
142+
143+
func shellQuote(s string) string {
144+
if strings.ContainsAny(s, " $") {
145+
return "'" + s + "'"
146+
}
147+
return s
148+
}

pkg/cmd/auth/shared/git_credential_test.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,42 @@ func TestGitCredentialSetup_configureExisting(t *testing.T) {
1212
cs.Register(`git credential reject`, 0, "")
1313
cs.Register(`git credential approve`, 0, "")
1414

15-
if err := GitCredentialSetup("example.com", "monalisa", "PASSWD", "osxkeychain"); err != nil {
15+
f := GitCredentialFlow{
16+
Executable: "gh",
17+
helper: "osxkeychain",
18+
}
19+
20+
if err := f.gitCredentialSetup("example.com", "monalisa", "PASSWD"); err != nil {
1621
t.Errorf("GitCredentialSetup() error = %v", err)
1722
}
1823
}
1924

2025
func TestGitCredentialSetup_setOurs(t *testing.T) {
2126
cs, restoreRun := run.Stub()
2227
defer restoreRun(t)
23-
cs.Register(`git config --global credential\.https://example\.com\.helper`, 0, "", func(args []string) {
24-
if val := args[len(args)-1]; val != "!gh auth git-credential" {
28+
cs.Register(`git config --global credential\.`, 0, "", func(args []string) {
29+
if key := args[len(args)-2]; key != "credential.https://example.com.helper" {
30+
t.Errorf("git config key was %q", key)
31+
}
32+
if val := args[len(args)-1]; val != "" {
2533
t.Errorf("global credential helper configured to %q", val)
2634
}
2735
})
36+
cs.Register(`git config --global --add credential\.`, 0, "", func(args []string) {
37+
if key := args[len(args)-2]; key != "credential.https://example.com.helper" {
38+
t.Errorf("git config key was %q", key)
39+
}
40+
if val := args[len(args)-1]; val != "!/path/to/gh auth git-credential" {
41+
t.Errorf("global credential helper configured to %q", val)
42+
}
43+
})
44+
45+
f := GitCredentialFlow{
46+
Executable: "/path/to/gh",
47+
helper: "",
48+
}
2849

29-
if err := GitCredentialSetup("example.com", "monalisa", "PASSWD", ""); err != nil {
50+
if err := f.gitCredentialSetup("example.com", "monalisa", "PASSWD"); err != nil {
3051
t.Errorf("GitCredentialSetup() error = %v", err)
3152
}
3253
}

pkg/cmd/auth/shared/login_flow.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type LoginOptions struct {
2828
Interactive bool
2929
Web bool
3030
Scopes []string
31+
Executable string
3132

3233
sshContext sshContext
3334
}
@@ -56,7 +57,7 @@ func Login(opts *LoginOptions) error {
5657

5758
var additionalScopes []string
5859

59-
credentialFlow := &GitCredentialFlow{}
60+
credentialFlow := &GitCredentialFlow{Executable: opts.Executable}
6061
if opts.Interactive && gitProtocol == "https" {
6162
if err := credentialFlow.Prompt(hostname); err != nil {
6263
return err

0 commit comments

Comments
 (0)
X Tutup