Skip to content

Commit 53d6d7f

Browse files
authored
Merge branch 'main' into win-support
2 parents b94e3dd + 3afdc10 commit 53d6d7f

10 files changed

Lines changed: 114 additions & 104 deletions

File tree

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,10 @@ Thumbs.db
3636
# Temporary files
3737
*.tmp
3838
*.temp
39+
40+
# Local caches (Codex sandbox / local runs)
41+
.gocache/
42+
.gomodcache/
43+
.gopath/
44+
.golangci-cache/
45+
.tmp_repro/

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ requirements.
5858
path) **wtp solution:** `wtp cd feature/auth` with tab completion
5959

6060
Jump between worktrees instantly. Use `wtp cd @` to return to your main
61-
worktree. No more terminal tab confusion.
61+
worktree (or just `wtp cd`). No more terminal tab confusion.
6262

6363
## Requirements
6464

@@ -349,6 +349,9 @@ two ways:
349349
cd "$(wtp cd feature/auth)"
350350
351351
# Change to the main worktree
352+
cd "$(wtp cd)"
353+
354+
# Or explicitly:
352355
cd "$(wtp cd @)"
353356
```
354357

@@ -365,6 +368,9 @@ Then use the simplified syntax:
365368
# Change to a worktree by its name
366369
wtp cd feature/auth
367370
371+
# Go to the main worktree (same as @)
372+
wtp cd
373+
368374
# Change to the root worktree using the '@' shorthand
369375
wtp cd @
370376

cmd/wtp/cd.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,30 @@ func NewCdCommand() *cli.Command {
2828
return &cli.Command{
2929
Name: "cd",
3030
Usage: "Output absolute path to worktree",
31-
Description: "Output the absolute path to the specified worktree.\n\n" +
31+
Description: "Output the absolute path to the specified worktree.\n" +
32+
"If no worktree is specified, outputs the main worktree path (like cd goes to $HOME).\n\n" +
3233
"Usage:\n" +
3334
" Direct: cd \"$(wtp cd feature)\"\n" +
34-
" With hook: wtp cd feature\n\n" +
35+
" With hook: wtp cd feature\n" +
36+
" Go home: wtp cd\n\n" +
3537
"To enable the hook for easier navigation:\n" +
3638
" Bash: eval \"$(wtp hook bash)\"\n" +
3739
" Zsh: eval \"$(wtp hook zsh)\"\n" +
3840
" Fish: wtp hook fish | source",
39-
ArgsUsage: "<worktree-name>",
41+
ArgsUsage: "[worktree-name]",
4042
Action: cdToWorktree,
4143
ShellComplete: completeWorktreesForCd,
4244
}
4345
}
4446

4547
func cdToWorktree(_ context.Context, cmd *cli.Command) error {
4648
args := cmd.Args()
47-
if args.Len() == 0 {
48-
return errors.WorktreeNameRequired()
49-
}
5049

51-
worktreeName := args.Get(0)
50+
// Default to main worktree (@) when no argument provided, like cd goes to $HOME
51+
worktreeName := "@"
52+
if args.Len() > 0 {
53+
worktreeName = args.Get(0)
54+
}
5255

5356
// Get current directory
5457
cwd, err := os.Getwd()

cmd/wtp/cd_test.go

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package main
22

33
import (
4-
"bytes"
54
"context"
65
"os"
76
"path/filepath"
@@ -128,42 +127,6 @@ func TestCdCommand_NoEnvironmentVariableDependency(t *testing.T) {
128127
}
129128
}
130129

131-
// Test critical error scenarios that users will encounter
132-
func TestCdCommand_UserFacingErrors(t *testing.T) {
133-
tests := []struct {
134-
name string
135-
args []string
136-
expectedError string
137-
}{
138-
{
139-
name: "no arguments",
140-
args: []string{},
141-
expectedError: "worktree name is required",
142-
},
143-
}
144-
145-
for _, tt := range tests {
146-
t.Run(tt.name, func(t *testing.T) {
147-
app := &cli.Command{
148-
Commands: []*cli.Command{
149-
NewCdCommand(),
150-
},
151-
}
152-
153-
var buf bytes.Buffer
154-
app.Writer = &buf
155-
156-
ctx := context.Background()
157-
cmdArgs := []string{"wtp", "cd"}
158-
cmdArgs = append(cmdArgs, tt.args...)
159-
160-
err := app.Run(ctx, cmdArgs)
161-
assert.Error(t, err)
162-
assert.Contains(t, err.Error(), tt.expectedError)
163-
})
164-
}
165-
}
166-
167130
// Test edge cases that could break in production
168131
func TestCdCommand_EdgeCases(t *testing.T) {
169132
tests := []struct {

cmd/wtp/hook.go

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,20 @@ wtp() {
9292
fi
9393
done
9494
if [[ "$1" == "cd" ]]; then
95+
local target_dir
9596
if [[ -z "$2" ]]; then
96-
echo "Usage: wtp cd <worktree>" >&2
97-
return 1
97+
target_dir=$(command wtp cd 2>/dev/null)
98+
else
99+
target_dir=$(command wtp cd "$2" 2>/dev/null)
98100
fi
99-
local target_dir
100-
target_dir=$(command wtp cd "$2" 2>/dev/null)
101101
if [[ $? -eq 0 && -n "$target_dir" ]]; then
102102
cd "$target_dir"
103103
else
104-
command wtp cd "$2"
104+
if [[ -z "$2" ]]; then
105+
command wtp cd
106+
else
107+
command wtp cd "$2"
108+
fi
105109
fi
106110
else
107111
command wtp "$@"
@@ -121,16 +125,20 @@ wtp() {
121125
fi
122126
done
123127
if [[ "$1" == "cd" ]]; then
128+
local target_dir
124129
if [[ -z "$2" ]]; then
125-
echo "Usage: wtp cd <worktree>" >&2
126-
return 1
130+
target_dir=$(command wtp cd 2>/dev/null)
131+
else
132+
target_dir=$(command wtp cd "$2" 2>/dev/null)
127133
fi
128-
local target_dir
129-
target_dir=$(command wtp cd "$2" 2>/dev/null)
130134
if [[ $? -eq 0 && -n "$target_dir" ]]; then
131135
cd "$target_dir"
132136
else
133-
command wtp cd "$2"
137+
if [[ -z "$2" ]]; then
138+
command wtp cd
139+
else
140+
command wtp cd "$2"
141+
fi
134142
fi
135143
else
136144
command wtp "$@"
@@ -151,14 +159,18 @@ function wtp
151159
end
152160
if test "$argv[1]" = "cd"
153161
if test -z "$argv[2]"
154-
echo "Usage: wtp cd <worktree>" >&2
155-
return 1
162+
set -l target_dir (command wtp cd 2>/dev/null)
163+
else
164+
set -l target_dir (command wtp cd $argv[2] 2>/dev/null)
156165
end
157-
set -l target_dir (command wtp cd $argv[2] 2>/dev/null)
158166
if test $status -eq 0 -a -n "$target_dir"
159-
cd $target_dir
167+
cd "$target_dir"
160168
else
161-
command wtp cd $argv[2]
169+
if test -z "$argv[2]"
170+
command wtp cd
171+
else
172+
command wtp cd $argv[2]
173+
end
162174
end
163175
else
164176
command wtp $argv

cmd/wtp/hook_test.go

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func TestHookCommand_GeneratesValidShellScripts(t *testing.T) {
6666
"function wtp",
6767
"if test \"$argv[1]\" = \"cd\"",
6868
"command wtp cd",
69-
"cd $target_dir",
69+
"cd \"$target_dir\"",
7070
},
7171
},
7272
}
@@ -106,23 +106,46 @@ func TestHookScripts_HandleEdgeCases(t *testing.T) {
106106
name string
107107
shell string
108108
requiredLogic []string
109+
notContains []string
109110
}{
110111
{
111-
name: "bash hook handles empty argument",
112+
name: "bash hook supports no-arg cd",
112113
shell: "bash",
113114
requiredLogic: []string{
114-
"if [[ -z \"$2\" ]]", // Empty argument check
115-
"echo \"Usage:", // Error message
116-
"return 1", // Error exit
115+
"if [[ -z \"$2\" ]]", // No-arg branch
116+
"target_dir=$(command wtp cd", // Uses `wtp cd` default behavior
117+
"target_dir=$(command wtp cd \"$2", // Uses explicit worktree name when present
118+
},
119+
notContains: []string{
120+
"Usage: wtp cd <worktree>",
121+
"echo \"Usage:",
122+
},
123+
},
124+
{
125+
name: "zsh hook supports no-arg cd",
126+
shell: "zsh",
127+
requiredLogic: []string{
128+
"if [[ -z \"$2\" ]]", // No-arg branch
129+
"target_dir=$(command wtp cd", // Uses `wtp cd` default behavior
130+
"target_dir=$(command wtp cd \"$2", // Uses explicit worktree name when present
131+
},
132+
notContains: []string{
133+
"Usage: wtp cd <worktree>",
134+
"echo \"Usage:",
117135
},
118136
},
119137
{
120-
name: "fish hook handles empty argument",
138+
name: "fish hook supports no-arg cd",
121139
shell: "fish",
122140
requiredLogic: []string{
123-
"if test -z \"$argv[2]\"", // Empty argument check
124-
"echo \"Usage:", // Error message
125-
"return 1", // Error exit
141+
"if test -z \"$argv[2]\"", // No-arg branch
142+
"set -l target_dir (command wtp cd", // Uses `wtp cd` default behavior
143+
"command wtp cd $argv[2]", // Uses explicit worktree name when present
144+
"cd \"$target_dir\"", // Handles spaces safely
145+
},
146+
notContains: []string{
147+
"Usage: wtp cd <worktree>",
148+
"echo \"Usage:",
126149
},
127150
},
128151
}
@@ -134,6 +157,8 @@ func TestHookScripts_HandleEdgeCases(t *testing.T) {
134157
switch tt.shell {
135158
case "bash":
136159
require.NoError(t, printBashHook(&buf))
160+
case "zsh":
161+
require.NoError(t, printZshHook(&buf))
137162
case "fish":
138163
require.NoError(t, printFishHook(&buf))
139164
}
@@ -142,6 +167,9 @@ func TestHookScripts_HandleEdgeCases(t *testing.T) {
142167
for _, logic := range tt.requiredLogic {
143168
assert.Contains(t, output, logic, "Hook must handle edge cases properly")
144169
}
170+
for _, unexpected := range tt.notContains {
171+
assert.NotContains(t, output, unexpected)
172+
}
145173
})
146174
}
147175
}

docs/architecture.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ Following Git's own behavior:
155155

156156
The `wtp cd` command uses a two-part architecture:
157157

158-
1. **Go Command**: `wtp cd <worktree>` finds the worktree path and outputs it
158+
1. **Go Command**: `wtp cd [worktree]` finds the worktree path and outputs it (defaults to the main worktree when omitted)
159159
2. **Shell Function**: Wraps the Go command and performs the actual `cd`
160160

161161
### Shell Integration Flow
@@ -165,7 +165,7 @@ The `wtp cd` command uses a two-part architecture:
165165
wtp cd feature/auth
166166

167167
# Shell function intercepts, runs:
168-
WTP_SHELL_INTEGRATION=1 wtp cd feature/auth
168+
command wtp cd feature/auth
169169

170170
# Go command returns path:
171171
/path/to/worktrees/feature/auth
@@ -176,8 +176,8 @@ cd /path/to/worktrees/feature/auth
176176

177177
### Key Design Decisions
178178

179-
- **Environment Variable Check**: `WTP_SHELL_INTEGRATION=1` prevents accidental direct usage
180-
- **Shell Function Wrapper**: Required because child processes can't change parent's directory
179+
- **Pure Path Output**: `wtp cd` only prints a path (no side effects), so hooks can safely consume it
180+
- **Shell Function Wrapper**: Required because child processes can't change the parent shell's directory
181181
- **Unified Setup Command**: `wtp shell-init <shell>` generates both completion and cd functionality
182182
- **Cross-Shell Support**: Bash, Zsh, and Fish implementations
183183

internal/errors/errors.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,6 @@ Examples:
4747
return errors.New(msg)
4848
}
4949

50-
// WorktreeNameRequired reports that a worktree name argument is missing.
51-
func WorktreeNameRequired() error {
52-
msg := `worktree name is required
53-
54-
Usage: wtp cd <worktree-name>
55-
56-
Tip: Run 'wtp list' to see available worktrees`
57-
return errors.New(msg)
58-
}
59-
6050
// WorktreeNameRequiredForRemove reports that a worktree name is required for removal.
6151
func WorktreeNameRequiredForRemove() error {
6252
msg := `worktree name is required

internal/errors/errors_test.go

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,6 @@ func TestBranchNameRequired(t *testing.T) {
7979
assert.Contains(t, err.Error(), "Examples:")
8080
}
8181

82-
func TestWorktreeNameRequired(t *testing.T) {
83-
err := WorktreeNameRequired()
84-
85-
assert.Error(t, err)
86-
assert.Contains(t, err.Error(), "worktree name is required")
87-
assert.Contains(t, err.Error(), "wtp cd")
88-
assert.Contains(t, err.Error(), "wtp list")
89-
}
90-
9182
func TestWorktreeNameRequiredForRemove(t *testing.T) {
9283
err := WorktreeNameRequiredForRemove()
9384

@@ -434,9 +425,9 @@ func TestErrorMessages_HelpfulContent(t *testing.T) {
434425
keywords: []string{"Solutions:", "git init", "Navigate"},
435426
},
436427
{
437-
name: "WorktreeNameRequired contains examples",
438-
errorFn: WorktreeNameRequired,
439-
keywords: []string{"wtp cd", "wtp list"},
428+
name: "WorktreeNameRequiredForRemove contains examples",
429+
errorFn: WorktreeNameRequiredForRemove,
430+
keywords: []string{"wtp remove", "wtp list"},
440431
},
441432
{
442433
name: "ShellIntegrationRequired contains setup",
@@ -466,7 +457,7 @@ func TestErrorMessages_Format(t *testing.T) {
466457
tests := []func() error{
467458
NotInGitRepository,
468459
func() error { return BranchNameRequired("example") },
469-
WorktreeNameRequired,
460+
WorktreeNameRequiredForRemove,
470461
ShellIntegrationRequired,
471462
}
472463

0 commit comments

Comments
 (0)