Skip to content

Commit 780af9d

Browse files
authored
Merge pull request #27 from satococoa/fix-completion-bug
fix: repair shell completion flag handling
2 parents 7025099 + de85bdf commit 780af9d

17 files changed

+1147
-42
lines changed

README.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ worktree. No more terminal tab confusion.
6767
- Linux (x86_64 or ARM64)
6868
- macOS (Apple Silicon M1/M2/M3)
6969
- One of the following shells (for completion support):
70-
- Bash
70+
- Bash (4+/5.x) with bash-completion v2
7171
- Zsh
7272
- Fish
7373

@@ -252,13 +252,18 @@ or any other worktree).
252252

253253
#### If installed via Homebrew
254254

255-
No manual setup required. Homebrew installs a tiny bootstrapper that runs `wtp shell-init <shell>` the first time you press `TAB` after typing `wtp`. That lazy call gives you both tab completion and the `wtp cd` integration for the rest of the session—no rc edits needed.
255+
No manual setup required. Homebrew installs a tiny bootstrapper that runs
256+
`wtp shell-init <shell>` the first time you press `TAB` after typing `wtp`. That
257+
lazy call gives you both tab completion and the `wtp cd` integration for the
258+
rest of the session—no rc edits needed.
256259

257-
Need to refresh inside an existing shell? Just run `wtp shell-init <shell>` yourself.
260+
Need to refresh inside an existing shell? Just run `wtp shell-init <shell>`
261+
yourself.
258262

259263
#### If installed via go install
260264

261-
Add a single line to your shell configuration file to enable both completion and shell integration:
265+
Add a single line to your shell configuration file to enable both completion and
266+
shell integration:
262267

263268
```bash
264269
# Bash: Add to ~/.bashrc or ~/.bash_profile
@@ -271,13 +276,20 @@ eval "$(wtp shell-init zsh)"
271276
wtp shell-init fish | source
272277
```
273278

279+
> **Note:** Bash completion requires bash-completion v2. On macOS, install
280+
> Homebrew’s Bash 5.x and `bash-completion@2`, then
281+
> `source /opt/homebrew/etc/profile.d/bash_completion.sh` (or the path shown
282+
> after installation) before enabling the one-liner above.
283+
274284
After reloading your shell you get the same experience as Homebrew users.
275285

276286
### Navigation with wtp cd
277287

278-
The `wtp cd` command outputs the absolute path to a worktree. You can use it in two ways:
288+
The `wtp cd` command outputs the absolute path to a worktree. You can use it in
289+
two ways:
279290

280291
#### Direct Usage
292+
281293
```bash
282294
# Change to a worktree using command substitution
283295
cd "$(wtp cd feature/auth)"
@@ -288,8 +300,10 @@ cd "$(wtp cd @)"
288300

289301
#### With Shell Hook (Recommended)
290302

291-
For a more seamless experience, enable the shell hook. `wtp shell-init <shell>` already bundles it, so Homebrew users get the hook automatically and go install users get it from the one-liner above. If you only want the hook without completions, you can still run `wtp hook <shell>` manually.
292-
303+
For a more seamless experience, enable the shell hook. `wtp shell-init <shell>`
304+
already bundles it, so Homebrew users get the hook automatically and go install
305+
users get it from the one-liner above. If you only want the hook without
306+
completions, you can still run `wtp hook <shell>` manually.
293307

294308
Then use the simplified syntax:
295309

@@ -306,7 +320,9 @@ wtp cd <TAB>
306320

307321
#### Complete Setup (Lazy Loading for Homebrew Users)
308322

309-
Homebrew ships a lightweight bootstrapper. Press `TAB` after typing `wtp` and it evaluates `wtp shell-init <shell>` once for your session—tab completion and `wtp cd` just work.
323+
Homebrew ships a lightweight bootstrapper. Press `TAB` after typing `wtp` and it
324+
evaluates `wtp shell-init <shell>` once for your session—tab completion and
325+
`wtp cd` just work.
310326

311327
## Worktree Structure
312328

@@ -372,6 +388,7 @@ go tool task build
372388
# Run locally
373389
./wtp --help
374390
```
391+
375392
## License
376393

377394
MIT License - see [LICENSE](LICENSE) file for details.

cmd/wtp/add.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ func analyzeGitWorktreeError(workTreePath, branchName string, gitError error, gi
171171
}
172172
}
173173

174+
if isBranchAlreadyExistsError(errorOutput) {
175+
return &BranchAlreadyExistsError{
176+
BranchName: branchName,
177+
GitError: gitError,
178+
}
179+
}
180+
174181
if isPathAlreadyExistsError(errorOutput) {
175182
return &PathAlreadyExistsError{
176183
Path: workTreePath,
@@ -227,6 +234,11 @@ func isWorktreeAlreadyExistsError(errorOutput string) bool {
227234
strings.Contains(errorOutput, "already used by worktree")
228235
}
229236

237+
func isBranchAlreadyExistsError(errorOutput string) bool {
238+
return strings.Contains(errorOutput, "branch") &&
239+
strings.Contains(errorOutput, "already exists")
240+
}
241+
230242
func isPathAlreadyExistsError(errorOutput string) bool {
231243
return strings.Contains(errorOutput, "already exists")
232244
}
@@ -264,6 +276,24 @@ Solutions:
264276
Original error: %v`, e.BranchName, e.BranchName, e.GitError)
265277
}
266278

279+
type BranchAlreadyExistsError struct {
280+
BranchName string
281+
GitError error
282+
}
283+
284+
func (e *BranchAlreadyExistsError) Error() string {
285+
return fmt.Sprintf(`branch '%s' already exists
286+
287+
The branch '%s' already exists in this repository.
288+
289+
Solutions:
290+
• Run 'wtp add %s' to create a worktree for the existing branch
291+
• Choose a different branch name with '--branch'
292+
• Delete the existing branch if it's no longer needed
293+
294+
Original error: %v`, e.BranchName, e.BranchName, e.BranchName, e.GitError)
295+
}
296+
267297
type PathAlreadyExistsError struct {
268298
Path string
269299
GitError error
@@ -395,7 +425,12 @@ func getBranches(w io.Writer) error {
395425
}
396426

397427
// completeBranches provides branch name completion for urfave/cli (wrapper for getBranches)
398-
func completeBranches(_ context.Context, _ *cli.Command) {
428+
func completeBranches(_ context.Context, cmd *cli.Command) {
429+
current, previous := completionArgsFromCommand(cmd)
430+
if maybeCompleteFlagSuggestions(cmd, current, previous) {
431+
return
432+
}
433+
399434
var buf bytes.Buffer
400435
if err := getBranches(&buf); err != nil {
401436
return

cmd/wtp/add_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,42 @@ func TestWorkTreeAlreadyExistsError(t *testing.T) {
7979
})
8080
}
8181

82+
func TestBranchAlreadyExistsError(t *testing.T) {
83+
t.Run("should format error message with branch name and guidance", func(t *testing.T) {
84+
// Given: a BranchAlreadyExistsError with branch name and git error
85+
originalErr := &MockGitError{msg: "A branch named 'feature/auth' already exists."}
86+
err := &BranchAlreadyExistsError{
87+
BranchName: "feature/auth",
88+
GitError: originalErr,
89+
}
90+
91+
// When: getting error message
92+
message := err.Error()
93+
94+
// Then: should contain branch name, guidance, and original error
95+
assert.Contains(t, message, "branch 'feature/auth' already exists")
96+
assert.Contains(t, message, "wtp add feature/auth")
97+
assert.Contains(t, message, "Choose a different branch name")
98+
assert.Contains(t, message, "Delete the existing branch")
99+
assert.Contains(t, message, "A branch named 'feature/auth' already exists.")
100+
})
101+
102+
t.Run("should handle empty branch name", func(t *testing.T) {
103+
// Given: error with empty branch name
104+
err := &BranchAlreadyExistsError{
105+
BranchName: "",
106+
GitError: &MockGitError{msg: "test error"},
107+
}
108+
109+
// When: getting error message
110+
message := err.Error()
111+
112+
// Then: should still provide valid message
113+
assert.Contains(t, message, "branch '' already exists")
114+
assert.Contains(t, message, "test error")
115+
})
116+
}
117+
82118
func TestPathAlreadyExistsError(t *testing.T) {
83119
t.Run("should format error message with path and solutions", func(t *testing.T) {
84120
// Given: a PathAlreadyExistsError with path and git error
@@ -875,6 +911,14 @@ func TestAnalyzeGitWorktreeError(t *testing.T) {
875911
expectedError: "",
876912
expectedType: &PathAlreadyExistsError{},
877913
},
914+
{
915+
name: "branch already exists error",
916+
workTreePath: "/path/to/worktree",
917+
branchName: "existing-branch",
918+
gitOutput: "fatal: A branch named 'existing-branch' already exists.",
919+
expectedError: "",
920+
expectedType: &BranchAlreadyExistsError{},
921+
},
878922
{
879923
name: "multiple branches error",
880924
workTreePath: "/path/to/worktree",

cmd/wtp/app.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package main
2+
3+
import "github.com/urfave/cli/v3"
4+
5+
func newApp() *cli.Command {
6+
return &cli.Command{
7+
Name: "wtp",
8+
Usage: "Enhanced Git worktree management",
9+
Description: "wtp (Worktree Plus) simplifies Git worktree creation with automatic branch tracking, " +
10+
"project-specific setup hooks, and convenient defaults.",
11+
Version: version,
12+
EnableShellCompletion: true,
13+
ConfigureShellCompletionCommand: configureCompletionCommand,
14+
Flags: []cli.Flag{
15+
&cli.BoolFlag{
16+
Name: "version",
17+
Usage: "Show version information",
18+
},
19+
},
20+
Commands: []*cli.Command{
21+
NewAddCommand(),
22+
NewListCommand(),
23+
NewRemoveCommand(),
24+
NewInitCommand(),
25+
NewCdCommand(),
26+
// Built-in completion is automatically provided by urfave/cli
27+
NewHookCommand(),
28+
NewShellInitCommand(),
29+
},
30+
}
31+
}

cmd/wtp/cd.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,15 +355,51 @@ func getWorktreesForCd(w io.Writer) error {
355355
}
356356

357357
// completeWorktreesForCd provides worktree name completion for cd command (wrapper for getWorktreesForCd)
358-
func completeWorktreesForCd(_ context.Context, _ *cli.Command) {
358+
func completeWorktreesForCd(_ context.Context, cmd *cli.Command) {
359+
current, previous := completionArgsFromCommand(cmd)
360+
361+
if maybeCompleteFlagSuggestions(cmd, current, previous) {
362+
return
363+
}
364+
365+
currentNormalized := strings.TrimSuffix(current, "*")
366+
367+
if currentNormalized == "" && len(previous) > 0 {
368+
return
369+
}
370+
359371
var buf bytes.Buffer
360372
if err := getWorktreesForCd(&buf); err != nil {
361373
return
362374
}
363375

376+
used := make(map[string]struct{}, len(previous))
377+
for _, arg := range previous {
378+
if arg == "" || strings.HasPrefix(arg, "-") {
379+
continue
380+
}
381+
key := strings.TrimSuffix(arg, "*")
382+
used[key] = struct{}{}
383+
}
384+
364385
// Output each line using fmt.Println for urfave/cli compatibility
365386
scanner := bufio.NewScanner(&buf)
366387
for scanner.Scan() {
367-
fmt.Println(scanner.Text())
388+
raw := scanner.Text()
389+
candidate := strings.TrimSuffix(raw, "*")
390+
391+
if candidate == "" {
392+
continue
393+
}
394+
395+
if _, exists := used[candidate]; exists {
396+
continue
397+
}
398+
399+
if currentNormalized != "" && candidate == currentNormalized {
400+
continue
401+
}
402+
403+
fmt.Println(candidate)
368404
}
369405
}

0 commit comments

Comments
 (0)