Skip to content

Commit 60f3391

Browse files
timvwclaude
andcommitted
feat: add interactive selection when no arguments provided
Add interactive dropdown selection for checkout, remove, pr, and mr commands when no argument is provided: - `wt checkout`: Shows list of available branches (without worktrees) - `wt remove`: Shows list of existing worktrees - `wt pr`: Shows list of open GitHub PRs (using gh CLI) - `wt mr`: Shows list of open GitLab MRs (using glab CLI) Uses promptui library for the interactive selection interface. Users can now run commands without arguments and select from a list: wt checkout # Pick from available branches wt rm # Pick from existing worktrees wt pr # Pick from open PRs wt mr # Pick from open MRs This improves UX by making discovery easier and reducing the need to remember branch names or PR/MR numbers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 7db6542 commit 60f3391

5 files changed

Lines changed: 279 additions & 12 deletions

File tree

checksums.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
31a9f6f8d8ff1adc85c4a43b7f1d71e1a021d94066bfb6b6b0a02537c6bd6612 wt-0.1.6.aarch64_linux.bottle.tar.gz
2+
48168444992bee9a78c48f39ef17417276cba513b4e1b34a42374fa8bd4825ad wt-0.1.6.arm64_sonoma.bottle.tar.gz
3+
2903b52578c62fa0428a4b2d41e2e7b17d6e3778667976376a4a2f5864445da2 wt-0.1.6.ventura.bottle.tar.gz
4+
848e3b76d68070bb3e95d282455105405971da1d5b911ea1c2c9d19dcf0908e4 wt-0.1.6.x86_64_linux.bottle.tar.gz
5+
28284172182f60fba604294ceb585fc6cfed93859df708dcdaf5f06ec32393d0 wt-darwin-amd64
6+
8ba9fd5a93e086fa196eaeee45bb852186578de73bccba3484c96301a9c5a86a wt-darwin-arm64
7+
bbad2e0eeef2df95b5cc81454e2efc304be2c9bc527a372950e02d3dad5385b8 wt-linux-amd64
8+
db6f96abb3ec30ef24223bc01f8e8b68e76249b6381b2a7621faa5c6261f0a2f wt-linux-arm64
9+
1846d9bff922af87c9386ecb5581bb4b4a61f4bd8edd6090c9c4c86f219819dc wt-windows-amd64.exe

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ go 1.21
55
require github.com/spf13/cobra v1.8.0
66

77
require (
8+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
89
github.com/inconshreveable/mousetrap v1.1.0 // indirect
10+
github.com/manifoldco/promptui v0.9.0 // indirect
911
github.com/spf13/pflag v1.0.5 // indirect
12+
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b // indirect
1013
)

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1+
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
2+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
3+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
4+
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
15
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
26
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
37
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
8+
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
9+
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
410
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
511
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
612
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
713
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
814
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
15+
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4=
16+
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
917
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1018
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

Lines changed: 198 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"regexp"
99
"strings"
1010

11+
"github.com/manifoldco/promptui"
1112
"github.com/spf13/cobra"
1213
)
1314

@@ -154,15 +155,125 @@ func printCDMarker(path string) {
154155
fmt.Printf("TREE_ME_CD:%s\n", path)
155156
}
156157

158+
func getAvailableBranches() ([]string, error) {
159+
// Get all branches
160+
cmd := exec.Command("git", "branch", "-a", "--format=%(refname:short)")
161+
output, err := cmd.Output()
162+
if err != nil {
163+
return nil, err
164+
}
165+
166+
branches := []string{}
167+
for _, line := range strings.Split(string(output), "\n") {
168+
branch := strings.TrimSpace(line)
169+
if branch == "" || strings.HasPrefix(branch, "origin/HEAD") {
170+
continue
171+
}
172+
// Remove origin/ prefix for remote branches
173+
branch = strings.TrimPrefix(branch, "origin/")
174+
// Check if this branch doesn't already have a worktree
175+
if _, exists := worktreeExists(branch); !exists {
176+
branches = append(branches, branch)
177+
}
178+
}
179+
return branches, nil
180+
}
181+
182+
func getExistingWorktreeBranches() ([]string, error) {
183+
cmd := exec.Command("git", "worktree", "list")
184+
output, err := cmd.Output()
185+
if err != nil {
186+
return nil, err
187+
}
188+
189+
branches := []string{}
190+
lines := strings.Split(string(output), "\n")
191+
for _, line := range lines[1:] { // Skip first line (main worktree)
192+
if line == "" {
193+
continue
194+
}
195+
// Extract branch name from [branch] format
196+
if matches := regexp.MustCompile(`\[([^\]]+)\]`).FindStringSubmatch(line); matches != nil {
197+
branches = append(branches, matches[1])
198+
}
199+
}
200+
return branches, nil
201+
}
202+
203+
func getOpenPRs() ([]string, []string, error) {
204+
cmd := exec.Command("gh", "pr", "list", "--json", "number,title", "--jq", ".[] | \"\\(.number)\\t\\(.title)\"")
205+
output, err := cmd.Output()
206+
if err != nil {
207+
return nil, nil, err
208+
}
209+
210+
var numbers []string
211+
var labels []string
212+
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
213+
if line == "" {
214+
continue
215+
}
216+
parts := strings.SplitN(line, "\t", 2)
217+
if len(parts) == 2 {
218+
numbers = append(numbers, parts[0])
219+
labels = append(labels, fmt.Sprintf("#%s: %s", parts[0], parts[1]))
220+
}
221+
}
222+
return numbers, labels, nil
223+
}
224+
225+
func getOpenMRs() ([]string, []string, error) {
226+
cmd := exec.Command("glab", "mr", "list")
227+
output, err := cmd.Output()
228+
if err != nil {
229+
return nil, nil, err
230+
}
231+
232+
var numbers []string
233+
var labels []string
234+
// Parse glab output: !123 title (branch) ← (target)
235+
mrRegex := regexp.MustCompile(`^!(\d+)\s+[^\s]+\s+(.+?)\s+\(`)
236+
for _, line := range strings.Split(string(output), "\n") {
237+
if matches := mrRegex.FindStringSubmatch(line); matches != nil {
238+
numbers = append(numbers, matches[1])
239+
labels = append(labels, fmt.Sprintf("!%s: %s", matches[1], strings.TrimSpace(matches[2])))
240+
}
241+
}
242+
return numbers, labels, nil
243+
}
244+
157245
// Commands
158246

159247
var checkoutCmd = &cobra.Command{
160-
Use: "checkout <branch>",
248+
Use: "checkout [branch]",
161249
Aliases: []string{"co"},
162250
Short: "Checkout existing branch in new worktree",
163-
Args: cobra.ExactArgs(1),
251+
Args: cobra.RangeArgs(0, 1),
164252
RunE: func(cmd *cobra.Command, args []string) error {
165-
branch := args[0]
253+
var branch string
254+
255+
// Interactive selection if no branch provided
256+
if len(args) == 0 {
257+
branches, err := getAvailableBranches()
258+
if err != nil {
259+
return fmt.Errorf("failed to get branches: %w", err)
260+
}
261+
if len(branches) == 0 {
262+
return fmt.Errorf("no available branches to checkout")
263+
}
264+
265+
prompt := promptui.Select{
266+
Label: "Select branch to checkout",
267+
Items: branches,
268+
}
269+
_, result, err := prompt.Run()
270+
if err != nil {
271+
return fmt.Errorf("selection cancelled")
272+
}
273+
branch = result
274+
} else {
275+
branch = args[0]
276+
}
166277
repo, err := getRepoName()
167278
if err != nil {
168279
return err
@@ -236,36 +347,88 @@ var createCmd = &cobra.Command{
236347
}
237348

238349
var prCmd = &cobra.Command{
239-
Use: "pr <number|url>",
350+
Use: "pr [number|url]",
240351
Short: "Checkout GitHub PR in worktree (uses gh CLI)",
241352
Long: `Checkout a GitHub Pull Request in a worktree.
242353
243354
Uses the 'gh' CLI to fetch and checkout pull requests.
244355
For GitLab Merge Requests, use 'wt mr' instead.
245356
246357
Examples:
358+
wt pr # Interactive PR selection
247359
wt pr 123 # GitHub PR number
248360
wt pr https://github.com/org/repo/pull/123 # GitHub PR URL`,
249-
Args: cobra.ExactArgs(1),
361+
Args: cobra.RangeArgs(0, 1),
250362
RunE: func(cmd *cobra.Command, args []string) error {
251-
return checkoutPROrMR(args[0], RemoteGitHub)
363+
var input string
364+
365+
// Interactive selection if no PR provided
366+
if len(args) == 0 {
367+
numbers, labels, err := getOpenPRs()
368+
if err != nil {
369+
return fmt.Errorf("failed to get PRs: %w (is 'gh' CLI installed?)", err)
370+
}
371+
if len(labels) == 0 {
372+
return fmt.Errorf("no open PRs found")
373+
}
374+
375+
prompt := promptui.Select{
376+
Label: "Select Pull Request",
377+
Items: labels,
378+
}
379+
idx, _, err := prompt.Run()
380+
if err != nil {
381+
return fmt.Errorf("selection cancelled")
382+
}
383+
input = numbers[idx]
384+
} else {
385+
input = args[0]
386+
}
387+
388+
return checkoutPROrMR(input, RemoteGitHub)
252389
},
253390
}
254391

255392
var mrCmd = &cobra.Command{
256-
Use: "mr <number|url>",
393+
Use: "mr [number|url]",
257394
Short: "Checkout GitLab MR in worktree (uses glab CLI)",
258395
Long: `Checkout a GitLab Merge Request in a worktree.
259396
260397
Uses the 'glab' CLI to fetch and checkout merge requests.
261398
For GitHub Pull Requests, use 'wt pr' instead.
262399
263400
Examples:
401+
wt mr # Interactive MR selection
264402
wt mr 123 # GitLab MR number
265403
wt mr https://gitlab.com/org/repo/-/merge_requests/123 # GitLab MR URL`,
266-
Args: cobra.ExactArgs(1),
404+
Args: cobra.RangeArgs(0, 1),
267405
RunE: func(cmd *cobra.Command, args []string) error {
268-
return checkoutPROrMR(args[0], RemoteGitLab)
406+
var input string
407+
408+
// Interactive selection if no MR provided
409+
if len(args) == 0 {
410+
numbers, labels, err := getOpenMRs()
411+
if err != nil {
412+
return fmt.Errorf("failed to get MRs: %w (is 'glab' CLI installed?)", err)
413+
}
414+
if len(labels) == 0 {
415+
return fmt.Errorf("no open MRs found")
416+
}
417+
418+
prompt := promptui.Select{
419+
Label: "Select Merge Request",
420+
Items: labels,
421+
}
422+
idx, _, err := prompt.Run()
423+
if err != nil {
424+
return fmt.Errorf("selection cancelled")
425+
}
426+
input = numbers[idx]
427+
} else {
428+
input = args[0]
429+
}
430+
431+
return checkoutPROrMR(input, RemoteGitLab)
269432
},
270433
}
271434

@@ -340,12 +503,35 @@ var listCmd = &cobra.Command{
340503
}
341504

342505
var removeCmd = &cobra.Command{
343-
Use: "remove <branch>",
506+
Use: "remove [branch]",
344507
Aliases: []string{"rm"},
345508
Short: "Remove a worktree",
346-
Args: cobra.ExactArgs(1),
509+
Args: cobra.RangeArgs(0, 1),
347510
RunE: func(cmd *cobra.Command, args []string) error {
348-
branch := args[0]
511+
var branch string
512+
513+
// Interactive selection if no branch provided
514+
if len(args) == 0 {
515+
branches, err := getExistingWorktreeBranches()
516+
if err != nil {
517+
return fmt.Errorf("failed to get worktrees: %w", err)
518+
}
519+
if len(branches) == 0 {
520+
return fmt.Errorf("no worktrees to remove")
521+
}
522+
523+
prompt := promptui.Select{
524+
Label: "Select worktree to remove",
525+
Items: branches,
526+
}
527+
_, result, err := prompt.Run()
528+
if err != nil {
529+
return fmt.Errorf("selection cancelled")
530+
}
531+
branch = result
532+
} else {
533+
branch = args[0]
534+
}
349535

350536
existingPath, exists := worktreeExists(branch)
351537
if !exists {

test-gitlab-support.sh

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/bin/bash
2+
set -e
3+
4+
echo "=== Testing GitLab/GitHub Support ==="
5+
echo ""
6+
7+
# Build if needed
8+
if [ ! -f bin/wt ]; then
9+
echo "Building wt..."
10+
go build -o bin/wt .
11+
fi
12+
13+
# Test 1: GitHub remote detection
14+
echo "Test 1: GitHub remote detection (current repo)"
15+
echo "Remote URL: $(git remote get-url origin)"
16+
echo ""
17+
echo "Testing: ./bin/wt pr 1"
18+
echo "(This will fail to fetch, but shows it detects GitHub and looks for 'gh')"
19+
./bin/wt pr 1 2>&1 | head -5 || true
20+
echo ""
21+
echo "---"
22+
echo ""
23+
24+
# Test 2: Temporarily change to GitLab remote for testing
25+
echo "Test 2: GitLab remote detection (simulated)"
26+
echo "Saving current remote..."
27+
ORIGINAL_REMOTE=$(git remote get-url origin)
28+
echo "Temporarily changing remote to GitLab..."
29+
git remote set-url origin https://gitlab.com/test/project.git
30+
31+
echo "Remote URL: $(git remote get-url origin)"
32+
echo ""
33+
echo "Testing: ./bin/wt mr 1"
34+
echo "(This will fail to fetch, but shows it detects GitLab and looks for 'glab')"
35+
./bin/wt mr 1 2>&1 | head -5 || true
36+
echo ""
37+
38+
# Restore original remote
39+
echo "Restoring original remote..."
40+
git remote set-url origin "$ORIGINAL_REMOTE"
41+
echo "Remote restored: $(git remote get-url origin)"
42+
echo ""
43+
echo "---"
44+
echo ""
45+
46+
# Test 3: URL parsing
47+
echo "Test 3: URL parsing"
48+
echo "GitHub PR URL should extract number 123:"
49+
./bin/wt pr --help | grep -A 1 "github.com" || true
50+
echo ""
51+
echo "GitLab MR URL should extract number 123:"
52+
./bin/wt pr --help | grep -A 1 "gitlab.com" || true
53+
echo ""
54+
55+
# Test 4: Command aliases
56+
echo "Test 4: Command aliases"
57+
echo "'wt pr' and 'wt mr' are aliases:"
58+
./bin/wt pr --help | grep "Aliases:" -A 1
59+
echo ""
60+
61+
echo "=== All Tests Complete ==="

0 commit comments

Comments
 (0)