Skip to content

Commit 39de653

Browse files
committed
refactor(worktree): use flat .WT. sibling pattern for worktree path generation
WHY: The previous nested pattern `../{repo}-worktrees/{branch}` created an intermediate container directory between the parent and each worktree. This breaks when the repository is a git submodule, because the extra nesting level changes relative paths that git uses internally to link worktrees back to their parent repository. WHAT: Replaced the two-level worktree path pattern with a flat sibling convention using `.WT.` as an infix separator: - Before: `../hug-scm-worktrees/feature-auth/` (nested in container) - After: `../hug-scm.WT.feature-auth/` (flat sibling) Key design decision: The `.WT.` uppercase-dot-delimited infix was chosen over `-wt-` because it provides stronger visual identification when scanning directory listings. The dots act as a clear structural delimiter between the repo name and the branch name, while the uppercase stands out against the lowercase hyphen-cased names on both sides. Dots are safe here because the branch sanitization (which converts dots to hyphens) applies only to the branch portion, not the structural separator. HOW: Single behavioral change in `generate_worktree_path()` in hug-git-worktree: - Path pattern: `../{repo}.WT.{branch}` (flat, no container directory) - Removed `mkdir -p` for intermediate directory (parent `..` always exists) - Added writability check on parent with `/tmp` fallback - `generate_unique_worktree_path()` inherits the new pattern automatically IMPACT: Worktree creation now works correctly inside git submodules without path resolution issues. No migration needed — git tracks worktrees by absolute path internally, so existing worktrees created with the old pattern continue working. The test helper `create_test_worktree()` was also aligned to use the same `.WT.` pattern, ensuring production and test code share the same path convention. All 1572 BATS + 439 Python tests pass. 🤖 Generated with [Qoder][https://qoder.com]
1 parent 8ad4b88 commit 39de653

9 files changed

Lines changed: 33 additions & 32 deletions

File tree

docs/commands/worktree.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ hug wtl --json # Output in JSON format
8080
```
8181
Worktrees:
8282
* [DIRTY] main (1b87e92) ~/IdeaProjects/hug-scm
83-
feature-auth (a3f2b1c) ~/IdeaProjects/worktrees-hug-scm/feature-auth
84-
hotfix-patch (d8e9f0a) ~/IdeaProjects/worktrees-hug-scm/hotfix-patch
83+
feature-auth (a3f2b1c) ~/IdeaProjects/hug-scm.WT.feature-auth
84+
hotfix-patch (d8e9f0a) ~/IdeaProjects/hug-scm.WT.hotfix-patch
8585
```
8686

8787
### `hug wtll` - List Worktrees (Long Format)
@@ -112,7 +112,7 @@ Worktrees (long format):
112112
docs: update branching command documentation for search filtering feature
113113
Status: Modified ! (1 staged, 2 unstaged) ! | Locked: No
114114
115-
feature-auth a3f2b1c (~/IdeaProjects/worktrees-hug-scm/feature-auth)
115+
feature-auth a3f2b1c (~/IdeaProjects/hug-scm.WT.feature-auth)
116116
feat: implement OAuth authentication flow
117117
Status: Clean ✓ | Locked: No
118118
```
@@ -243,7 +243,7 @@ hug wtc feature-auth -f # Create without confirmation
243243

244244
**Auto-generated Path Pattern:**
245245
```
246-
../worktrees-<repo-name>/<branch-name>
246+
../<repo-name>.WT.<branch-name>
247247
```
248248

249249
### `hug wtdel` - Remove Worktree
@@ -326,7 +326,7 @@ Both `hug wtl` and `hug wtll` support `--json` output for programmatic use:
326326
"current": true
327327
},
328328
{
329-
"path": "/home/user/worktrees-project/feature-auth",
329+
"path": "/home/user/project.WT.feature-auth",
330330
"branch": "feature-auth",
331331
"commit": "a3f2b1c",
332332
"dirty": false,
@@ -415,7 +415,7 @@ hug rb experiment-refactor main
415415
- Consider using prefixes: `feature/`, `bugfix/`, `hotfix/`, `experiment/`
416416

417417
**Path Management:**
418-
- Let Hug auto-generate paths when possible (`../worktrees-<repo>/`)
418+
- Let Hug auto-generate paths when possible (`../<repo>.WT.<branch>`)
419419
- Keep worktree paths short and meaningful
420420
- Use consistent base directory for all worktrees
421421

git-config/bin/git-sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ if [[ "$show_llm" == true ]]; then
9393
elif [[ "$show_stats_explicit" == true ]]; then
9494
show_stats=true
9595
else
96-
show_stats=true # Default for LLM format
96+
show_stats=true # Default for LLM format
9797
fi
9898
else
9999
output_format="standard"
@@ -103,7 +103,7 @@ else
103103
elif [[ "$show_stats_explicit" == true ]]; then
104104
show_stats=true
105105
else
106-
show_stats=true # Default for standard format
106+
show_stats=true # Default for standard format
107107
fi
108108
fi
109109

git-config/bin/git-wtc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ DESCRIPTION:
3333
work on multiple branches simultaneously without context switching overhead.
3434
3535
If no path is provided, generates a smart default path outside the main
36-
repository: ../<repo>-worktrees/<branch>
36+
repository: ../<repo>.WT.<branch>
3737
3838
By default, the branch must exist locally. With --new flag or with --force,
3939
non-existent branches will be created automatically from the current HEAD.

git-config/lib/hug-confirm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ prompt_confirm_danger() {
130130
# ✗ Overwriting or modifying existing data (use prompt_confirm_warn)
131131
# ✗ Irreversible or high-impact operations (use prompt_confirm_danger)
132132
#
133-
# Usage: prompt_confirm_safe "Create worktree for branch 'feature' at '../repo-worktrees/feature'?"
133+
# Usage: prompt_confirm_safe "Create worktree for branch 'feature' at '../repo.WT.feature'?"
134134
#
135135
# Returns: 0 if user confirms, 1 if cancelled (exits by default in non-interactive mode)
136136
#

git-config/lib/hug-git-worktree

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,10 @@ validate_worktree_creation_path() {
351351
# Generate smart default worktree path for a branch
352352
# Usage: path=$(generate_worktree_path "feature-auth")
353353
# Returns: Default worktree path outside main repository
354+
# Note: Uses flat sibling pattern ../{repo}.WT.{branch} instead of nested
355+
# ../{repo}-worktrees/{branch} to avoid breaking relative paths in submodules.
356+
# The .WT. infix provides strong visual identification when scanning directory
357+
# listings, clearly separating repo name from branch name.
354358
generate_worktree_path() {
355359
local branch="$1"
356360
local repo_name
@@ -366,17 +370,14 @@ generate_worktree_path() {
366370
# Make branch name safe for filesystem
367371
safe_branch=$(printf '%s' "$branch" | sed 's|/|-|g' | sed 's|\.|-|g' | tr '[:upper:]' '[:lower:]')
368372

369-
# Generate preferred path with auto-creation of parent directories
370-
local preferred_path="../${repo_name}-worktrees/${safe_branch}"
371-
local parent_dir=$(dirname "$preferred_path")
373+
# Flat sibling path — no intermediate container directory needed.
374+
# Parent (..) always exists since the repo itself lives there.
375+
local preferred_path="../${repo_name}.WT.${safe_branch}"
372376

373-
# Auto-create parent directory if it doesn't exist
374-
if [[ ! -d "$parent_dir" ]]; then
375-
mkdir -p "$parent_dir" 2>/dev/null || {
376-
# Fallback to absolute path in temp directory if auto-creation fails
377-
printf '/tmp/hug-wt-%s-%s/%s' "$repo_name" "$$" "$safe_branch"
378-
return 0
379-
}
377+
# Verify parent is writable; fallback to /tmp if not
378+
if [[ ! -w "$(dirname "$preferred_path")" ]]; then
379+
printf '/tmp/hug-wt-%s-%s-%s' "$repo_name" "$$" "$safe_branch"
380+
return 0
380381
fi
381382

382383
printf '%s' "$preferred_path"

git-config/lib/python/tests/test_worktree.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ def sample_porcelain_with_worktrees():
3939
branch refs/heads/main
4040
commit abc1234def5678
4141
42-
worktree /home/user/repo-worktrees/feature-1
42+
worktree /home/user/repo.WT.feature-1
4343
branch refs/heads/feature-1
4444
commit def5678901234
4545
locked
4646
47-
worktree /home/user/repo-worktrees/bugfix-2
47+
worktree /home/user/repo.WT.bugfix-2
4848
branch refs/heads/bugfix-2
4949
commit 123456789abcd"""
5050

@@ -55,7 +55,7 @@ def sample_porcelain_detached_head():
5555
return """worktree /home/user/repo
5656
HEAD abc1234def5678
5757
58-
worktree /home/user/repo-worktrees/feature-1
58+
worktree /home/user/repo.WT.feature-1
5959
branch refs/heads/feature-1
6060
commit def5678901234"""
6161

@@ -103,7 +103,7 @@ def worktree_info_main():
103103
def worktree_info_feature():
104104
"""Sample WorktreeInfo for feature branch."""
105105
return WorktreeInfo(
106-
path="/home/user/repo-worktrees/feature-1",
106+
path="/home/user/repo.WT.feature-1",
107107
branch="feature-1",
108108
commit="def5678",
109109
is_dirty=False,
@@ -291,11 +291,11 @@ def test_multiple_worktrees_excludes_main(self, mock_check_dirty):
291291
branch refs/heads/main
292292
commit abc1234def5678
293293
294-
worktree /home/user/repo-worktrees/feature-1
294+
worktree /home/user/repo.WT.feature-1
295295
branch refs/heads/feature-1
296296
commit def5678901234
297297
298-
worktree /home/user/repo-worktrees/bugfix-2
298+
worktree /home/user/repo.WT.bugfix-2
299299
branch refs/heads/bugfix-2
300300
commit 123456789abcd"""
301301
result = parse_worktree_list(porcelain, "/home/user/repo", include_main=False)
@@ -311,7 +311,7 @@ def test_multiple_worktrees_includes_main(self, mock_check_dirty):
311311
branch refs/heads/main
312312
commit abc1234def5678
313313
314-
worktree /home/user/repo-worktrees/feature-1
314+
worktree /home/user/repo.WT.feature-1
315315
branch refs/heads/feature-1
316316
commit def5678901234"""
317317
result = parse_worktree_list(porcelain, "/home/user/repo", include_main=True)
@@ -333,7 +333,7 @@ def test_detached_head_no_branch_line(self, mock_check_dirty):
333333
def test_locked_worktree_detected(self, mock_check_dirty):
334334
"""Should detect locked worktrees."""
335335
mock_check_dirty.return_value = False
336-
porcelain = """worktree /home/user/repo-worktrees/feature-1
336+
porcelain = """worktree /home/user/repo.WT.feature-1
337337
branch refs/heads/feature-1
338338
commit def5678901234
339339
locked"""

tests/lib/test_hug-git-worktree.bats

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,14 +214,14 @@ teardown() {
214214
local generated_path
215215
generated_path=$(generate_worktree_path "feature-1")
216216

217-
assert_equal "$generated_path" "../$(basename "$TEST_REPO")-worktrees/feature-1"
217+
assert_equal "$generated_path" "../$(basename "$TEST_REPO").WT.feature-1"
218218
}
219219

220220
@test "hug-git-worktree: generate_worktree_path sanitizes branch name" {
221221
local generated_path
222222
generated_path=$(generate_worktree_path "feature/auth.v2")
223223

224-
assert_equal "$generated_path" "../$(basename "$TEST_REPO")-worktrees/feature-auth-v2"
224+
assert_equal "$generated_path" "../$(basename "$TEST_REPO").WT.feature-auth-v2"
225225
}
226226

227227
@test "hug-git-worktree: generate_unique_worktree_path returns unique path" {

tests/test_helper.bash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1041,7 +1041,7 @@ create_test_worktree() {
10411041
)
10421042

10431043
# Create worktree path outside the repository
1044-
local worktree_path="${test_repo_path}-wt-${branch}"
1044+
local worktree_path="${test_repo_path}.WT.${branch}"
10451045

10461046
# Create the worktree
10471047
(

tests/unit/test_worktree_create.bats

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ teardown() {
156156

157157
@test "hug wtc: resolves path conflicts automatically" {
158158
# Create a directory at the default path location to force a conflict
159-
local default_path="${TEST_REPO}-worktrees/feature-2"
159+
local default_path="${TEST_REPO}.WT.feature-2"
160160
mkdir -p "$default_path"
161161
echo "existing file" > "$default_path/existing.txt"
162162

0 commit comments

Comments
 (0)