Skip to content

Commit 466f9d5

Browse files
bsamekclaude
andauthored
Add wt list command for worktr (#24)
Add a new `wt list` command that outputs all worktree names, one per line. This provides a simple way to see all available worktrees without needing to look at the .worktrees directory directly. Co-authored-by: Claude <noreply@anthropic.com>
1 parent deb9a76 commit 466f9d5

6 files changed

Lines changed: 144 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ wt <command> [options] [args]
117117
| `jump` | Jump to a worktree or repository root |
118118
| `create` | Create a new worktree with branch |
119119
| `remove` | Remove a worktree and its branch (auto-detects if inside worktree) |
120+
| `list` | List all worktrees |
120121
| `gha` | Monitor GitHub Actions status for current branch's PR |
121122
| `completion` | Generate shell completion script (bash, zsh, fish) |
122123

@@ -136,6 +137,7 @@ wt create my-feature # Create worktree for 'my-feature' branch
136137
wt create --hook setup.sh feat # Create worktree, run setup.sh as hook
137138
wt remove my-feature # Remove worktree and branch
138139
wt remove # Remove current worktree (when inside one)
140+
wt list # List all worktrees
139141
wt gha # Monitor GitHub Actions for current branch's PR
140142
wt completion bash # Generate bash completion script
141143
```

completion.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func bashCompletion(w io.Writer) error {
5757
local cur prev words cword
5858
_init_completion || return
5959
60-
local commands="jump create remove gha completion"
60+
local commands="jump create remove list gha completion"
6161
6262
case "${prev}" in
6363
wt)
@@ -120,6 +120,7 @@ _wt() {
120120
'jump:Jump to a worktree or repo root'
121121
'create:Create a new worktree with branch'
122122
'remove:Remove a worktree and its branch'
123+
'list:List all worktrees'
123124
'gha:Monitor GitHub Actions status for current branch PR'
124125
'completion:Generate shell completion script'
125126
)
@@ -176,6 +177,7 @@ complete -c wt -f
176177
complete -c wt -n "__fish_use_subcommand" -a "jump" -d "Jump to a worktree or repo root"
177178
complete -c wt -n "__fish_use_subcommand" -a "create" -d "Create a new worktree with branch"
178179
complete -c wt -n "__fish_use_subcommand" -a "remove" -d "Remove a worktree and its branch"
180+
complete -c wt -n "__fish_use_subcommand" -a "list" -d "List all worktrees"
179181
complete -c wt -n "__fish_use_subcommand" -a "gha" -d "Monitor GitHub Actions status"
180182
complete -c wt -n "__fish_use_subcommand" -a "completion" -d "Generate shell completion script"
181183

list.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
)
7+
8+
// list outputs all worktree names, one per line.
9+
func list(w io.Writer) error {
10+
worktrees, err := listWorktrees()
11+
if err != nil {
12+
return err
13+
}
14+
for _, wt := range worktrees {
15+
fmt.Fprintln(w, wt)
16+
}
17+
return nil
18+
}

list_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestList(t *testing.T) {
11+
// Save original function and restore after test
12+
origListWorktrees := listWorktreesFn
13+
defer func() {
14+
listWorktreesFn = origListWorktrees
15+
}()
16+
17+
t.Run("success with worktrees", func(t *testing.T) {
18+
listWorktreesFn = func() ([]string, error) {
19+
return []string{"feature-a", "feature-b", "bugfix-c"}, nil
20+
}
21+
22+
var buf bytes.Buffer
23+
err := list(&buf)
24+
if err != nil {
25+
t.Errorf("list() unexpected error: %v", err)
26+
}
27+
28+
output := buf.String()
29+
lines := strings.Split(strings.TrimSpace(output), "\n")
30+
if len(lines) != 3 {
31+
t.Errorf("list() output %d lines, want 3", len(lines))
32+
}
33+
expected := map[string]bool{"feature-a": true, "feature-b": true, "bugfix-c": true}
34+
for _, line := range lines {
35+
if !expected[line] {
36+
t.Errorf("list() unexpected line: %q", line)
37+
}
38+
}
39+
})
40+
41+
t.Run("success with no worktrees", func(t *testing.T) {
42+
listWorktreesFn = func() ([]string, error) {
43+
return []string{}, nil
44+
}
45+
46+
var buf bytes.Buffer
47+
err := list(&buf)
48+
if err != nil {
49+
t.Errorf("list() unexpected error: %v", err)
50+
}
51+
if buf.Len() != 0 {
52+
t.Errorf("list() wrote output for empty list: %q", buf.String())
53+
}
54+
})
55+
56+
t.Run("error from listWorktrees", func(t *testing.T) {
57+
listWorktreesFn = func() ([]string, error) {
58+
return nil, errors.New("not in a git repository")
59+
}
60+
61+
var buf bytes.Buffer
62+
err := list(&buf)
63+
if err == nil || err.Error() != "not in a git repository" {
64+
t.Errorf("list() error = %v, want 'not in a git repository'", err)
65+
}
66+
})
67+
}

main.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ var errShowHelp = errors.New("show help")
1414
var exitFn = os.Exit
1515

1616
// validCommands lists all valid command names
17-
var validCommands = []string{"create", "remove", "jump", "gha", "completion", "__complete"}
17+
var validCommands = []string{"create", "remove", "jump", "list", "gha", "completion", "__complete"}
1818

1919
func usageText() string {
2020
return `Usage: wt <command> [options] [args]
@@ -23,6 +23,7 @@ Commands:
2323
jump Jump to a worktree or repository root
2424
create Create a new worktree with branch
2525
remove Remove a worktree and its branch (auto-detects if inside worktree)
26+
list List all worktrees
2627
gha Monitor GitHub Actions status for current branch's PR
2728
completion Generate shell completion script (bash, zsh, fish)
2829
@@ -37,6 +38,7 @@ Examples:
3738
wt create --hook setup.sh feat Create worktree, run setup.sh as hook
3839
wt remove my-feature Remove worktree and branch
3940
wt remove Remove current worktree (when inside one)
41+
wt list List all worktrees
4042
wt gha Wait for GHA checks on current branch's PR
4143
wt completion bash Generate bash completion script
4244
`
@@ -139,6 +141,14 @@ func parseArgs(args []string) (cmd string, name string, hookPath string, err err
139141
return cmd, "", hookPath, nil
140142
}
141143

144+
// list command takes no additional arguments
145+
if cmd == "list" {
146+
if idx < len(args) {
147+
return "", "", "", fmt.Errorf("unexpected argument: %s", args[idx])
148+
}
149+
return cmd, "", hookPath, nil
150+
}
151+
142152
// completion command takes a shell name
143153
if cmd == "completion" {
144154
if idx >= len(args) {
@@ -210,6 +220,8 @@ func run(args []string) error {
210220
return create(name, hookPath)
211221
case "remove":
212222
return runRemove(name)
223+
case "list":
224+
return list(os.Stdout)
213225
case "gha":
214226
return gha()
215227
case "completion":

main_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func TestIsValidCommand(t *testing.T) {
5555
{"create", "create", true},
5656
{"remove", "remove", true},
5757
{"jump", "jump", true},
58+
{"list", "list", true},
5859
{"gha", "gha", true},
5960
{"completion", "completion", true},
6061
{"__complete", "__complete", true},
@@ -321,6 +322,18 @@ func TestParseArgs(t *testing.T) {
321322
args: []string{"gha", "extra"},
322323
wantErrMsg: "unexpected argument: extra",
323324
},
325+
{
326+
name: "list command no args",
327+
args: []string{"list"},
328+
wantCmd: "list",
329+
wantName: "",
330+
wantHook: DefaultHook,
331+
},
332+
{
333+
name: "list command with extra arg",
334+
args: []string{"list", "extra"},
335+
wantErrMsg: "unexpected argument: extra",
336+
},
324337
{
325338
name: "completion command bash",
326339
args: []string{"completion", "bash"},
@@ -558,6 +571,34 @@ func TestRun(t *testing.T) {
558571
}
559572
})
560573

574+
t.Run("list command calls list", func(t *testing.T) {
575+
origListWorktrees := listWorktreesFn
576+
defer func() { listWorktreesFn = origListWorktrees }()
577+
578+
listWorktreesFn = func() ([]string, error) {
579+
return []string{"feature-a", "feature-b"}, nil
580+
}
581+
582+
err := run([]string{"list"})
583+
if err != nil {
584+
t.Errorf("run() unexpected error: %v", err)
585+
}
586+
})
587+
588+
t.Run("list command with error", func(t *testing.T) {
589+
origListWorktrees := listWorktreesFn
590+
defer func() { listWorktreesFn = origListWorktrees }()
591+
592+
listWorktreesFn = func() ([]string, error) {
593+
return nil, errors.New("mock: not in git repo")
594+
}
595+
596+
err := run([]string{"list"})
597+
if err == nil || err.Error() != "mock: not in git repo" {
598+
t.Errorf("run() error = %v, want 'mock: not in git repo'", err)
599+
}
600+
})
601+
561602
t.Run("completion command calls completion", func(t *testing.T) {
562603
err := run([]string{"completion", "bash"})
563604
if err != nil {

0 commit comments

Comments
 (0)