github.com/abdfnx/gh-api@v0.0.0-20210414084727-f5432eec23b8/cmd/sm/main.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"net"
     8  	"os"
     9  	"os/exec"
    10  	"path"
    11  	"path/filepath"
    12  	"strings"
    13  	"time"
    14  
    15  	surveyCore "github.com/AlecAivazis/survey/v2/core"
    16  	"github.com/AlecAivazis/survey/v2/terminal"
    17  	"github.com/abdfnx/gh-api/api"
    18  	"github.com/abdfnx/gh-api/internal/build"
    19  	"github.com/abdfnx/gh-api/internal/config"
    20  	"github.com/abdfnx/gh-api/internal/ghinstance"
    21  	"github.com/abdfnx/gh-api/internal/ghrepo"
    22  	"github.com/abdfnx/gh-api/internal/run"
    23  	"github.com/abdfnx/gh-api/internal/update"
    24  	"github.com/abdfnx/gh-api/pkg/cmd/alias/expand"
    25  	"github.com/abdfnx/gh-api/pkg/cmd/factory"
    26  	"github.com/abdfnx/gh-api/pkg/cmd/root"
    27  	"github.com/abdfnx/gh-api/pkg/cmdutil"
    28  	"github.com/abdfnx/gh-api/utils"
    29  	"github.com/cli/safeexec"
    30  	"github.com/mattn/go-colorable"
    31  	"github.com/mgutz/ansi"
    32  	"github.com/spf13/cobra"
    33  )
    34  
    35  var updaterEnabled = ""
    36  
    37  type exitCode int
    38  
    39  const (
    40  	exitOK     exitCode = 0
    41  	exitError  exitCode = 1
    42  	exitCancel exitCode = 2
    43  	exitAuth   exitCode = 4
    44  )
    45  
    46  func main() {
    47  	code := mainRun()
    48  	os.Exit(int(code))
    49  }
    50  
    51  func mainRun() exitCode {
    52  	buildDate := build.Date
    53  	buildVersion := build.Version
    54  
    55  	updateMessageChan := make(chan *update.ReleaseInfo)
    56  	go func() {
    57  		rel, _ := checkForUpdate(buildVersion)
    58  		updateMessageChan <- rel
    59  	}()
    60  
    61  	hasDebug := os.Getenv("DEBUG") != ""
    62  
    63  	cmdFactory := factory.New(buildVersion)
    64  	stderr := cmdFactory.IOStreams.ErrOut
    65  	if !cmdFactory.IOStreams.ColorEnabled() {
    66  		surveyCore.DisableColor = true
    67  	} else {
    68  		// override survey's poor choice of color
    69  		surveyCore.TemplateFuncsWithColor["color"] = func(style string) string {
    70  			switch style {
    71  			case "white":
    72  				if cmdFactory.IOStreams.ColorSupport256() {
    73  					return fmt.Sprintf("\x1b[%d;5;%dm", 38, 242)
    74  				}
    75  				return ansi.ColorCode("default")
    76  			default:
    77  				return ansi.ColorCode(style)
    78  			}
    79  		}
    80  	}
    81  
    82  	// terminal. With this, a user can clone a repo (or take other actions) directly from explorer.
    83  	if len(os.Args) > 1 && os.Args[1] != "" {
    84  		cobra.MousetrapHelpText = ""
    85  	}
    86  
    87  	rootCmd := root.NewCmdRoot(cmdFactory, buildVersion, buildDate)
    88  
    89  	cfg, err := cmdFactory.Config()
    90  	if err != nil {
    91  		fmt.Fprintf(stderr, "failed to read configuration:  %s\n", err)
    92  		return exitError
    93  	}
    94  
    95  	if prompt, _ := cfg.Get("", "prompt"); prompt == "disabled" {
    96  		cmdFactory.IOStreams.SetNeverPrompt(true)
    97  	}
    98  
    99  	if pager, _ := cfg.Get("", "pager"); pager != "" {
   100  		cmdFactory.IOStreams.SetPager(pager)
   101  	}
   102  
   103  	// TODO: remove after FromFullName has been revisited
   104  	if host, err := cfg.DefaultHost(); err == nil {
   105  		ghrepo.SetDefaultHost(host)
   106  	}
   107  
   108  	expandedArgs := []string{}
   109  	if len(os.Args) > 0 {
   110  		expandedArgs = os.Args[1:]
   111  	}
   112  
   113  	cmd, _, err := rootCmd.Traverse(expandedArgs)
   114  	if err != nil || cmd == rootCmd {
   115  		originalArgs := expandedArgs
   116  		isShell := false
   117  
   118  		expandedArgs, isShell, err = expand.ExpandAlias(cfg, os.Args, nil)
   119  		if err != nil {
   120  			fmt.Fprintf(stderr, "failed to process aliases:  %s\n", err)
   121  			return exitError
   122  		}
   123  
   124  		if hasDebug {
   125  			fmt.Fprintf(stderr, "%v -> %v\n", originalArgs, expandedArgs)
   126  		}
   127  
   128  		if isShell {
   129  			exe, err := safeexec.LookPath(expandedArgs[0])
   130  			if err != nil {
   131  				fmt.Fprintf(stderr, "failed to run external command: %s", err)
   132  				return exitError
   133  			}
   134  
   135  			externalCmd := exec.Command(exe, expandedArgs[1:]...)
   136  			externalCmd.Stderr = os.Stderr
   137  			externalCmd.Stdout = os.Stdout
   138  			externalCmd.Stdin = os.Stdin
   139  			preparedCmd := run.PrepareCmd(externalCmd)
   140  
   141  			err = preparedCmd.Run()
   142  			if err != nil {
   143  				if ee, ok := err.(*exec.ExitError); ok {
   144  					return exitCode(ee.ExitCode())
   145  				}
   146  
   147  				fmt.Fprintf(stderr, "failed to run external command: %s", err)
   148  				return exitError
   149  			}
   150  
   151  			return exitOK
   152  		}
   153  	}
   154  
   155  	cs := cmdFactory.IOStreams.ColorScheme()
   156  
   157  	if cmd != nil && cmdutil.IsAuthCheckEnabled(cmd) && !cmdutil.CheckAuth(cfg) {
   158  		fmt.Fprintln(stderr, cs.Bold("Welcome to Secman Login!"))
   159  		fmt.Fprintln(stderr)
   160  		fmt.Fprintln(stderr, "To authenticate, please run `secman auth login`.")
   161  		return exitAuth
   162  	}
   163  
   164  	rootCmd.SetArgs(expandedArgs)
   165  
   166  	if cmd, err := rootCmd.ExecuteC(); err != nil {
   167  		if err == cmdutil.SilentError {
   168  			return exitError
   169  		} else if cmdutil.IsUserCancellation(err) {
   170  			if errors.Is(err, terminal.InterruptErr) {
   171  				// ensure the next shell prompt will start on its own line
   172  				fmt.Fprint(stderr, "\n")
   173  			}
   174  			return exitCancel
   175  		}
   176  
   177  		printError(stderr, err, cmd, hasDebug)
   178  
   179  		var httpErr api.HTTPError
   180  		if errors.As(err, &httpErr) && httpErr.StatusCode == 401 {
   181  			fmt.Fprintln(stderr, "hint: try authenticating with `secman auth login`")
   182  		}
   183  
   184  		return exitError
   185  	}
   186  	if root.HasFailed() {
   187  		return exitError
   188  	}
   189  
   190  	return exitOK
   191  }
   192  
   193  func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
   194  	var dnsError *net.DNSError
   195  	if errors.As(err, &dnsError) {
   196  		fmt.Fprintf(out, "error connecting to %s\n", dnsError.Name)
   197  		if debug {
   198  			fmt.Fprintln(out, dnsError)
   199  		}
   200  		fmt.Fprintln(out, "check your internet connection or githubstatus.com")
   201  		return
   202  	}
   203  
   204  	fmt.Fprintln(out, err)
   205  
   206  	var flagError *cmdutil.FlagError
   207  	if errors.As(err, &flagError) || strings.HasPrefix(err.Error(), "unknown command ") {
   208  		if !strings.HasSuffix(err.Error(), "\n") {
   209  			fmt.Fprintln(out)
   210  		}
   211  		fmt.Fprintln(out, cmd.UsageString())
   212  	}
   213  }
   214  
   215  func shouldCheckForUpdate() bool {
   216  	if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" {
   217  		return false
   218  	}
   219  	if os.Getenv("CODESPACES") != "" {
   220  		return false
   221  	}
   222  	return updaterEnabled != "" && !isCI() && utils.IsTerminal(os.Stdout) && utils.IsTerminal(os.Stderr)
   223  }
   224  
   225  // based on https://github.com/watson/ci-info/blob/HEAD/index.js
   226  func isCI() bool {
   227  	return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari
   228  		os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity
   229  		os.Getenv("RUN_ID") != "" // TaskCluster, dsari
   230  }
   231  
   232  func checkForUpdate(currentVersion string) (*update.ReleaseInfo, error) {
   233  	if !shouldCheckForUpdate() {
   234  		return nil, nil
   235  	}
   236  
   237  	client, err := basicClient(currentVersion)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  
   242  	repo := updaterEnabled
   243  	stateFilePath := path.Join(config.ConfigDir(), "state.yml")
   244  	return update.CheckForUpdate(client, stateFilePath, repo, currentVersion)
   245  }
   246  
   247  // BasicClient returns an API client for github.com only that borrows from but
   248  // does not depend on user configuration
   249  func basicClient(currentVersion string) (*api.Client, error) {
   250  	var opts []api.ClientOption
   251  	if verbose := os.Getenv("DEBUG"); verbose != "" {
   252  		opts = append(opts, apiVerboseLog())
   253  	}
   254  	opts = append(opts, api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", currentVersion)))
   255  
   256  	token, _ := config.AuthTokenFromEnv(ghinstance.Default())
   257  	if token == "" {
   258  		if c, err := config.ParseDefaultConfig(); err == nil {
   259  			token, _ = c.Get(ghinstance.Default(), "oauth_token")
   260  		}
   261  	}
   262  	if token != "" {
   263  		opts = append(opts, api.AddHeader("Authorization", fmt.Sprintf("token %s", token)))
   264  	}
   265  	return api.NewClient(opts...), nil
   266  }
   267  
   268  func apiVerboseLog() api.ClientOption {
   269  	logTraffic := strings.Contains(os.Getenv("DEBUG"), "api")
   270  	colorize := utils.IsTerminal(os.Stderr)
   271  	return api.VerboseLog(colorable.NewColorable(os.Stderr), logTraffic, colorize)
   272  }
   273  
   274  func isRecentRelease(publishedAt time.Time) bool {
   275  	return !publishedAt.IsZero() && time.Since(publishedAt) < time.Hour*24
   276  }
   277  
   278  // Check whether the gh binary was found under the Homebrew prefix
   279  func isUnderHomebrew(ghBinary string) bool {
   280  	brewExe, err := safeexec.LookPath("brew")
   281  	if err != nil {
   282  		return false
   283  	}
   284  
   285  	brewPrefixBytes, err := exec.Command(brewExe, "--prefix").Output()
   286  	if err != nil {
   287  		return false
   288  	}
   289  
   290  	brewBinPrefix := filepath.Join(strings.TrimSpace(string(brewPrefixBytes)), "bin") + string(filepath.Separator)
   291  	return strings.HasPrefix(ghBinary, brewBinPrefix)
   292  }