github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/cmd/gh/main.go (about) 1 package main 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "net" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "strings" 12 "time" 13 14 surveyCore "github.com/AlecAivazis/survey/v2/core" 15 "github.com/AlecAivazis/survey/v2/terminal" 16 "github.com/cli/cli/api" 17 "github.com/cli/cli/internal/build" 18 "github.com/cli/cli/internal/config" 19 "github.com/cli/cli/internal/ghinstance" 20 "github.com/cli/cli/internal/ghrepo" 21 "github.com/cli/cli/internal/run" 22 "github.com/cli/cli/internal/update" 23 "github.com/cli/cli/pkg/cmd/alias/expand" 24 "github.com/cli/cli/pkg/cmd/factory" 25 "github.com/cli/cli/pkg/cmd/root" 26 "github.com/cli/cli/pkg/cmdutil" 27 "github.com/cli/cli/utils" 28 "github.com/cli/safeexec" 29 "github.com/mattn/go-colorable" 30 "github.com/mgutz/ansi" 31 "github.com/spf13/cobra" 32 ) 33 34 var updaterEnabled = "" 35 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 45 func main() { 46 code := mainRun() 47 os.Exit(int(code)) 48 } 49 50 func mainRun() exitCode { 51 buildDate := build.Date 52 buildVersion := build.Version 53 54 updateMessageChan := make(chan *update.ReleaseInfo) 55 go func() { 56 rel, _ := checkForUpdate(buildVersion) 57 updateMessageChan <- rel 58 }() 59 60 hasDebug := os.Getenv("DEBUG") != "" 61 62 cmdFactory := factory.New(buildVersion) 63 stderr := cmdFactory.IOStreams.ErrOut 64 65 if spec := os.Getenv("GH_FORCE_TTY"); spec != "" { 66 cmdFactory.IOStreams.ForceTerminal(spec) 67 } 68 if !cmdFactory.IOStreams.ColorEnabled() { 69 surveyCore.DisableColor = true 70 } else { 71 // override survey's poor choice of color 72 surveyCore.TemplateFuncsWithColor["color"] = func(style string) string { 73 switch style { 74 case "white": 75 if cmdFactory.IOStreams.ColorSupport256() { 76 return fmt.Sprintf("\x1b[%d;5;%dm", 38, 242) 77 } 78 return ansi.ColorCode("default") 79 default: 80 return ansi.ColorCode(style) 81 } 82 } 83 } 84 85 // Enable running gh from Windows File Explorer's address bar. Without this, the user is told to stop and run from a 86 // terminal. With this, a user can clone a repo (or take other actions) directly from explorer. 87 if len(os.Args) > 1 && os.Args[1] != "" { 88 cobra.MousetrapHelpText = "" 89 } 90 91 rootCmd := root.NewCmdRoot(cmdFactory, buildVersion, buildDate) 92 93 cfg, err := cmdFactory.Config() 94 if err != nil { 95 fmt.Fprintf(stderr, "failed to read configuration: %s\n", err) 96 return exitError 97 } 98 99 // TODO: remove after FromFullName has been revisited 100 if host, err := cfg.DefaultHost(); err == nil { 101 ghrepo.SetDefaultHost(host) 102 } 103 104 expandedArgs := []string{} 105 if len(os.Args) > 0 { 106 expandedArgs = os.Args[1:] 107 } 108 109 // translate `gh help <command>` to `gh <command> --help` for extensions 110 if len(expandedArgs) == 2 && expandedArgs[0] == "help" && !hasCommand(rootCmd, expandedArgs[1:]) { 111 expandedArgs = []string{expandedArgs[1], "--help"} 112 } 113 114 if !hasCommand(rootCmd, expandedArgs) { 115 originalArgs := expandedArgs 116 isShell := false 117 118 argsForExpansion := append([]string{"gh"}, expandedArgs...) 119 expandedArgs, isShell, err = expand.ExpandAlias(cfg, argsForExpansion, nil) 120 if err != nil { 121 fmt.Fprintf(stderr, "failed to process aliases: %s\n", err) 122 return exitError 123 } 124 125 if hasDebug { 126 fmt.Fprintf(stderr, "%v -> %v\n", originalArgs, expandedArgs) 127 } 128 129 if isShell { 130 exe, err := safeexec.LookPath(expandedArgs[0]) 131 if err != nil { 132 fmt.Fprintf(stderr, "failed to run external command: %s", err) 133 return exitError 134 } 135 136 externalCmd := exec.Command(exe, expandedArgs[1:]...) 137 externalCmd.Stderr = os.Stderr 138 externalCmd.Stdout = os.Stdout 139 externalCmd.Stdin = os.Stdin 140 preparedCmd := run.PrepareCmd(externalCmd) 141 142 err = preparedCmd.Run() 143 if err != nil { 144 var execError *exec.ExitError 145 if errors.As(err, &execError) { 146 return exitCode(execError.ExitCode()) 147 } 148 fmt.Fprintf(stderr, "failed to run external command: %s", err) 149 return exitError 150 } 151 152 return exitOK 153 } else if len(expandedArgs) > 0 && !hasCommand(rootCmd, expandedArgs) { 154 extensionManager := cmdFactory.ExtensionManager 155 if found, err := extensionManager.Dispatch(expandedArgs, os.Stdin, os.Stdout, os.Stderr); err != nil { 156 var execError *exec.ExitError 157 if errors.As(err, &execError) { 158 return exitCode(execError.ExitCode()) 159 } 160 fmt.Fprintf(stderr, "failed to run extension: %s", err) 161 return exitError 162 } else if found { 163 return exitOK 164 } 165 } 166 } 167 168 // provide completions for aliases and extensions 169 rootCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 170 var results []string 171 if aliases, err := cfg.Aliases(); err == nil { 172 for aliasName := range aliases.All() { 173 if strings.HasPrefix(aliasName, toComplete) { 174 results = append(results, aliasName) 175 } 176 } 177 } 178 for _, ext := range cmdFactory.ExtensionManager.List(false) { 179 if strings.HasPrefix(ext.Name(), toComplete) { 180 results = append(results, ext.Name()) 181 } 182 } 183 return results, cobra.ShellCompDirectiveNoFileComp 184 } 185 186 cs := cmdFactory.IOStreams.ColorScheme() 187 188 authError := errors.New("authError") 189 rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { 190 // require that the user is authenticated before running most commands 191 if cmdutil.IsAuthCheckEnabled(cmd) && !cmdutil.CheckAuth(cfg) { 192 fmt.Fprintln(stderr, cs.Bold("Welcome to GitHub CLI!")) 193 fmt.Fprintln(stderr) 194 fmt.Fprintln(stderr, "To authenticate, please run `gh auth login`.") 195 return authError 196 } 197 198 return nil 199 } 200 201 rootCmd.SetArgs(expandedArgs) 202 203 if cmd, err := rootCmd.ExecuteC(); err != nil { 204 if err == cmdutil.SilentError { 205 return exitError 206 } else if cmdutil.IsUserCancellation(err) { 207 if errors.Is(err, terminal.InterruptErr) { 208 // ensure the next shell prompt will start on its own line 209 fmt.Fprint(stderr, "\n") 210 } 211 return exitCancel 212 } else if errors.Is(err, authError) { 213 return exitAuth 214 } 215 216 printError(stderr, err, cmd, hasDebug) 217 218 if strings.Contains(err.Error(), "Incorrect function") { 219 fmt.Fprintln(stderr, "You appear to be running in MinTTY without pseudo terminal support.") 220 fmt.Fprintln(stderr, "To learn about workarounds for this error, run: gh help mintty") 221 return exitError 222 } 223 224 var httpErr api.HTTPError 225 if errors.As(err, &httpErr) && httpErr.StatusCode == 401 { 226 fmt.Fprintln(stderr, "hint: try authenticating with `gh auth login`") 227 } 228 229 return exitError 230 } 231 if root.HasFailed() { 232 return exitError 233 } 234 235 newRelease := <-updateMessageChan 236 if newRelease != nil { 237 isHomebrew := isUnderHomebrew(cmdFactory.Executable) 238 if isHomebrew && isRecentRelease(newRelease.PublishedAt) { 239 // do not notify Homebrew users before the version bump had a chance to get merged into homebrew-core 240 return exitOK 241 } 242 fmt.Fprintf(stderr, "\n\n%s %s → %s\n", 243 ansi.Color("A new release of gh is available:", "yellow"), 244 ansi.Color(buildVersion, "cyan"), 245 ansi.Color(newRelease.Version, "cyan")) 246 if isHomebrew { 247 fmt.Fprintf(stderr, "To upgrade, run: %s\n", "brew update && brew upgrade gh") 248 } 249 fmt.Fprintf(stderr, "%s\n\n", 250 ansi.Color(newRelease.URL, "yellow")) 251 } 252 253 return exitOK 254 } 255 256 // hasCommand returns true if args resolve to a built-in command 257 func hasCommand(rootCmd *cobra.Command, args []string) bool { 258 c, _, err := rootCmd.Traverse(args) 259 return err == nil && c != rootCmd 260 } 261 262 func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) { 263 var dnsError *net.DNSError 264 if errors.As(err, &dnsError) { 265 fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name) 266 if debug { 267 fmt.Fprintln(out, dnsError) 268 } 269 fmt.Fprintln(out, "check your internet connection or https://githubstatus.com") 270 return 271 } 272 273 fmt.Fprintln(out, err) 274 275 var flagError *cmdutil.FlagError 276 if errors.As(err, &flagError) || strings.HasPrefix(err.Error(), "unknown command ") { 277 if !strings.HasSuffix(err.Error(), "\n") { 278 fmt.Fprintln(out) 279 } 280 fmt.Fprintln(out, cmd.UsageString()) 281 } 282 } 283 284 func shouldCheckForUpdate() bool { 285 if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" { 286 return false 287 } 288 if os.Getenv("CODESPACES") != "" { 289 return false 290 } 291 return updaterEnabled != "" && !isCI() && utils.IsTerminal(os.Stdout) && utils.IsTerminal(os.Stderr) 292 } 293 294 // based on https://github.com/watson/ci-info/blob/HEAD/index.js 295 func isCI() bool { 296 return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari 297 os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity 298 os.Getenv("RUN_ID") != "" // TaskCluster, dsari 299 } 300 301 func checkForUpdate(currentVersion string) (*update.ReleaseInfo, error) { 302 if !shouldCheckForUpdate() { 303 return nil, nil 304 } 305 306 client, err := basicClient(currentVersion) 307 if err != nil { 308 return nil, err 309 } 310 311 repo := updaterEnabled 312 stateFilePath := filepath.Join(config.StateDir(), "state.yml") 313 return update.CheckForUpdate(client, stateFilePath, repo, currentVersion) 314 } 315 316 // BasicClient returns an API client for github.com only that borrows from but 317 // does not depend on user configuration 318 func basicClient(currentVersion string) (*api.Client, error) { 319 var opts []api.ClientOption 320 if verbose := os.Getenv("DEBUG"); verbose != "" { 321 opts = append(opts, apiVerboseLog()) 322 } 323 opts = append(opts, api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", currentVersion))) 324 325 token, _ := config.AuthTokenFromEnv(ghinstance.Default()) 326 if token == "" { 327 if c, err := config.ParseDefaultConfig(); err == nil { 328 token, _ = c.Get(ghinstance.Default(), "oauth_token") 329 } 330 } 331 if token != "" { 332 opts = append(opts, api.AddHeader("Authorization", fmt.Sprintf("token %s", token))) 333 } 334 return api.NewClient(opts...), nil 335 } 336 337 func apiVerboseLog() api.ClientOption { 338 logTraffic := strings.Contains(os.Getenv("DEBUG"), "api") 339 colorize := utils.IsTerminal(os.Stderr) 340 return api.VerboseLog(colorable.NewColorable(os.Stderr), logTraffic, colorize) 341 } 342 343 func isRecentRelease(publishedAt time.Time) bool { 344 return !publishedAt.IsZero() && time.Since(publishedAt) < time.Hour*24 345 } 346 347 // Check whether the gh binary was found under the Homebrew prefix 348 func isUnderHomebrew(ghBinary string) bool { 349 brewExe, err := safeexec.LookPath("brew") 350 if err != nil { 351 return false 352 } 353 354 brewPrefixBytes, err := exec.Command(brewExe, "--prefix").Output() 355 if err != nil { 356 return false 357 } 358 359 brewBinPrefix := filepath.Join(strings.TrimSpace(string(brewPrefixBytes)), "bin") + string(filepath.Separator) 360 return strings.HasPrefix(ghBinary, brewBinPrefix) 361 }