Skip to content

Commit 1f61685

Browse files
committed
Merge remote-tracking branch 'origin' into auth-split
2 parents b48237a + 7fc8677 commit 1f61685

File tree

6 files changed

+569
-18
lines changed

6 files changed

+569
-18
lines changed

cmd/gh/main.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,28 @@ func main() {
3030

3131
hasDebug := os.Getenv("DEBUG") != ""
3232

33+
stderr := utils.NewColorable(os.Stderr)
34+
35+
expandedArgs := []string{}
36+
if len(os.Args) > 0 {
37+
expandedArgs = os.Args[1:]
38+
}
39+
40+
cmd, _, err := command.RootCmd.Traverse(expandedArgs)
41+
if err != nil || cmd == command.RootCmd {
42+
originalArgs := expandedArgs
43+
expandedArgs, err = command.ExpandAlias(os.Args)
44+
if err != nil {
45+
fmt.Fprintf(stderr, "failed to process aliases: %s\n", err)
46+
os.Exit(2)
47+
}
48+
if hasDebug {
49+
fmt.Fprintf(stderr, "%v -> %v\n", originalArgs, expandedArgs)
50+
}
51+
}
52+
53+
command.RootCmd.SetArgs(expandedArgs)
54+
3355
if cmd, err := command.RootCmd.ExecuteC(); err != nil {
3456
printError(os.Stderr, err, cmd, hasDebug)
3557
os.Exit(1)

command/alias.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package command
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/cli/cli/utils"
8+
"github.com/google/shlex"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func init() {
13+
RootCmd.AddCommand(aliasCmd)
14+
aliasCmd.AddCommand(aliasSetCmd)
15+
}
16+
17+
var aliasCmd = &cobra.Command{
18+
Use: "alias",
19+
Short: "Create shortcuts for gh commands",
20+
}
21+
22+
var aliasSetCmd = &cobra.Command{
23+
Use: "set <alias> <expansion>",
24+
// NB: Even when inside of a single-quoted string, cobra was noticing and parsing any flags
25+
// used in an alias expansion string argument. Since this command needs no flags, I disabled their
26+
// parsing. If we ever want to add flags to alias set we'll have to figure this out. I tested on
27+
// linux in various shells against cobra 1.0; others on macos did /not/ see the same behavior.
28+
DisableFlagParsing: true,
29+
Short: "Create a shortcut for a gh command",
30+
Long: `This command lets you write your own shortcuts for running gh. They can be simple strings or accept placeholder arguments.`,
31+
Example: `
32+
gh alias set pv 'pr view'
33+
# gh pv -w 123 -> gh pr view -w 123.
34+
35+
gh alias set bugs 'issue list --label="bugs"'
36+
# gh bugs -> gh issue list --label="bugs".
37+
38+
gh alias set epicsBy 'issue list --author="$1" --label="epic"'
39+
# gh epicsBy vilmibm -> gh issue list --author="$1" --label="epic"
40+
`,
41+
Args: cobra.MinimumNArgs(2),
42+
RunE: aliasSet,
43+
}
44+
45+
func aliasSet(cmd *cobra.Command, args []string) error {
46+
ctx := contextForCommand(cmd)
47+
cfg, err := ctx.Config()
48+
if err != nil {
49+
return err
50+
}
51+
52+
aliasCfg, err := cfg.Aliases()
53+
if err != nil {
54+
return err
55+
}
56+
57+
alias := args[0]
58+
expansion := processArgs(args[1:])
59+
60+
expansionStr := strings.Join(expansion, " ")
61+
62+
out := colorableOut(cmd)
63+
fmt.Fprintf(out, "- Adding alias for %s: %s\n", utils.Bold(alias), utils.Bold(expansionStr))
64+
65+
if validCommand([]string{alias}) {
66+
return fmt.Errorf("could not create alias: %q is already a gh command", alias)
67+
}
68+
69+
if !validCommand(expansion) {
70+
return fmt.Errorf("could not create alias: %s does not correspond to a gh command", utils.Bold(expansionStr))
71+
}
72+
73+
successMsg := fmt.Sprintf("%s Added alias.", utils.Green("✓"))
74+
75+
if aliasCfg.Exists(alias) {
76+
successMsg = fmt.Sprintf("%s Changed alias %s from %s to %s",
77+
utils.Green("✓"),
78+
utils.Bold(alias),
79+
utils.Bold(aliasCfg.Get(alias)),
80+
utils.Bold(expansionStr),
81+
)
82+
}
83+
84+
err = aliasCfg.Add(alias, expansionStr)
85+
if err != nil {
86+
return fmt.Errorf("could not create alias: %s", err)
87+
}
88+
89+
fmt.Fprintln(out, successMsg)
90+
91+
return nil
92+
}
93+
94+
func validCommand(expansion []string) bool {
95+
cmd, _, err := RootCmd.Traverse(expansion)
96+
return err == nil && cmd != RootCmd
97+
}
98+
99+
func processArgs(args []string) []string {
100+
if len(args) == 1 {
101+
split, _ := shlex.Split(args[0])
102+
return split
103+
}
104+
105+
newArgs := []string{}
106+
for _, a := range args {
107+
if !strings.HasPrefix(a, "-") && strings.Contains(a, " ") {
108+
a = fmt.Sprintf("%q", a)
109+
}
110+
newArgs = append(newArgs, a)
111+
}
112+
113+
return newArgs
114+
}

command/alias_test.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package command
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
8+
"github.com/cli/cli/internal/config"
9+
"github.com/cli/cli/test"
10+
)
11+
12+
func TestAliasSet_gh_command(t *testing.T) {
13+
initBlankContext("", "OWNER/REPO", "trunk")
14+
15+
mainBuf := bytes.Buffer{}
16+
hostsBuf := bytes.Buffer{}
17+
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
18+
19+
_, err := RunCommand("alias set pr pr status")
20+
if err == nil {
21+
t.Fatal("expected error")
22+
}
23+
24+
eq(t, err.Error(), `could not create alias: "pr" is already a gh command`)
25+
}
26+
27+
func TestAliasSet_empty_aliases(t *testing.T) {
28+
cfg := `---
29+
aliases:
30+
editor: vim
31+
`
32+
initBlankContext(cfg, "OWNER/REPO", "trunk")
33+
34+
mainBuf := bytes.Buffer{}
35+
hostsBuf := bytes.Buffer{}
36+
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
37+
38+
output, err := RunCommand("alias set co pr checkout")
39+
40+
if err != nil {
41+
t.Fatalf("unexpected error: %s", err)
42+
}
43+
44+
test.ExpectLines(t, output.String(), "Added alias")
45+
46+
expected := `aliases:
47+
co: pr checkout
48+
editor: vim
49+
`
50+
eq(t, mainBuf.String(), expected)
51+
}
52+
53+
func TestAliasSet_existing_alias(t *testing.T) {
54+
cfg := `---
55+
hosts:
56+
github.com:
57+
user: OWNER
58+
oauth_token: token123
59+
aliases:
60+
co: pr checkout
61+
`
62+
initBlankContext(cfg, "OWNER/REPO", "trunk")
63+
64+
mainBuf := bytes.Buffer{}
65+
hostsBuf := bytes.Buffer{}
66+
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
67+
68+
output, err := RunCommand("alias set co pr checkout -Rcool/repo")
69+
70+
if err != nil {
71+
t.Fatalf("unexpected error: %s", err)
72+
}
73+
74+
test.ExpectLines(t, output.String(), "Changed alias co from pr checkout to pr checkout -Rcool/repo")
75+
}
76+
77+
func TestAliasSet_space_args(t *testing.T) {
78+
initBlankContext("", "OWNER/REPO", "trunk")
79+
80+
mainBuf := bytes.Buffer{}
81+
hostsBuf := bytes.Buffer{}
82+
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
83+
84+
output, err := RunCommand(`alias set il issue list -l 'cool story'`)
85+
86+
if err != nil {
87+
t.Fatalf("unexpected error: %s", err)
88+
}
89+
90+
test.ExpectLines(t, output.String(), `Adding alias for il: issue list -l "cool story"`)
91+
92+
test.ExpectLines(t, mainBuf.String(), `il: issue list -l "cool story"`)
93+
}
94+
95+
func TestAliasSet_arg_processing(t *testing.T) {
96+
initBlankContext("", "OWNER/REPO", "trunk")
97+
cases := []struct {
98+
Cmd string
99+
ExpectedOutputLine string
100+
ExpectedConfigLine string
101+
}{
102+
{"alias set co pr checkout", "- Adding alias for co: pr checkout", "co: pr checkout"},
103+
104+
{`alias set il "issue list"`, "- Adding alias for il: issue list", "il: issue list"},
105+
106+
{`alias set iz 'issue list'`, "- Adding alias for iz: issue list", "iz: issue list"},
107+
108+
{`alias set iy issue list --author=\$1 --label=\$2`,
109+
`- Adding alias for iy: issue list --author=\$1 --label=\$2`,
110+
`iy: issue list --author=\$1 --label=\$2`},
111+
112+
{`alias set ii 'issue list --author="$1" --label="$2"'`,
113+
`- Adding alias for ii: issue list --author=\$1 --label=\$2`,
114+
`ii: issue list --author=\$1 --label=\$2`},
115+
116+
{`alias set ix issue list --author='$1' --label='$2'`,
117+
`- Adding alias for ix: issue list --author=\$1 --label=\$2`,
118+
`ix: issue list --author=\$1 --label=\$2`},
119+
}
120+
121+
for _, c := range cases {
122+
mainBuf := bytes.Buffer{}
123+
hostsBuf := bytes.Buffer{}
124+
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
125+
126+
output, err := RunCommand(c.Cmd)
127+
if err != nil {
128+
t.Fatalf("got unexpected error running %s: %s", c.Cmd, err)
129+
}
130+
131+
test.ExpectLines(t, output.String(), c.ExpectedOutputLine)
132+
test.ExpectLines(t, mainBuf.String(), c.ExpectedConfigLine)
133+
}
134+
}
135+
136+
func TestAliasSet_init_alias_cfg(t *testing.T) {
137+
cfg := `---
138+
editor: vim
139+
`
140+
initBlankContext(cfg, "OWNER/REPO", "trunk")
141+
142+
mainBuf := bytes.Buffer{}
143+
hostsBuf := bytes.Buffer{}
144+
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
145+
146+
output, err := RunCommand("alias set diff pr diff")
147+
if err != nil {
148+
t.Fatalf("unexpected error: %s", err)
149+
}
150+
expected := `editor: vim
151+
aliases:
152+
diff: pr diff
153+
`
154+
155+
test.ExpectLines(t, output.String(), "Adding alias for diff: pr diff", "Added alias.")
156+
eq(t, mainBuf.String(), expected)
157+
}
158+
159+
func TestAliasSet_existing_aliases(t *testing.T) {
160+
cfg := `---
161+
aliases:
162+
foo: bar
163+
`
164+
initBlankContext(cfg, "OWNER/REPO", "trunk")
165+
166+
mainBuf := bytes.Buffer{}
167+
hostsBuf := bytes.Buffer{}
168+
defer config.StubWriteConfig(&mainBuf, &hostsBuf)()
169+
170+
output, err := RunCommand("alias set view pr view")
171+
if err != nil {
172+
t.Fatalf("unexpected error: %s", err)
173+
}
174+
expected := `aliases:
175+
foo: bar
176+
view: pr view
177+
`
178+
179+
test.ExpectLines(t, output.String(), "Adding alias for view: pr view", "Added alias.")
180+
eq(t, mainBuf.String(), expected)
181+
182+
}
183+
184+
func TestExpandAlias(t *testing.T) {
185+
cfg := `---
186+
hosts:
187+
github.com:
188+
user: OWNER
189+
oauth_token: token123
190+
aliases:
191+
co: pr checkout
192+
il: issue list --author="$1" --label="$2"
193+
ia: issue list --author="$1" --assignee="$1"
194+
`
195+
initBlankContext(cfg, "OWNER/REPO", "trunk")
196+
for _, c := range []struct {
197+
Args string
198+
ExpectedArgs []string
199+
Err string
200+
}{
201+
{"gh co", []string{"pr", "checkout"}, ""},
202+
{"gh il", nil, `not enough arguments for alias: issue list --author="$1" --label="$2"`},
203+
{"gh il vilmibm", nil, `not enough arguments for alias: issue list --author="vilmibm" --label="$2"`},
204+
{"gh co 123", []string{"pr", "checkout", "123"}, ""},
205+
{"gh il vilmibm epic", []string{"issue", "list", `--author=vilmibm`, `--label=epic`}, ""},
206+
{"gh ia vilmibm", []string{"issue", "list", `--author=vilmibm`, `--assignee=vilmibm`}, ""},
207+
{"gh ia $coolmoney$", []string{"issue", "list", `--author=$coolmoney$`, `--assignee=$coolmoney$`}, ""},
208+
{"gh pr status", []string{"pr", "status"}, ""},
209+
{"gh il vilmibm epic -R vilmibm/testing", []string{"issue", "list", "--author=vilmibm", "--label=epic", "-R", "vilmibm/testing"}, ""},
210+
{"gh dne", []string{"dne"}, ""},
211+
{"gh", []string{}, ""},
212+
{"", []string{}, ""},
213+
} {
214+
args := []string{}
215+
if c.Args != "" {
216+
args = strings.Split(c.Args, " ")
217+
}
218+
219+
out, err := ExpandAlias(args)
220+
221+
if err == nil && c.Err != "" {
222+
t.Errorf("expected error %s for %s", c.Err, c.Args)
223+
continue
224+
}
225+
226+
if err != nil {
227+
eq(t, err.Error(), c.Err)
228+
continue
229+
}
230+
231+
eq(t, out, c.ExpectedArgs)
232+
}
233+
}
234+
235+
func TestAliasSet_invalid_command(t *testing.T) {
236+
initBlankContext("", "OWNER/REPO", "trunk")
237+
_, err := RunCommand("alias set co pe checkout")
238+
if err == nil {
239+
t.Fatal("expected error")
240+
}
241+
242+
eq(t, err.Error(), "could not create alias: pe checkout does not correspond to a gh command")
243+
}

0 commit comments

Comments
 (0)