Skip to content

Commit 7bbcc13

Browse files
authored
feat: add configurable branch separator for branchSafe variable (#48)
* feat: replace {.branchSafe} with configurable separator for template values Remove the {.branchSafe} template variable and introduce a unified 'separator' config option that controls how '/' and '\' in value variables ({.branch}, {.repo.Owner}, {.env.*}) are replaced. Path variables ({.repo.Main}, {.worktreeRoot}) are never transformed. Default separator is '/' (backward compatible - slashes create subdirs). Set to '-' or '_' for flat paths (e.g. feat/foo -> feat-foo). Configuration: TOML key 'separator', env var WORKTREE_SEPARATOR. Uses os.LookupEnv so empty string is a valid value. * fix: skip empty separator E2E test on Windows PowerShell treats empty environment variables as unset, so WORKTREE_SEPARATOR="" is not observable via os.LookupEnv on Windows.
1 parent 132190a commit 7bbcc13

7 files changed

Lines changed: 321 additions & 76 deletions

File tree

README.md

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ root = "~/projects/worktrees"
271271
# Worktree placement strategy
272272
strategy = "sibling-repo"
273273

274+
# Separator replaces "/" and "\" in value variables ({.branch}, {.repo.Owner}, {.env.*})
275+
# Default "/" preserves slashes (nested dirs). Set to "-" or "_" for flat paths.
276+
# separator = "/"
277+
274278
# Custom pattern (used when strategy = "custom", or to override any strategy's default)
275279
# pattern = "{.worktreeRoot}/{.repo.Name}/{.branch}"
276280
```
@@ -280,7 +284,7 @@ strategy = "sibling-repo"
280284
Configuration values are resolved in this order (highest priority first):
281285

282286
1. **CLI flags** (`--config`)
283-
2. **Environment variables** (`WORKTREE_ROOT`, `WORKTREE_STRATEGY`, `WORKTREE_PATTERN`)
287+
2. **Environment variables** (`WORKTREE_ROOT`, `WORKTREE_STRATEGY`, `WORKTREE_PATTERN`, `WORKTREE_SEPARATOR`)
284288
3. **Config file** (`~/.config/wt/config.toml`)
285289
4. **Built-in defaults**
286290

@@ -295,35 +299,56 @@ Configure the location with environment variables or the config file:
295299
- `WORKTREE_ROOT` / `root` (default: `~/dev/worktrees`)
296300
- `WORKTREE_STRATEGY` / `strategy` (`global`, `sibling-repo`, `parent-branches`, `parent-worktrees`, `parent-dotdir`, `inside-dotdir`, `custom`)
297301
- `WORKTREE_PATTERN` / `pattern` (optional; overrides the default structure within the chosen strategy)
302+
- `WORKTREE_SEPARATOR` / `separator` (default: `/`; controls how `/` and `\` in value variables are replaced)
298303

299304
Available pattern variables:
300305

301306
- `{.repo.Name}` repo name
302-
- `{.repo.Main}` main branch worktree path
307+
- `{.repo.Main}` main branch worktree path (path variable, not transformed by separator)
303308
- `{.repo.Owner}` repo owner/group (from origin URL)
304309
- `{.repo.Host}` git host (from origin URL)
305310
- `{.branch}` git branch name
306-
- `{.branchSafe}` git branch name (sanitized for filesystem paths)
307-
- `{.worktreeRoot}` value of `WORKTREE_ROOT`
311+
- `{.worktreeRoot}` value of `WORKTREE_ROOT` (path variable, not transformed by separator)
308312
- `{.env.VARNAME}` value of environment variable `VARNAME` (e.g. `{.env.USER}`, `{.env.HOME}`)
309313

310314
Default patterns per strategy:
311315

312316
| Strategy | Description | Default pattern |
313317
| --- | --- | --- |
314318
| `global` | worktrees under a global directory | `{.worktreeRoot}/{.repo.Name}/{.branch}` |
315-
| `sibling-repo` | worktrees next to the main repo directory | `{.repo.Main}/../{.repo.Name}-{.branchSafe}` |
319+
| `sibling-repo` | worktrees next to the main repo directory | `{.repo.Main}/../{.repo.Name}-{.branch}` |
316320
| `parent-branches` | branches as siblings of main | `{.repo.Main}/../{.branch}` |
317321
| `parent-worktrees` | branches under `<repo>.worktrees/` | `{.repo.Main}/../{.repo.Name}.worktrees/{.branch}` |
318322
| `parent-dotdir` | branches under `.worktrees/` next to main | `{.repo.Main}/../.worktrees/{.branch}` |
319323
| `inside-dotdir` | branches under `.worktrees/` inside main | `{.repo.Main}/.worktrees/{.branch}` |
320324
| `custom` | user-defined pattern | `WORKTREE_PATTERN` |
321325

326+
### Separator
327+
328+
The `separator` setting controls how `/` and `\` characters in **value variables** (`{.branch}`, `{.repo.Owner}`, `{.env.*}`) are replaced. **Path variables** (`{.repo.Main}`, `{.worktreeRoot}`) are never transformed.
329+
330+
| Separator | Branch `feat/foo` becomes | Use case |
331+
| --- | --- | --- |
332+
| `/` (default) | `feat/foo` (nested dirs) | Standard layout |
333+
| `-` | `feat-foo` (flat) | Sibling-repo, flat directories |
334+
| `_` | `feat_foo` (flat) | Alternative flat layout |
335+
| `""` | `featfoo` | Compact (rarely used) |
336+
337+
```toml
338+
# ~/.config/wt/config.toml
339+
separator = "-" # feat/foo -> feat-foo
340+
```
341+
342+
```bash
343+
export WORKTREE_SEPARATOR="-"
344+
```
345+
322346
Customize the location via environment variables:
323347

324348
```bash
325349
export WORKTREE_ROOT="$HOME/projects/worktrees"
326350
export WORKTREE_STRATEGY="sibling-repo"
351+
export WORKTREE_SEPARATOR="-"
327352
export WORKTREE_PATTERN="{.repo.Main}/../{.repo.Name}/{.branch}"
328353
```
329354

@@ -332,6 +357,7 @@ Or via config file:
332357
```toml
333358
root = "~/projects/worktrees"
334359
strategy = "sibling-repo"
360+
separator = "-"
335361
pattern = "{.repo.Main}/../{.repo.Name}/{.branch}"
336362
```
337363

config.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@ import (
1212

1313
// Config represents the wt configuration file structure.
1414
type Config struct {
15-
Root string `toml:"root"`
16-
Strategy string `toml:"strategy"`
17-
Pattern string `toml:"pattern"`
15+
Root string `toml:"root"`
16+
Strategy string `toml:"strategy"`
17+
Pattern string `toml:"pattern"`
18+
Separator string `toml:"separator"`
1819
}
1920

2021
// configSource tracks where each config value came from.
2122
type configSource struct {
22-
Root string
23-
Strategy string
24-
Pattern string
23+
Root string
24+
Strategy string
25+
Pattern string
26+
Separator string
2527
}
2628

2729
// configFilePath is the resolved path to the config file (set during loading).
@@ -50,10 +52,16 @@ const defaultConfigTemplate = `# wt configuration file
5052
5153
# Custom pattern (used when strategy = "custom", or to override any strategy's default)
5254
# Available variables: {.worktreeRoot}, {.repo.Name}, {.repo.Main},
53-
# {.repo.Owner}, {.repo.Host}, {.branch}, {.branchSafe},
55+
# {.repo.Owner}, {.repo.Host}, {.branch},
5456
# {.env.VARNAME} (access environment variables, e.g. {.env.USER})
5557
# pattern = "{.worktreeRoot}/{.repo.Name}/{.branch}"
5658
59+
# Separator replaces "/" and "\" in template value variables ({.branch}, {.repo.Owner}, {.env.*})
60+
# Default is "/" (no transformation — slashes create subdirectories).
61+
# Set to "-" or "_" for flat paths (e.g. feat/foo -> feat-foo).
62+
# Does NOT affect path variables ({.repo.Main}, {.worktreeRoot}).
63+
# separator = "/"
64+
5765
# Example: group worktrees by a FEATURE environment variable
5866
# strategy = "custom"
5967
# pattern = "{.worktreeRoot}/{.env.FEATURE}/{.repo.Name}"
@@ -95,11 +103,13 @@ func loadWorktreeConfig() {
95103
worktreeRoot = defaultRoot
96104
worktreeStrategy = "global"
97105
worktreePattern = ""
106+
worktreeSeparator = "/"
98107

99108
configSources = configSource{
100-
Root: "default",
101-
Strategy: "default",
102-
Pattern: "default",
109+
Root: "default",
110+
Strategy: "default",
111+
Pattern: "default",
112+
Separator: "default",
103113
}
104114

105115
// 2. Load config file
@@ -122,6 +132,10 @@ func loadWorktreeConfig() {
122132
worktreePattern = strings.TrimSpace(cfg.Pattern)
123133
configSources.Pattern = "config file"
124134
}
135+
if cfg.Separator != "" {
136+
worktreeSeparator = cfg.Separator
137+
configSources.Separator = "config file"
138+
}
125139
}
126140
}
127141

@@ -138,6 +152,10 @@ func loadWorktreeConfig() {
138152
worktreePattern = strings.TrimSpace(v)
139153
configSources.Pattern = "env: WORKTREE_PATTERN"
140154
}
155+
if v, ok := os.LookupEnv("WORKTREE_SEPARATOR"); ok {
156+
worktreeSeparator = v
157+
configSources.Separator = "env: WORKTREE_SEPARATOR"
158+
}
141159
}
142160

143161
// expandHome replaces a leading ~ with the user's home directory.

e2e/scenarios/info.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ scenarios:
1010
- run: $WT_BIN info
1111
expect:
1212
exit_code: 0
13-
output_contains: "Strategy: global"
13+
output_contains: "Strategy: global"
1414
- run: $WT_BIN info
1515
expect:
1616
output_contains: "Pattern variables:"
@@ -24,4 +24,4 @@ scenarios:
2424
WORKTREE_STRATEGY: sibling-repo
2525
expect:
2626
exit_code: 0
27-
output_contains: "Strategy: sibling-repo"
27+
output_contains: "Strategy: sibling-repo"

e2e/scenarios/separator.yaml

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# E2E tests for the separator configuration
2+
name: separator
3+
description: Test WORKTREE_SEPARATOR controls how slashes in value variables are replaced
4+
5+
scenarios:
6+
- name: separator_default_preserves_slashes
7+
description: Default separator "/" preserves slashes as subdirectories
8+
steps:
9+
- run: wt create feature/deep/branch
10+
env:
11+
WORKTREE_STRATEGY: global
12+
expect:
13+
exit_code: 0
14+
- run: pwd
15+
expect:
16+
cwd_ends_with: /worktrees/test-repo/feature/deep/branch
17+
18+
- name: separator_dash
19+
description: Separator "-" replaces slashes with dashes
20+
steps:
21+
- run: wt create feature/dash-test
22+
env:
23+
WORKTREE_STRATEGY: custom
24+
WORKTREE_PATTERN: "{.worktreeRoot}/{.repo.Name}/{.branch}"
25+
WORKTREE_SEPARATOR: "-"
26+
expect:
27+
exit_code: 0
28+
- run: pwd
29+
expect:
30+
cwd_ends_with: /worktrees/test-repo/feature-dash-test
31+
32+
- name: separator_underscore
33+
description: Separator "_" replaces slashes with underscores
34+
steps:
35+
- run: wt create feature/underscore-test
36+
env:
37+
WORKTREE_STRATEGY: custom
38+
WORKTREE_PATTERN: "{.worktreeRoot}/{.repo.Name}/{.branch}"
39+
WORKTREE_SEPARATOR: "_"
40+
expect:
41+
exit_code: 0
42+
- run: pwd
43+
expect:
44+
cwd_ends_with: /worktrees/test-repo/feature_underscore-test
45+
46+
- name: separator_double_dash
47+
description: Separator "--" replaces slashes with double dashes
48+
steps:
49+
- run: wt create feature/double
50+
env:
51+
WORKTREE_STRATEGY: custom
52+
WORKTREE_PATTERN: "{.worktreeRoot}/{.repo.Name}/{.branch}"
53+
WORKTREE_SEPARATOR: "--"
54+
expect:
55+
exit_code: 0
56+
- run: pwd
57+
expect:
58+
cwd_ends_with: /worktrees/test-repo/feature--double
59+
60+
- name: separator_empty_removes_slashes
61+
description: Empty separator removes slashes entirely
62+
skip_os: [windows] # PowerShell treats empty env vars as unset
63+
steps:
64+
- run: wt create feature/empty
65+
env:
66+
WORKTREE_STRATEGY: custom
67+
WORKTREE_PATTERN: "{.worktreeRoot}/{.repo.Name}/{.branch}"
68+
WORKTREE_SEPARATOR: ""
69+
expect:
70+
exit_code: 0
71+
- run: pwd
72+
expect:
73+
cwd_ends_with: /worktrees/test-repo/featureempty
74+
75+
- name: separator_with_sibling_repo
76+
description: Separator works with sibling-repo strategy
77+
steps:
78+
- run: wt create feature/sibling-sep
79+
env:
80+
WORKTREE_STRATEGY: sibling-repo
81+
WORKTREE_SEPARATOR: "-"
82+
expect:
83+
exit_code: 0
84+
- run: pwd
85+
expect:
86+
cwd_ends_with: /test-repo-feature-sibling-sep
87+
88+
- name: separator_multiple_slashes
89+
description: Separator replaces multiple slashes in branch name
90+
steps:
91+
- run: wt create a/b/c/d
92+
env:
93+
WORKTREE_STRATEGY: custom
94+
WORKTREE_PATTERN: "{.worktreeRoot}/{.repo.Name}/{.branch}"
95+
WORKTREE_SEPARATOR: "_"
96+
expect:
97+
exit_code: 0
98+
- run: pwd
99+
expect:
100+
cwd_ends_with: /worktrees/test-repo/a_b_c_d
101+
102+
- name: separator_no_effect_without_slashes
103+
description: Separator has no effect on branches without slashes
104+
steps:
105+
- run: wt create simple-branch
106+
env:
107+
WORKTREE_STRATEGY: custom
108+
WORKTREE_PATTERN: "{.worktreeRoot}/{.repo.Name}/{.branch}"
109+
WORKTREE_SEPARATOR: "-"
110+
expect:
111+
exit_code: 0
112+
- run: pwd
113+
expect:
114+
cwd_ends_with: /worktrees/test-repo/simple-branch

e2e/scenarios/worktree-strategy.yaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ scenarios:
2121
- run: wt create feature/sibling
2222
env:
2323
WORKTREE_STRATEGY: sibling-repo
24+
WORKTREE_SEPARATOR: "-"
2425
expect:
2526
exit_code: 0
2627
- run: pwd
@@ -100,12 +101,13 @@ scenarios:
100101
exit_code: 1
101102

102103
- name: strategy_branch_sanitized
103-
description: Branch names are sanitized when using branchSafe
104+
description: Separator replaces slashes in branch names
104105
steps:
105106
- run: wt create feature/sanitized
106107
env:
107108
WORKTREE_STRATEGY: custom
108-
WORKTREE_PATTERN: "{.worktreeRoot}/{.repo.Name}/{.branchSafe}"
109+
WORKTREE_PATTERN: "{.worktreeRoot}/{.repo.Name}/{.branch}"
110+
WORKTREE_SEPARATOR: "-"
109111
expect:
110112
exit_code: 0
111113
- run: pwd
@@ -158,7 +160,8 @@ scenarios:
158160
env:
159161
WORKTREE_ROOT: worktrees
160162
WORKTREE_STRATEGY: custom
161-
WORKTREE_PATTERN: "{.repo.Main}/../{.worktreeRoot}/custom/{.repo.Host}/{.repo.Owner}/{.repo.Name}/{.branchSafe}"
163+
WORKTREE_PATTERN: "{.repo.Main}/../{.worktreeRoot}/custom/{.repo.Host}/{.repo.Owner}/{.repo.Name}/{.branch}"
164+
WORKTREE_SEPARATOR: "-"
162165
expect:
163166
exit_code: 0
164167
- run: pwd
@@ -192,6 +195,7 @@ scenarios:
192195
- run: wt checkout checkout-sibling-branch
193196
env:
194197
WORKTREE_STRATEGY: sibling-repo
198+
WORKTREE_SEPARATOR: "-"
195199
expect:
196200
exit_code: 0
197201
- run: pwd
@@ -206,6 +210,7 @@ scenarios:
206210
- run: wt checkout remove-sibling-branch
207211
env:
208212
WORKTREE_STRATEGY: sibling-repo
213+
WORKTREE_SEPARATOR: "-"
209214
expect:
210215
exit_code: 0
211216
- cd: $REPO_DIR

0 commit comments

Comments
 (0)