Before, when gh detected there was a new release in the `cli/cli` repo,
it would show this notice:
A new release of gh is available: {V1} → {V2}
Additionally, when the release was more than 24h old, we would show this
to Homebrew users:
To upgrade, run: brew update && brew upgrade gh
Ref. feb4acc2c0
This change makes it so that the original notice "A new release of gh is
available" is NOT shown to Homebrew users unless the release is older
than 24h. We effectively hide the fact that any release happened until
we're sure that the version bump has made it to `homebrew-core`.
293 lines
7.8 KiB
Go
293 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
surveyCore "github.com/AlecAivazis/survey/v2/core"
|
|
"github.com/cli/cli/api"
|
|
"github.com/cli/cli/internal/build"
|
|
"github.com/cli/cli/internal/config"
|
|
"github.com/cli/cli/internal/ghinstance"
|
|
"github.com/cli/cli/internal/run"
|
|
"github.com/cli/cli/internal/update"
|
|
"github.com/cli/cli/pkg/cmd/alias/expand"
|
|
"github.com/cli/cli/pkg/cmd/factory"
|
|
"github.com/cli/cli/pkg/cmd/root"
|
|
"github.com/cli/cli/pkg/cmdutil"
|
|
"github.com/cli/cli/utils"
|
|
"github.com/cli/safeexec"
|
|
"github.com/mattn/go-colorable"
|
|
"github.com/mgutz/ansi"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var updaterEnabled = ""
|
|
|
|
func main() {
|
|
buildDate := build.Date
|
|
buildVersion := build.Version
|
|
|
|
updateMessageChan := make(chan *update.ReleaseInfo)
|
|
go func() {
|
|
rel, _ := checkForUpdate(buildVersion)
|
|
updateMessageChan <- rel
|
|
}()
|
|
|
|
hasDebug := os.Getenv("DEBUG") != ""
|
|
|
|
if hostFromEnv := os.Getenv("GH_HOST"); hostFromEnv != "" {
|
|
ghinstance.OverrideDefault(hostFromEnv)
|
|
}
|
|
|
|
cmdFactory := factory.New(buildVersion)
|
|
stderr := cmdFactory.IOStreams.ErrOut
|
|
if !cmdFactory.IOStreams.ColorEnabled() {
|
|
surveyCore.DisableColor = true
|
|
} else {
|
|
// override survey's poor choice of color
|
|
surveyCore.TemplateFuncsWithColor["color"] = func(style string) string {
|
|
switch style {
|
|
case "white":
|
|
if cmdFactory.IOStreams.ColorSupport256() {
|
|
return fmt.Sprintf("\x1b[%d;5;%dm", 38, 242)
|
|
}
|
|
return ansi.ColorCode("default")
|
|
default:
|
|
return ansi.ColorCode(style)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enable running gh from Windows File Explorer's address bar. Without this, the user is told to stop and run from a
|
|
// terminal. With this, a user can clone a repo (or take other actions) directly from explorer.
|
|
if len(os.Args) > 1 && os.Args[1] != "" {
|
|
cobra.MousetrapHelpText = ""
|
|
}
|
|
|
|
rootCmd := root.NewCmdRoot(cmdFactory, buildVersion, buildDate)
|
|
|
|
cfg, err := cmdFactory.Config()
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "failed to read configuration: %s\n", err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
if prompt, _ := cfg.Get("", "prompt"); prompt == "disabled" {
|
|
cmdFactory.IOStreams.SetNeverPrompt(true)
|
|
}
|
|
|
|
if pager, _ := cfg.Get("", "pager"); pager != "" {
|
|
cmdFactory.IOStreams.SetPager(pager)
|
|
}
|
|
|
|
expandedArgs := []string{}
|
|
if len(os.Args) > 0 {
|
|
expandedArgs = os.Args[1:]
|
|
}
|
|
|
|
cmd, _, err := rootCmd.Traverse(expandedArgs)
|
|
if err != nil || cmd == rootCmd {
|
|
originalArgs := expandedArgs
|
|
isShell := false
|
|
|
|
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)
|
|
}
|
|
|
|
if hasDebug {
|
|
fmt.Fprintf(stderr, "%v -> %v\n", originalArgs, expandedArgs)
|
|
}
|
|
|
|
if isShell {
|
|
exe, err := safeexec.LookPath(expandedArgs[0])
|
|
if err != nil {
|
|
fmt.Fprintf(stderr, "failed to run external command: %s", err)
|
|
os.Exit(3)
|
|
}
|
|
|
|
externalCmd := exec.Command(exe, expandedArgs[1:]...)
|
|
externalCmd.Stderr = os.Stderr
|
|
externalCmd.Stdout = os.Stdout
|
|
externalCmd.Stdin = os.Stdin
|
|
preparedCmd := run.PrepareCmd(externalCmd)
|
|
|
|
err = preparedCmd.Run()
|
|
if err != nil {
|
|
if ee, ok := err.(*exec.ExitError); ok {
|
|
os.Exit(ee.ExitCode())
|
|
}
|
|
|
|
fmt.Fprintf(stderr, "failed to run external command: %s", err)
|
|
os.Exit(3)
|
|
}
|
|
|
|
os.Exit(0)
|
|
}
|
|
}
|
|
|
|
cs := cmdFactory.IOStreams.ColorScheme()
|
|
|
|
if cmd != nil && cmdutil.IsAuthCheckEnabled(cmd) && !cmdutil.CheckAuth(cfg) {
|
|
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)
|
|
}
|
|
|
|
rootCmd.SetArgs(expandedArgs)
|
|
|
|
if cmd, err := rootCmd.ExecuteC(); err != nil {
|
|
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`")
|
|
}
|
|
|
|
os.Exit(1)
|
|
}
|
|
if root.HasFailed() {
|
|
os.Exit(1)
|
|
}
|
|
|
|
newRelease := <-updateMessageChan
|
|
if newRelease != nil {
|
|
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 isHomebrew {
|
|
fmt.Fprintf(stderr, "To upgrade, run: %s\n", "brew update && brew upgrade gh")
|
|
}
|
|
fmt.Fprintf(stderr, "%s\n\n",
|
|
ansi.Color(newRelease.URL, "yellow"))
|
|
}
|
|
}
|
|
|
|
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)
|
|
if debug {
|
|
fmt.Fprintln(out, dnsError)
|
|
}
|
|
fmt.Fprintln(out, "check your internet connection or githubstatus.com")
|
|
return
|
|
}
|
|
|
|
fmt.Fprintln(out, err)
|
|
|
|
var flagError *cmdutil.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 {
|
|
if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" {
|
|
return false
|
|
}
|
|
if os.Getenv("CODESPACES") != "" {
|
|
return false
|
|
}
|
|
return updaterEnabled != "" && !isCI() && !isCompletionCommand() && utils.IsTerminal(os.Stderr)
|
|
}
|
|
|
|
// based on https://github.com/watson/ci-info/blob/HEAD/index.js
|
|
func isCI() bool {
|
|
return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari
|
|
os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity
|
|
os.Getenv("RUN_ID") != "" // TaskCluster, dsari
|
|
}
|
|
|
|
func isCompletionCommand() bool {
|
|
return len(os.Args) > 1 && os.Args[1] == "completion"
|
|
}
|
|
|
|
func checkForUpdate(currentVersion string) (*update.ReleaseInfo, error) {
|
|
if !shouldCheckForUpdate() {
|
|
return nil, nil
|
|
}
|
|
|
|
client, err := basicClient(currentVersion)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repo := updaterEnabled
|
|
stateFilePath := path.Join(config.ConfigDir(), "state.yml")
|
|
return update.CheckForUpdate(client, stateFilePath, repo, currentVersion)
|
|
}
|
|
|
|
// BasicClient returns an API client for github.com only that borrows from but
|
|
// does not depend on user configuration
|
|
func basicClient(currentVersion string) (*api.Client, error) {
|
|
var opts []api.ClientOption
|
|
if verbose := os.Getenv("DEBUG"); verbose != "" {
|
|
opts = append(opts, apiVerboseLog())
|
|
}
|
|
opts = append(opts, api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", currentVersion)))
|
|
|
|
token, _ := config.AuthTokenFromEnv(ghinstance.Default())
|
|
if token == "" {
|
|
if c, err := config.ParseDefaultConfig(); err == nil {
|
|
token, _ = c.Get(ghinstance.Default(), "oauth_token")
|
|
}
|
|
}
|
|
if token != "" {
|
|
opts = append(opts, api.AddHeader("Authorization", fmt.Sprintf("token %s", token)))
|
|
}
|
|
return api.NewClient(opts...), nil
|
|
}
|
|
|
|
func apiVerboseLog() api.ClientOption {
|
|
logTraffic := strings.Contains(os.Getenv("DEBUG"), "api")
|
|
colorize := utils.IsTerminal(os.Stderr)
|
|
return api.VerboseLog(colorable.NewColorable(os.Stderr), logTraffic, colorize)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
brewPrefixBytes, err := exec.Command(brewExe, "--prefix").Output()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
brewBinPrefix := filepath.Join(strings.TrimSpace(string(brewPrefixBytes)), "bin") + string(filepath.Separator)
|
|
return strings.HasPrefix(ghBinary, brewBinPrefix)
|
|
}
|