Initial testscript introduction
This commit is contained in:
parent
76ea939627
commit
d7465bdf3c
7 changed files with 389 additions and 271 deletions
85
acceptance/pr_test.go
Normal file
85
acceptance/pr_test.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package acceptance_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"math/rand"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghcmd"
|
||||
"github.com/rogpeppe/go-internal/testscript"
|
||||
)
|
||||
|
||||
func ghMain() int {
|
||||
return int(ghcmd.Main())
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(testscript.RunMain(m, map[string]func() int{
|
||||
"gh": ghMain,
|
||||
}))
|
||||
}
|
||||
|
||||
func TestPullRequests(t *testing.T) {
|
||||
testscript.Run(t, params("pr"))
|
||||
}
|
||||
|
||||
func params(dir string) testscript.Params {
|
||||
return testscript.Params{
|
||||
Dir: path.Join("testdata", dir),
|
||||
Files: []string{},
|
||||
Setup: sharedSetup,
|
||||
Cmds: sharedCmds,
|
||||
RequireExplicitExec: true,
|
||||
RequireUniqueNames: true,
|
||||
}
|
||||
}
|
||||
|
||||
var sharedSetup = func(ts *testscript.Env) error {
|
||||
scriptName, ok := extractScriptName(ts.Vars)
|
||||
if !ok {
|
||||
ts.T().Fatal("script name not found")
|
||||
}
|
||||
ts.Setenv("SCRIPT_NAME", scriptName)
|
||||
|
||||
ts.Setenv("HOME", ts.Cd)
|
||||
ts.Setenv("GH_CONFIG_DIR", ts.Cd)
|
||||
|
||||
ts.Setenv("GH_TOKEN", os.Getenv("GH_TOKEN"))
|
||||
|
||||
ts.Setenv("ORG", os.Getenv("GH_ACCEPTANCE_ORG"))
|
||||
|
||||
ts.Setenv("RANDOM_STRING", randomString(10))
|
||||
return nil
|
||||
}
|
||||
|
||||
var sharedCmds = map[string]func(ts *testscript.TestScript, neg bool, args []string){
|
||||
"defer": func(ts *testscript.TestScript, neg bool, args []string) {
|
||||
ts.Defer(func() {
|
||||
if err := ts.Exec(args[0], args[1:]...); err != nil {
|
||||
ts.Fatalf("deferred command failed: %v", err)
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func randomString(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func extractScriptName(vars []string) (string, bool) {
|
||||
for _, kv := range vars {
|
||||
if strings.HasPrefix(kv, "WORK=") {
|
||||
v := strings.Split(kv, "=")[1]
|
||||
return strings.CutPrefix(path.Base(v), "script-")
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
24
acceptance/testdata/pr/pr-create-basic.txt
vendored
Normal file
24
acceptance/testdata/pr/pr-create-basic.txt
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Use gh as a credential helper
|
||||
exec gh auth setup-git
|
||||
|
||||
# Create a repository with a file so it has a default branch
|
||||
exec gh repo create $ORG/$SCRIPT_NAME-$RANDOM_STRING --add-readme --private
|
||||
|
||||
# Defer repo cleanup
|
||||
defer gh repo delete --yes $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Clone the repo
|
||||
exec gh repo clone $ORG/$SCRIPT_NAME-$RANDOM_STRING
|
||||
|
||||
# Prepare a branch to PR
|
||||
cd $SCRIPT_NAME-$RANDOM_STRING
|
||||
exec git checkout -b feature-branch
|
||||
exec git commit --allow-empty -m 'Empty Commit'
|
||||
exec git push -u origin feature-branch
|
||||
|
||||
# Create the PR
|
||||
exec gh pr create --title 'Feature Title' --body 'Feature Body'
|
||||
|
||||
# Check the PR is indeed created
|
||||
exec gh pr list
|
||||
stdout 'Feature Title'
|
||||
268
cmd/gh/main.go
268
cmd/gh/main.go
|
|
@ -1,276 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
surveyCore "github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/build"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/config/migration"
|
||||
"github.com/cli/cli/v2/internal/update"
|
||||
"github.com/cli/cli/v2/pkg/cmd/factory"
|
||||
"github.com/cli/cli/v2/pkg/cmd/root"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/utils"
|
||||
"github.com/cli/safeexec"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mgutz/ansi"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var updaterEnabled = ""
|
||||
|
||||
type exitCode int
|
||||
|
||||
const (
|
||||
exitOK exitCode = 0
|
||||
exitError exitCode = 1
|
||||
exitCancel exitCode = 2
|
||||
exitAuth exitCode = 4
|
||||
exitPending exitCode = 8
|
||||
"github.com/cli/cli/v2/internal/ghcmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
code := mainRun()
|
||||
code := ghcmd.Main()
|
||||
os.Exit(int(code))
|
||||
}
|
||||
|
||||
func mainRun() exitCode {
|
||||
buildDate := build.Date
|
||||
buildVersion := build.Version
|
||||
hasDebug, _ := utils.IsDebugEnabled()
|
||||
|
||||
cmdFactory := factory.New(buildVersion)
|
||||
stderr := cmdFactory.IOStreams.ErrOut
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if cfg, err := cmdFactory.Config(); err == nil {
|
||||
var m migration.MultiAccount
|
||||
if err := cfg.Migrate(m); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
return exitError
|
||||
}
|
||||
}
|
||||
|
||||
updateCtx, updateCancel := context.WithCancel(ctx)
|
||||
defer updateCancel()
|
||||
updateMessageChan := make(chan *update.ReleaseInfo)
|
||||
go func() {
|
||||
rel, err := checkForUpdate(updateCtx, cmdFactory, buildVersion)
|
||||
if err != nil && hasDebug {
|
||||
fmt.Fprintf(stderr, "warning: checking for update failed: %v", err)
|
||||
}
|
||||
updateMessageChan <- rel
|
||||
}()
|
||||
|
||||
if !cmdFactory.IOStreams.ColorEnabled() {
|
||||
surveyCore.DisableColor = true
|
||||
ansi.DisableColors(true)
|
||||
} else {
|
||||
// override survey's poor choice of color
|
||||
surveyCore.TemplateFuncsWithColor["color"] = func(style string) string {
|
||||
switch style {
|
||||
case "white":
|
||||
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, err := root.NewCmdRoot(cmdFactory, buildVersion, buildDate)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "failed to create root command: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
expandedArgs := []string{}
|
||||
if len(os.Args) > 0 {
|
||||
expandedArgs = os.Args[1:]
|
||||
}
|
||||
|
||||
// translate `gh help <command>` to `gh <command> --help` for extensions.
|
||||
if len(expandedArgs) >= 2 && expandedArgs[0] == "help" && isExtensionCommand(rootCmd, expandedArgs[1:]) {
|
||||
expandedArgs = expandedArgs[1:]
|
||||
expandedArgs = append(expandedArgs, "--help")
|
||||
}
|
||||
|
||||
rootCmd.SetArgs(expandedArgs)
|
||||
|
||||
if cmd, err := rootCmd.ExecuteContextC(ctx); err != nil {
|
||||
var pagerPipeError *iostreams.ErrClosedPagerPipe
|
||||
var noResultsError cmdutil.NoResultsError
|
||||
var extError *root.ExternalCommandExitError
|
||||
var authError *root.AuthError
|
||||
if err == cmdutil.SilentError {
|
||||
return exitError
|
||||
} else if err == cmdutil.PendingError {
|
||||
return exitPending
|
||||
} else if cmdutil.IsUserCancellation(err) {
|
||||
if errors.Is(err, terminal.InterruptErr) {
|
||||
// ensure the next shell prompt will start on its own line
|
||||
fmt.Fprint(stderr, "\n")
|
||||
}
|
||||
return exitCancel
|
||||
} else if errors.As(err, &authError) {
|
||||
return exitAuth
|
||||
} else if errors.As(err, &pagerPipeError) {
|
||||
// ignore the error raised when piping to a closed pager
|
||||
return exitOK
|
||||
} else if errors.As(err, &noResultsError) {
|
||||
if cmdFactory.IOStreams.IsStdoutTTY() {
|
||||
fmt.Fprintln(stderr, noResultsError.Error())
|
||||
}
|
||||
// no results is not a command failure
|
||||
return exitOK
|
||||
} else if errors.As(err, &extError) {
|
||||
// pass on exit codes from extensions and shell aliases
|
||||
return exitCode(extError.ExitCode())
|
||||
}
|
||||
|
||||
printError(stderr, err, cmd, hasDebug)
|
||||
|
||||
if strings.Contains(err.Error(), "Incorrect function") {
|
||||
fmt.Fprintln(stderr, "You appear to be running in MinTTY without pseudo terminal support.")
|
||||
fmt.Fprintln(stderr, "To learn about workarounds for this error, run: gh help mintty")
|
||||
return exitError
|
||||
}
|
||||
|
||||
var httpErr api.HTTPError
|
||||
if errors.As(err, &httpErr) && httpErr.StatusCode == 401 {
|
||||
fmt.Fprintln(stderr, "Try authenticating with: gh auth login")
|
||||
} else if u := factory.SSOURL(); u != "" {
|
||||
// handles organization SAML enforcement error
|
||||
fmt.Fprintf(stderr, "Authorize in your web browser: %s\n", u)
|
||||
} else if msg := httpErr.ScopesSuggestion(); msg != "" {
|
||||
fmt.Fprintln(stderr, msg)
|
||||
}
|
||||
|
||||
return exitError
|
||||
}
|
||||
if root.HasFailed() {
|
||||
return exitError
|
||||
}
|
||||
|
||||
updateCancel() // if the update checker hasn't completed by now, abort it
|
||||
newRelease := <-updateMessageChan
|
||||
if newRelease != nil {
|
||||
isHomebrew := isUnderHomebrew(cmdFactory.Executable())
|
||||
if isHomebrew && isRecentRelease(newRelease.PublishedAt) {
|
||||
// do not notify Homebrew users before the version bump had a chance to get merged into homebrew-core
|
||||
return exitOK
|
||||
}
|
||||
fmt.Fprintf(stderr, "\n\n%s %s → %s\n",
|
||||
ansi.Color("A new release of gh is available:", "yellow"),
|
||||
ansi.Color(strings.TrimPrefix(buildVersion, "v"), "cyan"),
|
||||
ansi.Color(strings.TrimPrefix(newRelease.Version, "v"), "cyan"))
|
||||
if isHomebrew {
|
||||
fmt.Fprintf(stderr, "To upgrade, run: %s\n", "brew upgrade gh")
|
||||
}
|
||||
fmt.Fprintf(stderr, "%s\n\n",
|
||||
ansi.Color(newRelease.URL, "yellow"))
|
||||
}
|
||||
|
||||
return exitOK
|
||||
}
|
||||
|
||||
// isExtensionCommand returns true if args resolve to an extension command.
|
||||
func isExtensionCommand(rootCmd *cobra.Command, args []string) bool {
|
||||
c, _, err := rootCmd.Find(args)
|
||||
return err == nil && c != nil && c.GroupID == "extension"
|
||||
}
|
||||
|
||||
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 https://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() && isTerminal(os.Stdout) && isTerminal(os.Stderr)
|
||||
}
|
||||
|
||||
func isTerminal(f *os.File) bool {
|
||||
return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
|
||||
}
|
||||
|
||||
// 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 checkForUpdate(ctx context.Context, f *cmdutil.Factory, currentVersion string) (*update.ReleaseInfo, error) {
|
||||
if !shouldCheckForUpdate() {
|
||||
return nil, nil
|
||||
}
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo := updaterEnabled
|
||||
stateFilePath := filepath.Join(config.StateDir(), "state.yml")
|
||||
return update.CheckForUpdate(ctx, httpClient, stateFilePath, repo, currentVersion)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -125,6 +125,7 @@ require (
|
|||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rodaine/table v1.0.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
|
|
@ -160,6 +161,7 @@ require (
|
|||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -366,8 +366,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rodaine/table v1.0.1 h1:U/VwCnUxlVYxw8+NJiLIuCxA/xa6jL38MY3FYysVWWQ=
|
||||
github.com/rodaine/table v1.0.1/go.mod h1:UVEtfBsflpeEcD56nF4F5AocNFta0ZuolpSVdPtlmP4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
|
|
@ -533,8 +533,8 @@ golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
|||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.172.0 h1:/1OcMZGPmW1rX2LCu2CmGUD1KXK1+pfzxotxyRUCCdk=
|
||||
google.golang.org/api v0.172.0/go.mod h1:+fJZq6QXWfa9pXhnIzsjx4yI22d4aI9ZpLb58gvXjis=
|
||||
|
|
|
|||
271
internal/ghcmd/cmd.go
Normal file
271
internal/ghcmd/cmd.go
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
package ghcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
surveyCore "github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/internal/build"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/config/migration"
|
||||
"github.com/cli/cli/v2/internal/update"
|
||||
"github.com/cli/cli/v2/pkg/cmd/factory"
|
||||
"github.com/cli/cli/v2/pkg/cmd/root"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/utils"
|
||||
"github.com/cli/safeexec"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mgutz/ansi"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var updaterEnabled = ""
|
||||
|
||||
type exitCode int
|
||||
|
||||
const (
|
||||
exitOK exitCode = 0
|
||||
exitError exitCode = 1
|
||||
exitCancel exitCode = 2
|
||||
exitAuth exitCode = 4
|
||||
exitPending exitCode = 8
|
||||
)
|
||||
|
||||
func Main() exitCode {
|
||||
buildDate := build.Date
|
||||
buildVersion := build.Version
|
||||
hasDebug, _ := utils.IsDebugEnabled()
|
||||
|
||||
cmdFactory := factory.New(buildVersion)
|
||||
stderr := cmdFactory.IOStreams.ErrOut
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if cfg, err := cmdFactory.Config(); err == nil {
|
||||
var m migration.MultiAccount
|
||||
if err := cfg.Migrate(m); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
return exitError
|
||||
}
|
||||
}
|
||||
|
||||
updateCtx, updateCancel := context.WithCancel(ctx)
|
||||
defer updateCancel()
|
||||
updateMessageChan := make(chan *update.ReleaseInfo)
|
||||
go func() {
|
||||
rel, err := checkForUpdate(updateCtx, cmdFactory, buildVersion)
|
||||
if err != nil && hasDebug {
|
||||
fmt.Fprintf(stderr, "warning: checking for update failed: %v", err)
|
||||
}
|
||||
updateMessageChan <- rel
|
||||
}()
|
||||
|
||||
if !cmdFactory.IOStreams.ColorEnabled() {
|
||||
surveyCore.DisableColor = true
|
||||
ansi.DisableColors(true)
|
||||
} else {
|
||||
// override survey's poor choice of color
|
||||
surveyCore.TemplateFuncsWithColor["color"] = func(style string) string {
|
||||
switch style {
|
||||
case "white":
|
||||
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, err := root.NewCmdRoot(cmdFactory, buildVersion, buildDate)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "failed to create root command: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
expandedArgs := []string{}
|
||||
if len(os.Args) > 0 {
|
||||
expandedArgs = os.Args[1:]
|
||||
}
|
||||
|
||||
// translate `gh help <command>` to `gh <command> --help` for extensions.
|
||||
if len(expandedArgs) >= 2 && expandedArgs[0] == "help" && isExtensionCommand(rootCmd, expandedArgs[1:]) {
|
||||
expandedArgs = expandedArgs[1:]
|
||||
expandedArgs = append(expandedArgs, "--help")
|
||||
}
|
||||
|
||||
rootCmd.SetArgs(expandedArgs)
|
||||
|
||||
if cmd, err := rootCmd.ExecuteContextC(ctx); err != nil {
|
||||
var pagerPipeError *iostreams.ErrClosedPagerPipe
|
||||
var noResultsError cmdutil.NoResultsError
|
||||
var extError *root.ExternalCommandExitError
|
||||
var authError *root.AuthError
|
||||
if err == cmdutil.SilentError {
|
||||
return exitError
|
||||
} else if err == cmdutil.PendingError {
|
||||
return exitPending
|
||||
} else if cmdutil.IsUserCancellation(err) {
|
||||
if errors.Is(err, terminal.InterruptErr) {
|
||||
// ensure the next shell prompt will start on its own line
|
||||
fmt.Fprint(stderr, "\n")
|
||||
}
|
||||
return exitCancel
|
||||
} else if errors.As(err, &authError) {
|
||||
return exitAuth
|
||||
} else if errors.As(err, &pagerPipeError) {
|
||||
// ignore the error raised when piping to a closed pager
|
||||
return exitOK
|
||||
} else if errors.As(err, &noResultsError) {
|
||||
if cmdFactory.IOStreams.IsStdoutTTY() {
|
||||
fmt.Fprintln(stderr, noResultsError.Error())
|
||||
}
|
||||
// no results is not a command failure
|
||||
return exitOK
|
||||
} else if errors.As(err, &extError) {
|
||||
// pass on exit codes from extensions and shell aliases
|
||||
return exitCode(extError.ExitCode())
|
||||
}
|
||||
|
||||
printError(stderr, err, cmd, hasDebug)
|
||||
|
||||
if strings.Contains(err.Error(), "Incorrect function") {
|
||||
fmt.Fprintln(stderr, "You appear to be running in MinTTY without pseudo terminal support.")
|
||||
fmt.Fprintln(stderr, "To learn about workarounds for this error, run: gh help mintty")
|
||||
return exitError
|
||||
}
|
||||
|
||||
var httpErr api.HTTPError
|
||||
if errors.As(err, &httpErr) && httpErr.StatusCode == 401 {
|
||||
fmt.Fprintln(stderr, "Try authenticating with: gh auth login")
|
||||
} else if u := factory.SSOURL(); u != "" {
|
||||
// handles organization SAML enforcement error
|
||||
fmt.Fprintf(stderr, "Authorize in your web browser: %s\n", u)
|
||||
} else if msg := httpErr.ScopesSuggestion(); msg != "" {
|
||||
fmt.Fprintln(stderr, msg)
|
||||
}
|
||||
|
||||
return exitError
|
||||
}
|
||||
if root.HasFailed() {
|
||||
return exitError
|
||||
}
|
||||
|
||||
updateCancel() // if the update checker hasn't completed by now, abort it
|
||||
newRelease := <-updateMessageChan
|
||||
if newRelease != nil {
|
||||
isHomebrew := isUnderHomebrew(cmdFactory.Executable())
|
||||
if isHomebrew && isRecentRelease(newRelease.PublishedAt) {
|
||||
// do not notify Homebrew users before the version bump had a chance to get merged into homebrew-core
|
||||
return exitOK
|
||||
}
|
||||
fmt.Fprintf(stderr, "\n\n%s %s → %s\n",
|
||||
ansi.Color("A new release of gh is available:", "yellow"),
|
||||
ansi.Color(strings.TrimPrefix(buildVersion, "v"), "cyan"),
|
||||
ansi.Color(strings.TrimPrefix(newRelease.Version, "v"), "cyan"))
|
||||
if isHomebrew {
|
||||
fmt.Fprintf(stderr, "To upgrade, run: %s\n", "brew upgrade gh")
|
||||
}
|
||||
fmt.Fprintf(stderr, "%s\n\n",
|
||||
ansi.Color(newRelease.URL, "yellow"))
|
||||
}
|
||||
|
||||
return exitOK
|
||||
}
|
||||
|
||||
// isExtensionCommand returns true if args resolve to an extension command.
|
||||
func isExtensionCommand(rootCmd *cobra.Command, args []string) bool {
|
||||
c, _, err := rootCmd.Find(args)
|
||||
return err == nil && c != nil && c.GroupID == "extension"
|
||||
}
|
||||
|
||||
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 https://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() && isTerminal(os.Stdout) && isTerminal(os.Stderr)
|
||||
}
|
||||
|
||||
func isTerminal(f *os.File) bool {
|
||||
return isatty.IsTerminal(f.Fd()) || isatty.IsCygwinTerminal(f.Fd())
|
||||
}
|
||||
|
||||
// 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 checkForUpdate(ctx context.Context, f *cmdutil.Factory, currentVersion string) (*update.ReleaseInfo, error) {
|
||||
if !shouldCheckForUpdate() {
|
||||
return nil, nil
|
||||
}
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo := updaterEnabled
|
||||
stateFilePath := filepath.Join(config.StateDir(), "state.yml")
|
||||
return update.CheckForUpdate(ctx, httpClient, stateFilePath, repo, currentVersion)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package ghcmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
Loading…
Add table
Add a link
Reference in a new issue