Skip to content

Commit 36331a3

Browse files
authored
feat: add wtt init command and symlink_files config support (#6)
1 parent 65f5093 commit 36331a3

File tree

8 files changed

+295
-32
lines changed

8 files changed

+295
-32
lines changed

.wtt.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
# wtt configuration
2+
# See: https://github.com/songtov/wtt
13

4+
# Directory for worktrees (relative to repo root)
5+
# worktree_dir = "../<reponame>-worktrees"
6+
7+
# Files to copy into new worktrees
28
copy_files = [".env", ".gitignore"]
39

10+
# Directories to copy into new worktrees
11+
# copy_dirs = []
12+
13+
# Files to symlink (shared with main repo) into new worktrees
14+
symlink_files = [".claude/settings.local.json"]
15+
16+
# Commands to run after creating a worktree
17+
post_create = ["echo hello"]

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION ?= v0.5.0
1+
VERSION ?= v0.6.0
22

33
build:
44
go build -ldflags "-X github.com/songtov/wtt/cmd.Version=$(VERSION)" -o wtt-bin .

README.md

Lines changed: 170 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# wtt — Git Worktree Manager
22

3-
**wtt** stands for **worktree-tov**.
3+
A fast, opinionated Git worktree manager.
44

5-
`wtt` wraps `git worktree` to create, navigate, and remove worktrees with minimal friction.
5+
**wtt** = **w**ork**t**ree-**tov**
6+
7+
---
68

79
## Installation
810

@@ -11,74 +13,219 @@ brew tap songtov/tap
1113
brew install wtt
1214
```
1315

14-
To update to the latest version:
16+
To update:
1517

1618
```sh
1719
brew update && brew upgrade wtt
1820
```
1921

20-
Then add the shell integration so `wtt` can `cd` for you.
22+
[fzf](https://github.com/junegunn/fzf) is optional but recommended — used for interactive pickers.
23+
24+
### Shell integration
25+
26+
The shell wrapper is what lets `wtt` `cd` into worktrees. Add one of these to your shell config and reload.
2127

22-
**zsh**add to `~/.zshrc`:
28+
**zsh**`~/.zshrc`:
2329
```sh
2430
eval "$(wtt-bin --init zsh)"
2531
```
2632

27-
**bash**add to `~/.bashrc` or `~/.bash_profile`:
33+
**bash**`~/.bashrc` or `~/.bash_profile`:
2834
```sh
2935
eval "$(wtt-bin --init bash)"
3036
```
3137

32-
**fish**add to `~/.config/fish/config.fish`:
38+
**fish**`~/.config/fish/config.fish`:
3339
```sh
3440
wtt-bin --init fish | source
3541
```
3642

37-
Reload your shell after (`source ~/.zshrc` or equivalent).
43+
`--init` also wires up [shell prompt integration](#shell-prompt) automatically — no extra config needed.
3844

39-
## Usage
45+
---
46+
47+
## Quick Start
4048

4149
```sh
42-
# Create a worktree — auto-generates a branch name if omitted
50+
# Create a worktree with a random branch name, then navigate into it
4351
wtt create
52+
53+
# Create a worktree for a specific branch
4454
wtt create feature/login
4555

46-
# Navigate to a worktree (cds into the directory)
47-
wtt feature/login
56+
# Create a worktree branching from a specific ref
57+
wtt create feature/login -b main
4858

49-
# Pick a worktree interactively with fzf, then navigate
59+
# Pick a worktree interactively and navigate to it
5060
wtt list
5161

52-
# Remove a worktree — pick interactively if no branch given
53-
wtt remove
62+
# Remove a worktree (interactive picker if no branch given)
5463
wtt remove feature/login
55-
wtt remove --force feature/login # skip confirmation
5664
```
5765

66+
---
67+
68+
## Commands
69+
70+
| Command | Description |
71+
|---|---|
72+
| `wtt create [branch]` | Create a new worktree |
73+
| `wtt list` | List worktrees and navigate interactively |
74+
| `wtt remove [branch]` | Remove a worktree |
75+
| `wtt <branch>` | Navigate directly to a worktree |
76+
| `wtt init` | Scaffold a `.wtt.toml` config file |
77+
| `wtt repo list` | Switch the active repository context |
78+
| `wtt repo remove` | Remove a repo from the known repos list |
79+
| `wtt version` | Print version |
80+
81+
### `wtt create [branch]`
82+
83+
Creates a new worktree and navigates into it.
84+
85+
```sh
86+
wtt create # random name (e.g. myrepo-crisp-summit)
87+
wtt create feature/login # specific branch
88+
wtt create feature/login -b main # branch from main instead of HEAD
89+
```
90+
91+
| Flag | Description |
92+
|---|---|
93+
| `-b, --base <ref>` | Base commit/branch/ref (default: `HEAD`) |
94+
95+
### `wtt list`
96+
97+
Opens an fzf picker to select and navigate to a worktree. Falls back to a numbered list if fzf is not installed.
98+
99+
```sh
100+
wtt list
101+
```
102+
103+
### `wtt remove [branch]`
104+
105+
Removes a worktree. Opens an interactive picker if no branch is given.
106+
107+
```sh
108+
wtt remove # pick interactively
109+
wtt remove feature/login
110+
wtt remove -f feature/login # skip confirmation + force-remove from git
111+
```
112+
113+
| Flag | Description |
114+
|---|---|
115+
| `-f, --force` | Skip confirmation prompt and pass `--force` to `git worktree remove` |
116+
117+
### `wtt <branch>`
118+
119+
Navigate directly to a worktree by branch name.
120+
121+
```sh
122+
wtt feature/login
123+
```
124+
125+
### `wtt init`
126+
127+
Scaffolds a `.wtt.toml` in the repo root with commented-out defaults.
128+
129+
```sh
130+
wtt init # fails if .wtt.toml already exists
131+
wtt init -f # overwrite existing file
132+
```
133+
134+
| Flag | Description |
135+
|---|---|
136+
| `-f, --force` | Overwrite an existing `.wtt.toml` |
137+
138+
### `wtt repo list`
139+
140+
Switches the active repository context — kubens-style. After switching, all `wtt` commands (`create`, `list`, `remove`) operate on the selected repo, even when run from outside it.
141+
142+
```sh
143+
wtt repo list
144+
```
145+
146+
### `wtt repo remove`
147+
148+
Removes a repository from the known repos list.
149+
150+
```sh
151+
wtt repo remove
152+
wtt repo remove -f # skip confirmation
153+
```
154+
155+
| Flag | Description |
156+
|---|---|
157+
| `-f, --force` | Skip confirmation prompt |
158+
159+
---
160+
58161
## Configuration
59162

60-
Create `.wtt.toml` in your repo root to customize behavior:
163+
Run `wtt init` to create a `.wtt.toml` in your repo root, then edit it:
164+
165+
```sh
166+
wtt init
167+
```
168+
169+
### Config keys
170+
171+
| Key | Type | Default | Description |
172+
|---|---|---|---|
173+
| `worktree_dir` | string | `../<repo>-worktrees` | Directory where worktrees are created |
174+
| `copy_files` | list | `[".gitignore"]` | Files copied from the main worktree into each new worktree |
175+
| `copy_dirs` | list | `[]` | Directories copied recursively into each new worktree |
176+
| `symlink_files` | list | `[]` | Files symlinked (not copied) — changes in one worktree are shared across all |
177+
| `post_create` | list | `[]` | Shell commands run inside the new worktree after creation |
178+
179+
### Example `.wtt.toml`
61180

62181
```toml
63-
# Where to put worktrees (default: ../<repo>-worktrees)
182+
# Where to create worktrees (default: ../<repo>-worktrees)
64183
worktree_dir = "../myproject-worktrees"
65184

66-
# Files to copy from main worktree into each new worktree (default: [".gitignore"])
185+
# Files copied into each new worktree
67186
copy_files = [".gitignore", ".env.local"]
68187

69-
# Directories to copy recursively
70-
copy_dirs = []
188+
# Directories copied recursively
189+
copy_dirs = ["scripts"]
190+
191+
# Files symlinked — edits in any worktree are reflected everywhere
192+
symlink_files = [".env.secrets"]
71193

72-
# Commands to run inside the new worktree after creation
194+
# Commands run inside the new worktree after creation
73195
post_create = ["npm install"]
74196
```
75197

198+
---
199+
200+
## Shell Prompt
201+
202+
`--init` automatically injects the active repository name into your shell prompt — no manual configuration needed.
203+
204+
| Shell | Where it appears | Framework notes |
205+
|---|---|---|
206+
| zsh | Right prompt (`RPROMPT`) | Auto-injects into Powerlevel10k as a custom segment; falls back to generic `RPROMPT` for plain zsh and oh-my-zsh |
207+
| bash | Left prompt (`PS1`) | Prepended via `PROMPT_COMMAND` |
208+
| fish | Right prompt | Defines `fish_right_prompt` if not already set; for Tide/Starship call `wtt_segment` from your theme hook |
209+
210+
The prompt segment is powered by `wtt-bin context` internally. There is no separate `wtt context` command intended for direct use.
211+
212+
---
213+
76214
## How It Works
77215

78-
A child process can't change the parent shell's directory, so `wtt` ships as `wtt-bin` plus a shell wrapper function. When a command prints a directory path, the wrapper `cd`s into it — the same pattern used by `nvm` and `direnv`.
216+
A child process can't change the parent shell's directory, so `wtt` ships as two parts:
217+
218+
1. **`wtt-bin`** — the Go binary that does the work
219+
2. **`wtt`** — a shell function (installed by `--init`) that runs `wtt-bin` and `cd`s into the output if it's a directory
220+
221+
This is the same pattern used by tools like `nvm` and `direnv`.
79222

80223
Worktrees land at `../<repo>-worktrees/<branch>/` by default. Slashes in branch names become dashes (`feature/login``feature-login`).
81224

225+
When fzf is not installed, interactive pickers fall back to a numbered list — `wtt list` and `wtt remove` always work regardless.
226+
227+
---
228+
82229
## Contributing
83230

84231
Contributions are welcome! Here's how to get started:

cmd/create.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ func runCreate(cmd *cobra.Command, args []string) error {
7272
if err := worktree.CopyDirs(repoRoot, worktreePath, cfg.CopyDirs); err != nil {
7373
fmt.Fprintf(os.Stderr, "Warning: copying dirs: %v\n", err)
7474
}
75+
if err := worktree.SymlinkFiles(repoRoot, worktreePath, cfg.SymlinkFiles); err != nil {
76+
fmt.Fprintf(os.Stderr, "Warning: symlinking files: %v\n", err)
77+
}
7578

7679
// Run post_create commands
7780
for _, command := range cfg.PostCreate {

cmd/init.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/spf13/cobra"
9+
)
10+
11+
const wttTomlTemplate = `# wtt configuration
12+
# See: https://github.com/songtov/wtt
13+
14+
# Directory for worktrees (relative to repo root)
15+
# worktree_dir = "../<reponame>-worktrees"
16+
17+
# Files to copy into new worktrees
18+
copy_files = [".env", ".gitignore"]
19+
20+
# Directories to copy into new worktrees
21+
# copy_dirs = []
22+
23+
# Files to symlink (shared with main repo) into new worktrees
24+
symlink_files = [".claude/settings.local.json"]
25+
26+
# Commands to run after creating a worktree
27+
# post_create = []
28+
`
29+
30+
var initForce bool
31+
32+
func init() {
33+
initCmd.Flags().BoolVarP(&initForce, "force", "f", false, "Overwrite existing .wtt.toml")
34+
}
35+
36+
var initCmd = &cobra.Command{
37+
Use: "init",
38+
Short: "Create a .wtt.toml configuration file",
39+
Long: `Scaffold a .wtt.toml configuration file in the repo root.`,
40+
Args: cobra.NoArgs,
41+
RunE: runInit,
42+
}
43+
44+
func runInit(cmd *cobra.Command, args []string) error {
45+
repoRoot, err := repoRootWithFallback()
46+
if err != nil {
47+
return err
48+
}
49+
50+
dest := filepath.Join(repoRoot, ".wtt.toml")
51+
52+
if _, err := os.Stat(dest); err == nil && !initForce {
53+
fmt.Fprintf(os.Stderr, "`.wtt.toml` already exists. Use `wtt init -f` to overwrite.\n")
54+
return nil
55+
}
56+
57+
if err := os.WriteFile(dest, []byte(wttTomlTemplate), 0o644); err != nil {
58+
return fmt.Errorf("writing .wtt.toml: %w", err)
59+
}
60+
61+
fmt.Fprintf(os.Stderr, "Created %s\n", dest)
62+
return nil
63+
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func init() {
3434
rootCmd.AddCommand(versionCmd)
3535
rootCmd.AddCommand(repoCmd)
3636
rootCmd.AddCommand(contextCmd)
37+
rootCmd.AddCommand(initCmd)
3738
}
3839

3940
// repoRootWithFallback returns the git repo root for the current directory.

0 commit comments

Comments
 (0)