Allow creating of nested aliases (#7457)
This commit is contained in:
parent
79128a23c8
commit
74ee8cacae
17 changed files with 556 additions and 502 deletions
|
|
@ -2,13 +2,17 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/docs"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/cmd/root"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
|
@ -41,9 +45,13 @@ func run(args []string) error {
|
|||
}
|
||||
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
rootCmd := root.NewCmdRoot(&cmdutil.Factory{
|
||||
rootCmd, _ := root.NewCmdRoot(&cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
Browser: &browser{},
|
||||
Config: func() (config.Config, error) {
|
||||
return config.NewBlankConfig(), nil
|
||||
},
|
||||
ExtensionManager: &em{},
|
||||
}, "", "")
|
||||
rootCmd.InitDefaultHelpCmd()
|
||||
|
||||
|
|
@ -79,8 +87,42 @@ func linkHandler(name string) string {
|
|||
return fmt.Sprintf("./%s", strings.TrimSuffix(name, ".md"))
|
||||
}
|
||||
|
||||
// Implements browser.Browser interface.
|
||||
type browser struct{}
|
||||
|
||||
func (b *browser) Browse(url string) error {
|
||||
func (b *browser) Browse(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Implements extensions.ExtensionManager interface.
|
||||
type em struct{}
|
||||
|
||||
func (e *em) List() []extensions.Extension {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) Install(_ ghrepo.Interface, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) InstallLocal(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) Upgrade(_ string, _ bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) Remove(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) Dispatch(_ []string, _ io.Reader, _, _ io.Writer) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (e *em) Create(_ string, _ extensions.ExtTemplateType) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *em) EnableDryRunMode() {}
|
||||
|
|
|
|||
155
cmd/gh/main.go
155
cmd/gh/main.go
|
|
@ -14,16 +14,10 @@ import (
|
|||
|
||||
surveyCore "github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/build"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/internal/update"
|
||||
"github.com/cli/cli/v2/pkg/cmd/alias/expand"
|
||||
"github.com/cli/cli/v2/pkg/cmd/factory"
|
||||
"github.com/cli/cli/v2/pkg/cmd/root"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
|
|
@ -93,11 +87,9 @@ func mainRun() exitCode {
|
|||
cobra.MousetrapHelpText = ""
|
||||
}
|
||||
|
||||
rootCmd := root.NewCmdRoot(cmdFactory, buildVersion, buildDate)
|
||||
|
||||
cfg, err := cmdFactory.Config()
|
||||
rootCmd, err := root.NewCmdRoot(cmdFactory, buildVersion, buildDate)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "failed to read configuration: %s\n", err)
|
||||
fmt.Fprintf(stderr, "failed to create root command: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
|
|
@ -106,110 +98,10 @@ func mainRun() exitCode {
|
|||
expandedArgs = os.Args[1:]
|
||||
}
|
||||
|
||||
// translate `gh help <command>` to `gh <command> --help` for extensions
|
||||
if len(expandedArgs) == 2 && expandedArgs[0] == "help" && !hasCommand(rootCmd, expandedArgs[1:]) {
|
||||
expandedArgs = []string{expandedArgs[1], "--help"}
|
||||
}
|
||||
|
||||
if !hasCommand(rootCmd, expandedArgs) {
|
||||
originalArgs := expandedArgs
|
||||
isShell := false
|
||||
|
||||
argsForExpansion := append([]string{"gh"}, expandedArgs...)
|
||||
expandedArgs, isShell, err = expand.ExpandAlias(cfg, argsForExpansion, nil)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "failed to process aliases: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
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)
|
||||
return exitError
|
||||
}
|
||||
|
||||
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 {
|
||||
var execError *exec.ExitError
|
||||
if errors.As(err, &execError) {
|
||||
return exitCode(execError.ExitCode())
|
||||
}
|
||||
fmt.Fprintf(stderr, "failed to run external command: %s\n", err)
|
||||
return exitError
|
||||
}
|
||||
|
||||
return exitOK
|
||||
} else if len(expandedArgs) > 0 && !hasCommand(rootCmd, expandedArgs) {
|
||||
extensionManager := cmdFactory.ExtensionManager
|
||||
if found, err := extensionManager.Dispatch(expandedArgs, os.Stdin, os.Stdout, os.Stderr); err != nil {
|
||||
var execError *exec.ExitError
|
||||
if errors.As(err, &execError) {
|
||||
return exitCode(execError.ExitCode())
|
||||
}
|
||||
fmt.Fprintf(stderr, "failed to run extension: %s\n", err)
|
||||
return exitError
|
||||
} else if found {
|
||||
return exitOK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// provide completions for aliases and extensions
|
||||
rootCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
var results []string
|
||||
aliases := cfg.Aliases()
|
||||
for aliasName, aliasValue := range aliases.All() {
|
||||
if strings.HasPrefix(aliasName, toComplete) {
|
||||
var s string
|
||||
if strings.HasPrefix(aliasValue, "!") {
|
||||
s = fmt.Sprintf("%s\tShell alias", aliasName)
|
||||
} else {
|
||||
aliasValue = text.Truncate(80, aliasValue)
|
||||
s = fmt.Sprintf("%s\tAlias for %s", aliasName, aliasValue)
|
||||
}
|
||||
results = append(results, s)
|
||||
}
|
||||
}
|
||||
for _, ext := range cmdFactory.ExtensionManager.List() {
|
||||
if strings.HasPrefix(ext.Name(), toComplete) {
|
||||
var s string
|
||||
if ext.IsLocal() {
|
||||
s = fmt.Sprintf("%s\tLocal extension gh-%s", ext.Name(), ext.Name())
|
||||
} else {
|
||||
path := ext.URL()
|
||||
if u, err := git.ParseURL(ext.URL()); err == nil {
|
||||
if r, err := ghrepo.FromURL(u); err == nil {
|
||||
path = ghrepo.FullName(r)
|
||||
}
|
||||
}
|
||||
s = fmt.Sprintf("%s\tExtension %s", ext.Name(), path)
|
||||
}
|
||||
results = append(results, s)
|
||||
}
|
||||
}
|
||||
return results, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
authError := errors.New("authError")
|
||||
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
// require that the user is authenticated before running most commands
|
||||
if cmdutil.IsAuthCheckEnabled(cmd) && !cmdutil.CheckAuth(cfg) {
|
||||
fmt.Fprint(stderr, authHelp())
|
||||
return authError
|
||||
}
|
||||
|
||||
return nil
|
||||
// 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)
|
||||
|
|
@ -217,6 +109,8 @@ func mainRun() exitCode {
|
|||
if cmd, err := rootCmd.ExecuteContextC(ctx); err != nil {
|
||||
var pagerPipeError *iostreams.ErrClosedPagerPipe
|
||||
var noResultsError cmdutil.NoResultsError
|
||||
var execError *exec.ExitError
|
||||
var authError *root.AuthError
|
||||
if err == cmdutil.SilentError {
|
||||
return exitError
|
||||
} else if cmdutil.IsUserCancellation(err) {
|
||||
|
|
@ -225,7 +119,7 @@ func mainRun() exitCode {
|
|||
fmt.Fprint(stderr, "\n")
|
||||
}
|
||||
return exitCancel
|
||||
} else if errors.Is(err, authError) {
|
||||
} else if errors.As(err, &authError) {
|
||||
return exitAuth
|
||||
} else if errors.As(err, &pagerPipeError) {
|
||||
// ignore the error raised when piping to a closed pager
|
||||
|
|
@ -236,6 +130,8 @@ func mainRun() exitCode {
|
|||
}
|
||||
// no results is not a command failure
|
||||
return exitOK
|
||||
} else if errors.As(err, &execError) {
|
||||
return exitCode(execError.ExitCode())
|
||||
}
|
||||
|
||||
printError(stderr, err, cmd, hasDebug)
|
||||
|
|
@ -284,10 +180,10 @@ func mainRun() exitCode {
|
|||
return exitOK
|
||||
}
|
||||
|
||||
// hasCommand returns true if args resolve to a built-in command
|
||||
func hasCommand(rootCmd *cobra.Command, args []string) bool {
|
||||
c, _, err := rootCmd.Traverse(args)
|
||||
return err == nil && c != rootCmd
|
||||
// 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) {
|
||||
|
|
@ -312,27 +208,6 @@ func printError(out io.Writer, err error, cmd *cobra.Command, debug bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func authHelp() string {
|
||||
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||
return heredoc.Doc(`
|
||||
gh: To use GitHub CLI in a GitHub Actions workflow, set the GH_TOKEN environment variable. Example:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
`)
|
||||
}
|
||||
|
||||
if os.Getenv("CI") != "" {
|
||||
return heredoc.Doc(`
|
||||
gh: To use GitHub CLI in automation, set the GH_TOKEN environment variable.
|
||||
`)
|
||||
}
|
||||
|
||||
return heredoc.Doc(`
|
||||
To get started with GitHub CLI, please run: gh auth login
|
||||
Alternatively, populate the GH_TOKEN environment variable with a GitHub API authentication token.
|
||||
`)
|
||||
}
|
||||
|
||||
func shouldCheckForUpdate() bool {
|
||||
if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
package expand
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/pkg/findsh"
|
||||
"github.com/google/shlex"
|
||||
)
|
||||
|
||||
// ExpandAlias processes argv to see if it should be rewritten according to a user's aliases. The
|
||||
// second return value indicates whether the alias should be executed in a new shell process instead
|
||||
// of running gh itself.
|
||||
func ExpandAlias(cfg config.Config, args []string, findShFunc func() (string, error)) (expanded []string, isShell bool, err error) {
|
||||
if len(args) < 2 {
|
||||
// the command is lacking a subcommand
|
||||
return
|
||||
}
|
||||
expanded = args[1:]
|
||||
|
||||
aliases := cfg.Aliases()
|
||||
|
||||
expansion, getErr := aliases.Get(args[1])
|
||||
if getErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(expansion, "!") {
|
||||
isShell = true
|
||||
if findShFunc == nil {
|
||||
findShFunc = findSh
|
||||
}
|
||||
shPath, shErr := findShFunc()
|
||||
if shErr != nil {
|
||||
err = shErr
|
||||
return
|
||||
}
|
||||
|
||||
expanded = []string{shPath, "-c", expansion[1:]}
|
||||
|
||||
if len(args[2:]) > 0 {
|
||||
expanded = append(expanded, "--")
|
||||
expanded = append(expanded, args[2:]...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
extraArgs := []string{}
|
||||
for i, a := range args[2:] {
|
||||
if !strings.Contains(expansion, "$") {
|
||||
extraArgs = append(extraArgs, a)
|
||||
} else {
|
||||
expansion = strings.ReplaceAll(expansion, fmt.Sprintf("$%d", i+1), a)
|
||||
}
|
||||
}
|
||||
lingeringRE := regexp.MustCompile(`\$\d`)
|
||||
if lingeringRE.MatchString(expansion) {
|
||||
err = fmt.Errorf("not enough arguments for alias: %s", expansion)
|
||||
return
|
||||
}
|
||||
|
||||
var newArgs []string
|
||||
newArgs, err = shlex.Split(expansion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
expanded = append(newArgs, extraArgs...)
|
||||
return
|
||||
}
|
||||
|
||||
func findSh() (string, error) {
|
||||
shPath, err := findsh.Find()
|
||||
if err != nil {
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "", errors.New("unable to locate sh to execute the shell alias with. The sh.exe interpreter is typically distributed with Git for Windows.")
|
||||
}
|
||||
return "", errors.New("unable to locate sh to execute shell alias with")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return shPath, nil
|
||||
}
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
package expand
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
)
|
||||
|
||||
func TestExpandAlias(t *testing.T) {
|
||||
findShFunc := func() (string, error) {
|
||||
return "/usr/bin/sh", nil
|
||||
}
|
||||
|
||||
cfg := config.NewFromString(heredoc.Doc(`
|
||||
aliases:
|
||||
co: pr checkout
|
||||
il: issue list --author="$1" --label="$2"
|
||||
ia: issue list --author="$1" --assignee="$1"
|
||||
`))
|
||||
|
||||
type args struct {
|
||||
config config.Config
|
||||
argv []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantExpanded []string
|
||||
wantIsShell bool
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{},
|
||||
},
|
||||
wantExpanded: []string(nil),
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "too few arguments",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh"},
|
||||
},
|
||||
wantExpanded: []string(nil),
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "no expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "pr", "status"},
|
||||
},
|
||||
wantExpanded: []string{"pr", "status"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "simple expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "co"},
|
||||
},
|
||||
wantExpanded: []string{"pr", "checkout"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "adding arguments after expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "co", "123"},
|
||||
},
|
||||
wantExpanded: []string{"pr", "checkout", "123"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "not enough arguments for expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "il"},
|
||||
},
|
||||
wantExpanded: []string{},
|
||||
wantIsShell: false,
|
||||
wantErr: errors.New(`not enough arguments for alias: issue list --author="$1" --label="$2"`),
|
||||
},
|
||||
{
|
||||
name: "not enough arguments for expansion 2",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "il", "vilmibm"},
|
||||
},
|
||||
wantExpanded: []string{},
|
||||
wantIsShell: false,
|
||||
wantErr: errors.New(`not enough arguments for alias: issue list --author="vilmibm" --label="$2"`),
|
||||
},
|
||||
{
|
||||
name: "satisfy expansion arguments",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "il", "vilmibm", "help wanted"},
|
||||
},
|
||||
wantExpanded: []string{"issue", "list", "--author=vilmibm", "--label=help wanted"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "mixed positional and non-positional arguments",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "il", "vilmibm", "epic", "-R", "monalisa/testing"},
|
||||
},
|
||||
wantExpanded: []string{"issue", "list", "--author=vilmibm", "--label=epic", "-R", "monalisa/testing"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "dollar in expansion",
|
||||
args: args{
|
||||
config: cfg,
|
||||
argv: []string{"gh", "ia", "$coolmoney$"},
|
||||
},
|
||||
wantExpanded: []string{"issue", "list", "--author=$coolmoney$", "--assignee=$coolmoney$"},
|
||||
wantIsShell: false,
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotExpanded, gotIsShell, err := ExpandAlias(tt.args.config, tt.args.argv, findShFunc)
|
||||
if tt.wantErr != nil {
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if tt.wantErr.Error() != err.Error() {
|
||||
t.Fatalf("expected error %q, got %q", tt.wantErr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("got error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(gotExpanded, tt.wantExpanded) {
|
||||
t.Errorf("ExpandAlias() gotExpanded = %v, want %v", gotExpanded, tt.wantExpanded)
|
||||
}
|
||||
if gotIsShell != tt.wantIsShell {
|
||||
t.Errorf("ExpandAlias() gotIsShell = %v, want %v", gotIsShell, tt.wantIsShell)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// cfg := `---
|
||||
// aliases:
|
||||
// co: pr checkout
|
||||
// il: issue list --author="$1" --label="$2"
|
||||
// ia: issue list --author="$1" --assignee="$1"
|
||||
// `
|
||||
// initBlankContext(cfg, "OWNER/REPO", "trunk")
|
||||
// for _, c := range []struct {
|
||||
// Args string
|
||||
// ExpectedArgs []string
|
||||
// Err string
|
||||
// }{
|
||||
// {"gh co", []string{"pr", "checkout"}, ""},
|
||||
// {"gh il", nil, `not enough arguments for alias: issue list --author="$1" --label="$2"`},
|
||||
// {"gh il vilmibm", nil, `not enough arguments for alias: issue list --author="vilmibm" --label="$2"`},
|
||||
// {"gh co 123", []string{"pr", "checkout", "123"}, ""},
|
||||
// {"gh il vilmibm epic", []string{"issue", "list", `--author=vilmibm`, `--label=epic`}, ""},
|
||||
// {"gh ia vilmibm", []string{"issue", "list", `--author=vilmibm`, `--assignee=vilmibm`}, ""},
|
||||
// {"gh ia $coolmoney$", []string{"issue", "list", `--author=$coolmoney$`, `--assignee=$coolmoney$`}, ""},
|
||||
// {"gh pr status", []string{"pr", "status"}, ""},
|
||||
// {"gh il vilmibm epic -R vilmibm/testing", []string{"issue", "list", "--author=vilmibm", "--label=epic", "-R", "vilmibm/testing"}, ""},
|
||||
// {"gh dne", []string{"dne"}, ""},
|
||||
// {"gh", []string{}, ""},
|
||||
// {"", []string{}, ""},
|
||||
// } {
|
||||
|
|
@ -21,7 +21,8 @@ type ImportOptions struct {
|
|||
Filename string
|
||||
OverwriteExisting bool
|
||||
|
||||
existingCommand func(string) bool
|
||||
validAliasName func(string) bool
|
||||
validAliasExpansion func(string) bool
|
||||
}
|
||||
|
||||
func NewCmdImport(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Command {
|
||||
|
|
@ -74,7 +75,8 @@ func NewCmdImport(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co
|
|||
opts.Filename = args[0]
|
||||
}
|
||||
|
||||
opts.existingCommand = shared.ExistingCommandFunc(f, cmd)
|
||||
opts.validAliasName = shared.ValidAliasNameFunc(cmd)
|
||||
opts.validAliasExpansion = shared.ValidAliasExpansionFunc(cmd)
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
|
|
@ -120,9 +122,9 @@ func importRun(opts *ImportOptions) error {
|
|||
var msg strings.Builder
|
||||
|
||||
for _, alias := range getSortedKeys(aliasMap) {
|
||||
if opts.existingCommand(alias) {
|
||||
if !opts.validAliasName(alias) {
|
||||
msg.WriteString(
|
||||
fmt.Sprintf("%s Could not import alias %s: already a gh command\n",
|
||||
fmt.Sprintf("%s Could not import alias %s: already a gh command, extension, or alias\n",
|
||||
cs.FailureIcon(),
|
||||
cs.Bold(alias),
|
||||
),
|
||||
|
|
@ -133,9 +135,9 @@ func importRun(opts *ImportOptions) error {
|
|||
|
||||
expansion := aliasMap[alias]
|
||||
|
||||
if !(strings.HasPrefix(expansion, "!") || opts.existingCommand(expansion)) {
|
||||
if !opts.validAliasExpansion(expansion) {
|
||||
msg.WriteString(
|
||||
fmt.Sprintf("%s Could not import alias %s: expansion does not correspond to a gh command\n",
|
||||
fmt.Sprintf("%s Could not import alias %s: expansion does not correspond to a gh command, extension, or alias\n",
|
||||
cs.FailureIcon(),
|
||||
cs.Bold(alias),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/cli/cli/v2/internal/config"
|
||||
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -257,9 +256,9 @@ func TestImportRun(t *testing.T) {
|
|||
wantStderr: strings.Join(
|
||||
[]string{
|
||||
importFileMsg,
|
||||
"X Could not import alias api: already a gh command",
|
||||
"X Could not import alias issue: already a gh command",
|
||||
"X Could not import alias pr: already a gh command\n\n",
|
||||
"X Could not import alias api: already a gh command, extension, or alias",
|
||||
"X Could not import alias issue: already a gh command, extension, or alias",
|
||||
"X Could not import alias pr: already a gh command, extension, or alias\n\n",
|
||||
},
|
||||
"\n",
|
||||
),
|
||||
|
|
@ -277,8 +276,8 @@ func TestImportRun(t *testing.T) {
|
|||
wantStderr: strings.Join(
|
||||
[]string{
|
||||
importFileMsg,
|
||||
"X Could not import alias alias1: expansion does not correspond to a gh command",
|
||||
"X Could not import alias alias2: expansion does not correspond to a gh command\n\n",
|
||||
"X Could not import alias alias1: expansion does not correspond to a gh command, extension, or alias",
|
||||
"X Could not import alias alias2: expansion does not correspond to a gh command, extension, or alias\n\n",
|
||||
},
|
||||
"\n",
|
||||
),
|
||||
|
|
@ -304,16 +303,6 @@ func TestImportRun(t *testing.T) {
|
|||
return cfg, nil
|
||||
}
|
||||
|
||||
// Create fake command factory for testing.
|
||||
f := &cmdutil.Factory{
|
||||
IOStreams: ios,
|
||||
ExtensionManager: &extensions.ExtensionManagerMock{
|
||||
ListFunc: func() []extensions.Extension {
|
||||
return []extensions.Extension{}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create fake command structure for testing.
|
||||
rootCmd := &cobra.Command{}
|
||||
prCmd := &cobra.Command{Use: "pr"}
|
||||
|
|
@ -327,7 +316,8 @@ func TestImportRun(t *testing.T) {
|
|||
apiCmd.AddCommand(&cobra.Command{Use: "graphql"})
|
||||
rootCmd.AddCommand(apiCmd)
|
||||
|
||||
tt.opts.existingCommand = shared.ExistingCommandFunc(f, rootCmd)
|
||||
tt.opts.validAliasName = shared.ValidAliasNameFunc(rootCmd)
|
||||
tt.opts.validAliasExpansion = shared.ValidAliasExpansionFunc(rootCmd)
|
||||
|
||||
if tt.stdin != "" {
|
||||
stdin.WriteString(tt.stdin)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ type SetOptions struct {
|
|||
Expansion string
|
||||
IsShell bool
|
||||
|
||||
existingCommand func(string) bool
|
||||
validAliasName func(string) bool
|
||||
validAliasExpansion func(string) bool
|
||||
}
|
||||
|
||||
func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command {
|
||||
|
|
@ -59,6 +60,9 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
$ gh alias set homework 'issue list --assignee @me'
|
||||
$ gh homework
|
||||
|
||||
$ gh alias set 'issue mine' 'issue list --mention @me'
|
||||
$ gh issue mine
|
||||
|
||||
$ gh alias set epicsBy 'issue list --author="$1" --label="epic"'
|
||||
$ gh epicsBy vilmibm #=> gh issue list --author="vilmibm" --label="epic"
|
||||
|
||||
|
|
@ -70,7 +74,8 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
|
|||
opts.Name = args[0]
|
||||
opts.Expansion = args[1]
|
||||
|
||||
opts.existingCommand = shared.ExistingCommandFunc(f, cmd)
|
||||
opts.validAliasName = shared.ValidAliasNameFunc(cmd)
|
||||
opts.validAliasExpansion = shared.ValidAliasExpansionFunc(cmd)
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
|
|
@ -104,18 +109,16 @@ func setRun(opts *SetOptions) error {
|
|||
fmt.Fprintf(opts.IO.ErrOut, "- Adding alias for %s: %s\n", cs.Bold(opts.Name), cs.Bold(expansion))
|
||||
}
|
||||
|
||||
isShell := opts.IsShell
|
||||
if isShell && !strings.HasPrefix(expansion, "!") {
|
||||
if opts.IsShell && !strings.HasPrefix(expansion, "!") {
|
||||
expansion = "!" + expansion
|
||||
}
|
||||
isShell = strings.HasPrefix(expansion, "!")
|
||||
|
||||
if opts.existingCommand(opts.Name) {
|
||||
return fmt.Errorf("could not create alias: %q is already a gh command", opts.Name)
|
||||
if !opts.validAliasName(opts.Name) {
|
||||
return fmt.Errorf("could not create alias: %q is already a gh command, extension, or alias", opts.Name)
|
||||
}
|
||||
|
||||
if !isShell && !opts.existingCommand(expansion) {
|
||||
return fmt.Errorf("could not create alias: %s does not correspond to a gh command", expansion)
|
||||
if !opts.validAliasExpansion(expansion) {
|
||||
return fmt.Errorf("could not create alias: %s does not correspond to a gh command, extension, or alias", expansion)
|
||||
}
|
||||
|
||||
successMsg := fmt.Sprintf("%s Added alias.", cs.SuccessIcon())
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ func TestAliasSet_gh_command(t *testing.T) {
|
|||
cfg := config.NewFromString(``)
|
||||
|
||||
_, err := runCommand(cfg, true, "pr 'pr status'", "")
|
||||
assert.EqualError(t, err, `could not create alias: "pr" is already a gh command`)
|
||||
assert.EqualError(t, err, `could not create alias: "pr" is already a gh command, extension, or alias`)
|
||||
}
|
||||
|
||||
func TestAliasSet_empty_aliases(t *testing.T) {
|
||||
|
|
@ -231,7 +231,7 @@ func TestAliasSet_invalid_command(t *testing.T) {
|
|||
cfg := config.NewFromString(``)
|
||||
|
||||
_, err := runCommand(cfg, true, "co 'pe checkout'", "")
|
||||
assert.EqualError(t, err, "could not create alias: pe checkout does not correspond to a gh command")
|
||||
assert.EqualError(t, err, "could not create alias: pe checkout does not correspond to a gh command, extension, or alias")
|
||||
}
|
||||
|
||||
func TestShellAlias_flag(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"strings"
|
||||
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ExistingCommandFunc returns a function that will check if the given string
|
||||
// corresponds to an existing command.
|
||||
func ExistingCommandFunc(f *cmdutil.Factory, cmd *cobra.Command) func(string) bool {
|
||||
// ValidAliasNameFunc returns a function that will check if the given string
|
||||
// is a valid alias name. A name is valid if:
|
||||
// - it does not shadow an existing command,
|
||||
// - it is not nested under a command that is runnable,
|
||||
// - it is not nested under a command that does not exist.
|
||||
func ValidAliasNameFunc(cmd *cobra.Command) func(string) bool {
|
||||
return func(args string) bool {
|
||||
split, err := shlex.Split(args)
|
||||
if err != nil || len(split) == 0 {
|
||||
|
|
@ -16,17 +20,32 @@ func ExistingCommandFunc(f *cmdutil.Factory, cmd *cobra.Command) func(string) bo
|
|||
}
|
||||
|
||||
rootCmd := cmd.Root()
|
||||
cmd, _, err = rootCmd.Traverse(split)
|
||||
if err == nil && cmd != rootCmd {
|
||||
foundCmd, foundArgs, _ := rootCmd.Find(split)
|
||||
if foundCmd != nil && !foundCmd.Runnable() && len(foundArgs) == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, ext := range f.ExtensionManager.List() {
|
||||
if ext.Name() == split[0] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ValidAliasExpansionFunc returns a function that will check if the given string
|
||||
// is a valid alias expansion. An expansion is valid if:
|
||||
// - it is a shell expansion,
|
||||
// - it is a non-shell expansion that corresponds to an existing command, extension, or alias.
|
||||
func ValidAliasExpansionFunc(cmd *cobra.Command) func(string) bool {
|
||||
return func(expansion string) bool {
|
||||
if strings.HasPrefix(expansion, "!") {
|
||||
return true
|
||||
}
|
||||
|
||||
split, err := shlex.Split(expansion)
|
||||
if err != nil || len(split) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
rootCmd := cmd.Root()
|
||||
cmd, _, _ = rootCmd.Find(split)
|
||||
return cmd != rootCmd
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,22 +3,11 @@ package shared
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExistingCommandFunc(t *testing.T) {
|
||||
// Create fake command factory for testing.
|
||||
factory := &cmdutil.Factory{
|
||||
ExtensionManager: &extensions.ExtensionManagerMock{
|
||||
ListFunc: func() []extensions.Extension {
|
||||
return []extensions.Extension{}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestValidAliasNameFunc(t *testing.T) {
|
||||
// Create fake command structure for testing.
|
||||
issueCmd := &cobra.Command{Use: "issue"}
|
||||
prCmd := &cobra.Command{Use: "pr"}
|
||||
|
|
@ -28,13 +17,38 @@ func TestExistingCommandFunc(t *testing.T) {
|
|||
cmd.AddCommand(prCmd)
|
||||
cmd.AddCommand(issueCmd)
|
||||
|
||||
f := ExistingCommandFunc(factory, cmd)
|
||||
f := ValidAliasNameFunc(cmd)
|
||||
|
||||
assert.True(t, f("pr"))
|
||||
assert.True(t, f("pr checkout"))
|
||||
assert.True(t, f("issue"))
|
||||
assert.False(t, f("pr"))
|
||||
assert.False(t, f("pr checkout"))
|
||||
assert.False(t, f("issue"))
|
||||
assert.False(t, f("repo list"))
|
||||
|
||||
assert.True(t, f("ps"))
|
||||
assert.True(t, f("checkout"))
|
||||
assert.True(t, f("issue erase"))
|
||||
assert.True(t, f("pr erase"))
|
||||
assert.True(t, f("pr checkout branch"))
|
||||
}
|
||||
|
||||
func TestValidAliasExpansionFunc(t *testing.T) {
|
||||
// Create fake command structure for testing.
|
||||
issueCmd := &cobra.Command{Use: "issue"}
|
||||
prCmd := &cobra.Command{Use: "pr"}
|
||||
prCmd.AddCommand(&cobra.Command{Use: "checkout"})
|
||||
|
||||
cmd := &cobra.Command{}
|
||||
cmd.AddCommand(prCmd)
|
||||
cmd.AddCommand(issueCmd)
|
||||
|
||||
f := ValidAliasExpansionFunc(cmd)
|
||||
|
||||
assert.False(t, f("ps"))
|
||||
assert.False(t, f("checkout"))
|
||||
assert.False(t, f("repo list"))
|
||||
|
||||
assert.True(t, f("!git branch --show-current"))
|
||||
assert.True(t, f("pr"))
|
||||
assert.True(t, f("pr checkout"))
|
||||
assert.True(t, f("issue"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -643,10 +643,8 @@ func checkValidExtension(rootCmd *cobra.Command, m extensions.ExtensionManager,
|
|||
}
|
||||
|
||||
commandName := strings.TrimPrefix(extName, "gh-")
|
||||
if c, _, err := rootCmd.Traverse([]string{commandName}); err != nil {
|
||||
return nil, err
|
||||
} else if c != rootCmd {
|
||||
return nil, fmt.Errorf("%q matches the name of a built-in command", commandName)
|
||||
if c, _, _ := rootCmd.Find([]string{commandName}); c != rootCmd && c.GroupID != "extension" {
|
||||
return nil, fmt.Errorf("%q matches the name of a built-in command or alias", commandName)
|
||||
}
|
||||
|
||||
for _, ext := range m.List() {
|
||||
|
|
|
|||
|
|
@ -974,7 +974,7 @@ func Test_checkValidExtension(t *testing.T) {
|
|||
manager: m,
|
||||
extName: "gh-auth",
|
||||
},
|
||||
wantError: "\"auth\" matches the name of a built-in command",
|
||||
wantError: "\"auth\" matches the name of a built-in command or alias",
|
||||
},
|
||||
{
|
||||
name: "clashes with an installed extension",
|
||||
|
|
|
|||
126
pkg/cmd/root/alias.go
Normal file
126
pkg/cmd/root/alias.go
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
package root
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/cli/cli/v2/internal/run"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/findsh"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdShellAlias(io *iostreams.IOStreams, aliasName, aliasValue string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: aliasName,
|
||||
Short: fmt.Sprintf("Shell alias for %q", text.Truncate(80, aliasValue)),
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
expandedArgs, err := expandShellAlias(aliasValue, args, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
externalCmd := exec.Command(expandedArgs[0], expandedArgs[1:]...)
|
||||
externalCmd.Stderr = io.ErrOut
|
||||
externalCmd.Stdout = io.Out
|
||||
externalCmd.Stdin = io.In
|
||||
preparedCmd := run.PrepareCmd(externalCmd)
|
||||
if err = preparedCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to run external command: %w\n", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
GroupID: "alias",
|
||||
Annotations: map[string]string{
|
||||
"skipAuthCheck": "true",
|
||||
},
|
||||
DisableFlagParsing: true,
|
||||
}
|
||||
}
|
||||
|
||||
func NewCmdAlias(io *iostreams.IOStreams, aliasName, aliasValue string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: aliasName,
|
||||
Short: fmt.Sprintf("Alias for %q", text.Truncate(80, aliasValue)),
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
expandedArgs, err := expandAlias(aliasValue, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root := c.Root()
|
||||
root.SetArgs(expandedArgs)
|
||||
return root.Execute()
|
||||
},
|
||||
GroupID: "alias",
|
||||
Annotations: map[string]string{
|
||||
"skipAuthCheck": "true",
|
||||
},
|
||||
DisableFlagParsing: true,
|
||||
}
|
||||
}
|
||||
|
||||
// ExpandAlias processes argv to see if it should be rewritten according to a user's aliases.
|
||||
func expandAlias(expansion string, args []string) ([]string, error) {
|
||||
extraArgs := []string{}
|
||||
for i, a := range args {
|
||||
if !strings.Contains(expansion, "$") {
|
||||
extraArgs = append(extraArgs, a)
|
||||
} else {
|
||||
expansion = strings.ReplaceAll(expansion, fmt.Sprintf("$%d", i+1), a)
|
||||
}
|
||||
}
|
||||
|
||||
lingeringRE := regexp.MustCompile(`\$\d`)
|
||||
if lingeringRE.MatchString(expansion) {
|
||||
return nil, fmt.Errorf("not enough arguments for alias: %s", expansion)
|
||||
}
|
||||
|
||||
newArgs, err := shlex.Split(expansion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expanded := append(newArgs, extraArgs...)
|
||||
|
||||
return expanded, nil
|
||||
}
|
||||
|
||||
// ExpandShellAlias processes argv to see if it should be rewritten according to a user's aliases.
|
||||
func expandShellAlias(expansion string, args []string, findShFunc func() (string, error)) ([]string, error) {
|
||||
if findShFunc == nil {
|
||||
findShFunc = findSh
|
||||
}
|
||||
|
||||
shPath, shErr := findShFunc()
|
||||
if shErr != nil {
|
||||
return nil, shErr
|
||||
}
|
||||
|
||||
expanded := []string{shPath, "-c", expansion[1:]}
|
||||
|
||||
if len(args) > 0 {
|
||||
expanded = append(expanded, "--")
|
||||
expanded = append(expanded, args...)
|
||||
}
|
||||
|
||||
return expanded, nil
|
||||
}
|
||||
|
||||
func findSh() (string, error) {
|
||||
shPath, err := findsh.Find()
|
||||
if err != nil {
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "", errors.New("unable to locate sh to execute the shell alias with. The sh.exe interpreter is typically distributed with Git for Windows.")
|
||||
}
|
||||
return "", errors.New("unable to locate sh to execute shell alias with")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return shPath, nil
|
||||
}
|
||||
123
pkg/cmd/root/alias_test.go
Normal file
123
pkg/cmd/root/alias_test.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package root
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExpandAlias(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expansion string
|
||||
args []string
|
||||
wantExpanded []string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "no expansion",
|
||||
expansion: "pr status",
|
||||
args: []string{},
|
||||
wantExpanded: []string{"pr", "status"},
|
||||
},
|
||||
{
|
||||
name: "adding arguments after expansion",
|
||||
expansion: "pr checkout",
|
||||
args: []string{"123"},
|
||||
wantExpanded: []string{"pr", "checkout", "123"},
|
||||
},
|
||||
{
|
||||
name: "not enough arguments for expansion",
|
||||
expansion: `issue list --author="$1" --label="$2"`,
|
||||
args: []string{},
|
||||
wantErr: `not enough arguments for alias: issue list --author="$1" --label="$2"`,
|
||||
},
|
||||
{
|
||||
name: "not enough arguments for expansion 2",
|
||||
expansion: `issue list --author="$1" --label="$2"`,
|
||||
args: []string{"vilmibm"},
|
||||
wantErr: `not enough arguments for alias: issue list --author="vilmibm" --label="$2"`,
|
||||
},
|
||||
{
|
||||
name: "satisfy expansion arguments",
|
||||
expansion: `issue list --author="$1" --label="$2"`,
|
||||
args: []string{"vilmibm", "help wanted"},
|
||||
wantExpanded: []string{"issue", "list", "--author=vilmibm", "--label=help wanted"},
|
||||
},
|
||||
{
|
||||
name: "mixed positional and non-positional arguments",
|
||||
expansion: `issue list --author="$1" --label="$2"`,
|
||||
args: []string{"vilmibm", "epic", "-R", "monalisa/testing"},
|
||||
wantExpanded: []string{"issue", "list", "--author=vilmibm", "--label=epic", "-R", "monalisa/testing"},
|
||||
},
|
||||
{
|
||||
name: "dollar in expansion",
|
||||
expansion: `issue list --author="$1" --assignee="$1"`,
|
||||
args: []string{"$coolmoney$"},
|
||||
wantExpanded: []string{"issue", "list", "--author=$coolmoney$", "--assignee=$coolmoney$"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotExpanded, err := expandAlias(tt.expansion, tt.args)
|
||||
if tt.wantErr != "" {
|
||||
assert.Nil(t, gotExpanded)
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantExpanded, gotExpanded)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandShellAlias(t *testing.T) {
|
||||
findShFunc := func() (string, error) {
|
||||
return "/usr/bin/sh", nil
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
expansion string
|
||||
args []string
|
||||
findSh func() (string, error)
|
||||
wantExpanded []string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "simple expansion",
|
||||
expansion: "!git branch --show-current",
|
||||
args: []string{},
|
||||
findSh: findShFunc,
|
||||
wantExpanded: []string{"/usr/bin/sh", "-c", "git branch --show-current"},
|
||||
},
|
||||
{
|
||||
name: "adding arguments after expansion",
|
||||
expansion: "!git branch checkout",
|
||||
args: []string{"123"},
|
||||
findSh: findShFunc,
|
||||
wantExpanded: []string{"/usr/bin/sh", "-c", "git branch checkout", "--", "123"},
|
||||
},
|
||||
{
|
||||
name: "unable to find sh",
|
||||
expansion: "!git branch --show-current",
|
||||
args: []string{},
|
||||
findSh: func() (string, error) {
|
||||
return "", errors.New("unable to locate sh")
|
||||
},
|
||||
wantErr: "unable to locate sh",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotExpanded, err := expandShellAlias(tt.expansion, tt.args, tt.findSh)
|
||||
if tt.wantErr != "" {
|
||||
assert.Nil(t, gotExpanded)
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantExpanded, gotExpanded)
|
||||
})
|
||||
}
|
||||
}
|
||||
42
pkg/cmd/root/extension.go
Normal file
42
pkg/cmd/root/extension.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package root
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cli/cli/v2/git"
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
"github.com/cli/cli/v2/pkg/extensions"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdExtension(io *iostreams.IOStreams, em extensions.ExtensionManager, ext extensions.Extension) *cobra.Command {
|
||||
var short string
|
||||
if ext.IsLocal() {
|
||||
short = fmt.Sprintf("Local extension gh-%s", ext.Name())
|
||||
} else {
|
||||
path := ext.URL()
|
||||
if u, err := git.ParseURL(ext.URL()); err == nil {
|
||||
if r, err := ghrepo.FromURL(u); err == nil {
|
||||
path = ghrepo.FullName(r)
|
||||
}
|
||||
}
|
||||
short = fmt.Sprintf("Extension %s", path)
|
||||
}
|
||||
return &cobra.Command{
|
||||
Use: ext.Name(),
|
||||
Short: short,
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
args = append([]string{ext.Name()}, args...)
|
||||
if _, err := em.Dispatch(args, io.In, io.Out, io.ErrOut); err != nil {
|
||||
return fmt.Errorf("failed to run extension: %w\n", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
GroupID: "extension",
|
||||
Annotations: map[string]string{
|
||||
"skipAuthCheck": "true",
|
||||
},
|
||||
DisableFlagParsing: true,
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,11 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/internal/text"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -16,13 +18,17 @@ import (
|
|||
func rootUsageFunc(w io.Writer, command *cobra.Command) error {
|
||||
fmt.Fprintf(w, "Usage: %s", command.UseLine())
|
||||
|
||||
subcommands := command.Commands()
|
||||
var subcommands []*cobra.Command
|
||||
for _, c := range command.Commands() {
|
||||
if !c.IsAvailableCommand() {
|
||||
continue
|
||||
}
|
||||
subcommands = append(subcommands, c)
|
||||
}
|
||||
|
||||
if len(subcommands) > 0 {
|
||||
fmt.Fprint(w, "\n\nAvailable commands:\n")
|
||||
for _, c := range subcommands {
|
||||
if c.Hidden {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " %s\n", c.Name())
|
||||
}
|
||||
return nil
|
||||
|
|
@ -82,8 +88,10 @@ func isRootCmd(command *cobra.Command) bool {
|
|||
}
|
||||
|
||||
func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) {
|
||||
flags := command.Flags()
|
||||
|
||||
if isRootCmd(command) {
|
||||
if versionVal, err := command.Flags().GetBool("version"); err == nil && versionVal {
|
||||
if versionVal, err := flags.GetBool("version"); err == nil && versionVal {
|
||||
fmt.Fprint(f.IOStreams.Out, command.Annotations["versionInfo"])
|
||||
return
|
||||
} else if err != nil {
|
||||
|
|
@ -95,8 +103,8 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) {
|
|||
|
||||
cs := f.IOStreams.ColorScheme()
|
||||
|
||||
if isRootCmd(command.Parent()) && len(args) >= 2 && args[1] != "--help" && args[1] != "-h" {
|
||||
nestedSuggestFunc(f.IOStreams.ErrOut, command, args[1])
|
||||
if help, _ := flags.GetBool("help"); !help && !command.Runnable() && len(flags.Args()) > 0 {
|
||||
nestedSuggestFunc(f.IOStreams.ErrOut, command, strings.Join(flags.Args(), " "))
|
||||
hasFailed = true
|
||||
return
|
||||
}
|
||||
|
|
@ -144,14 +152,6 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) {
|
|||
}
|
||||
sort.Strings(helpTopics)
|
||||
helpEntries = append(helpEntries, helpEntry{"HELP TOPICS", strings.Join(helpTopics, "\n")})
|
||||
|
||||
if exts := f.ExtensionManager.List(); len(exts) > 0 {
|
||||
var names []string
|
||||
for _, ext := range exts {
|
||||
names = append(names, ext.Name())
|
||||
}
|
||||
helpEntries = append(helpEntries, helpEntry{"EXTENSION COMMANDS", strings.Join(names, "\n")})
|
||||
}
|
||||
}
|
||||
|
||||
flagUsages := command.LocalFlags().FlagUsages()
|
||||
|
|
@ -189,6 +189,27 @@ Read the manual at https://cli.github.com/manual`})
|
|||
}
|
||||
}
|
||||
|
||||
func authHelp() string {
|
||||
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||
return heredoc.Doc(`
|
||||
gh: To use GitHub CLI in a GitHub Actions workflow, set the GH_TOKEN environment variable. Example:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
`)
|
||||
}
|
||||
|
||||
if os.Getenv("CI") != "" {
|
||||
return heredoc.Doc(`
|
||||
gh: To use GitHub CLI in automation, set the GH_TOKEN environment variable.
|
||||
`)
|
||||
}
|
||||
|
||||
return heredoc.Doc(`
|
||||
To get started with GitHub CLI, please run: gh auth login
|
||||
Alternatively, populate the GH_TOKEN environment variable with a GitHub API authentication token.
|
||||
`)
|
||||
}
|
||||
|
||||
func findCommand(cmd *cobra.Command, name string) *cobra.Command {
|
||||
for _, c := range cmd.Commands() {
|
||||
if c.Name() == name {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package root
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
|
|
@ -10,6 +12,7 @@ import (
|
|||
codespacesAPI "github.com/cli/cli/v2/internal/codespaces/api"
|
||||
actionsCmd "github.com/cli/cli/v2/pkg/cmd/actions"
|
||||
aliasCmd "github.com/cli/cli/v2/pkg/cmd/alias"
|
||||
"github.com/cli/cli/v2/pkg/cmd/alias/shared"
|
||||
apiCmd "github.com/cli/cli/v2/pkg/cmd/api"
|
||||
authCmd "github.com/cli/cli/v2/pkg/cmd/auth"
|
||||
browseCmd "github.com/cli/cli/v2/pkg/cmd/browse"
|
||||
|
|
@ -36,15 +39,29 @@ import (
|
|||
versionCmd "github.com/cli/cli/v2/pkg/cmd/version"
|
||||
workflowCmd "github.com/cli/cli/v2/pkg/cmd/workflow"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/google/shlex"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
||||
type AuthError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (ae *AuthError) Error() string {
|
||||
return ae.err.Error()
|
||||
}
|
||||
|
||||
func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) (*cobra.Command, error) {
|
||||
io := f.IOStreams
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read configuration: %s\n", err)
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "gh <command> <subcommand> [flags]",
|
||||
Short: "GitHub CLI",
|
||||
Long: `Work seamlessly with GitHub from the command line.`,
|
||||
|
||||
Example: heredoc.Doc(`
|
||||
$ gh issue create
|
||||
$ gh repo clone cli/cli
|
||||
|
|
@ -53,6 +70,14 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
|||
Annotations: map[string]string{
|
||||
"versionInfo": versionCmd.Format(version, buildDate),
|
||||
},
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// require that the user is authenticated before running most commands
|
||||
if cmdutil.IsAuthCheckEnabled(cmd) && !cmdutil.CheckAuth(cfg) {
|
||||
fmt.Fprint(io.ErrOut, authHelp())
|
||||
return &AuthError{}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// cmd.SetOut(f.IOStreams.Out) // can't use due to https://github.com/spf13/cobra/issues/1708
|
||||
|
|
@ -85,6 +110,10 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
|||
ID: "actions",
|
||||
Title: "GitHub Actions commands",
|
||||
})
|
||||
cmd.AddGroup(&cobra.Group{
|
||||
ID: "extension",
|
||||
Title: "Extension commands",
|
||||
})
|
||||
|
||||
// Child commands
|
||||
cmd.AddCommand(versionCmd.NewCmdVersion(f, version, buildDate))
|
||||
|
|
@ -136,6 +165,51 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
|||
}
|
||||
}
|
||||
|
||||
// Aliases
|
||||
aliases := cfg.Aliases()
|
||||
validAliasName := shared.ValidAliasNameFunc(cmd)
|
||||
validAliasExpansion := shared.ValidAliasExpansionFunc(cmd)
|
||||
for k, v := range aliases.All() {
|
||||
aliasName := k
|
||||
aliasValue := v
|
||||
if validAliasName(aliasName) && validAliasExpansion(aliasValue) {
|
||||
split, _ := shlex.Split(aliasName)
|
||||
parentCmd, parentArgs, _ := cmd.Find(split)
|
||||
if !parentCmd.ContainsGroup("alias") {
|
||||
parentCmd.AddGroup(&cobra.Group{
|
||||
ID: "alias",
|
||||
Title: "Alias commands",
|
||||
})
|
||||
}
|
||||
if strings.HasPrefix(aliasValue, "!") {
|
||||
shellAliasCmd := NewCmdShellAlias(io, parentArgs[0], aliasValue)
|
||||
parentCmd.AddCommand(shellAliasCmd)
|
||||
parentCmd.ValidArgs = append(parentCmd.ValidArgs, fmt.Sprintf("%s\tShell alias", aliasName))
|
||||
} else {
|
||||
aliasCmd := NewCmdAlias(io, parentArgs[0], aliasValue)
|
||||
split, _ := shlex.Split(aliasValue)
|
||||
child, _, _ := cmd.Find(split)
|
||||
aliasCmd.SetUsageFunc(func(_ *cobra.Command) error {
|
||||
return rootUsageFunc(f.IOStreams.ErrOut, child)
|
||||
})
|
||||
aliasCmd.SetHelpFunc(func(_ *cobra.Command, args []string) {
|
||||
rootHelpFunc(f, child, args)
|
||||
})
|
||||
parentCmd.AddCommand(aliasCmd)
|
||||
parentCmd.ValidArgs = append(parentCmd.ValidArgs, fmt.Sprintf("%s\tAlias for %s", aliasName, aliasValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extensions
|
||||
em := f.ExtensionManager
|
||||
for _, e := range em.List() {
|
||||
extension := e
|
||||
extensionCmd := NewCmdExtension(io, em, e)
|
||||
cmd.AddCommand(extensionCmd)
|
||||
cmd.ValidArgs = append(cmd.ValidArgs, fmt.Sprintf("%s\t%s", extension.Name(), extensionCmd.Short))
|
||||
}
|
||||
|
||||
cmdutil.DisableAuthCheck(cmd)
|
||||
|
||||
// The reference command produces paged output that displays information on every other command.
|
||||
|
|
@ -145,7 +219,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
|||
// is special.
|
||||
referenceCmd.Long = stringifyReference(cmd)
|
||||
referenceCmd.SetHelpFunc(longPager(f.IOStreams))
|
||||
return cmd
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func bareHTTPClient(f *cmdutil.Factory, version string) func() (*http.Client, error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue