1<!--
2SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
3
4SPDX-License-Identifier: CC0-1.0
5-->
6
7# wt, Lua worktree manager
8
9## Commands
10- `make all` — format, lint, type-check, and test (the standard workflow)
11- `make test-quiet` — **use this!** shows summary on success, full output on failure
12- `make test` — verbose test output (busted)
13- `lx test spec/foo_spec.lua` — run a single test file
14- `lx run` — run the application (src/main.lua)
15- `make dist` — bundle all modules into `dist/wt` single-file executable
16- `make install` — install to `~/.local/bin/`
17- `make ci` — CI mode (format check + lint + check + test)
18
19## Structure
20```
21src/
22├── main.lua # Entry point, command dispatch
23└── wt/
24 ├── exit.lua # Exit codes
25 ├── shell.lua # run_cmd, run_cmd_silent, die, quote
26 ├── path.lua # Path manipulation (branch_to_path, etc.)
27 ├── git.lua # Git operations, project root detection
28 ├── config.lua # Config loading (global + project)
29 ├── hooks.lua # Hook permissions and execution
30 ├── help.lua # Usage text
31 └── cmd/ # One module per command
32spec/ # Tests (busted framework, *_spec.lua)
33scripts/bundle.lua # Builds single-file dist/wt
34```
35
36## Lux Handles These (don't suggest manually)
37- Dependencies: use `lx add <pkg>`, not `luarocks install`
38- Tools (busted, luacheck, stylua): auto-installed on first use
39- Module paths: Lux loader resolves requires; don't manipulate `package.path`
40
41## Code Style
42- Lua 5.4; use LuaCATS annotations (`---@param`, `---@return`, `---@class`)
43- Tabs for indentation, 120 char line limit (see stylua.toml)
44- Prefix intentionally unused variables with `_` to silence luacheck
45
46## Architecture
47
48### Core Pattern: Bare Repo + Worktrees
49wt manages git repositories using a **bare repository structure**:
50- `.bare/` - the actual git repository (moved from `.git/`)
51- `.git` - a file pointing to `.bare/` (makes git commands work)
52- Worktrees checked out as sibling directories alongside `.bare/`
53
54### Module Organization
55Each command lives in `src/wt/cmd/<name>.lua` and exports a `cmd_<name>(args)` function. Commands:
561. Parse flags and positional args from the `args` table
572. Validate inputs with `shell.die(msg)` for user errors
583. Execute via `shell.run_cmd()` / `shell.run_cmd_silent()`
594. Return consistent exit codes from `wt.exit`
60
61Utility modules (`shell`, `git`, `path`, `config`, `hooks`) are shared across commands.
62
63### Exit Codes and Error Handling
64```lua
65local exit = require("wt.exit")
66exit.EXIT_SUCCESS -- 0: normal completion
67exit.EXIT_USER_ERROR -- 1: user mistakes (bad args, conflicts)
68exit.EXIT_SYSTEM_ERROR -- 2: system failures (git errors, IO errors)
69```
70
71Use `shell.die(msg, code)` to print to stderr and exit. Default code is `EXIT_USER_ERROR`.
72
73### Git Command Patterns
74- **Bare repo operations**: `GIT_DIR=<path>/.bare git <command>`
75- **Worktree operations**: `git -C <path> <command>`
76- Always check exit codes from `run_cmd()` before assuming success
77- Use `shell.quote(str)` when embedding user input in commands
78
79### Testing
80
81Tests use busted framework in `spec/`. Key patterns:
82
83```lua
84-- Load wt as module (exports functions when required, not run directly)
85package.path = package.path .. ";./?.lua"
86local wt = dofile("src/main.lua")
87
88-- Use spec/test_helper.lua for git operations
89local helper = require("spec.test_helper")
90local git = helper.git -- Preconfigured git wrapper (suppresses noise)
91
92-- Standard setup/teardown for temp directories
93local temp_dir
94setup(function()
95 local handle = io.popen("mktemp -d")
96 if handle then
97 temp_dir = handle:read("*l")
98 handle:close()
99 end
100end)
101teardown(function()
102 if temp_dir then os.execute("rm -rf " .. temp_dir) end
103end)
104```
105
106The `dofile("src/main.lua")` pattern works because main.lua detects when it's `require`d vs run directly and exports test utilities.
107
108### Distribution
109
110`make dist` bundles all modules into a single `dist/wt` file using `scripts/bundle.lua`. The bundler:
111- Embeds each module as a string literal
112- Provides custom `require()` that loads embedded modules first
113- Falls back to real `require()` for stdlib
114
115**Order matters**: Update `MODULE_ORDER` in `scripts/bundle.lua` when adding new modules (dependencies before dependents).
116
117## Non-Obvious Behaviors
118
1191. **Source Worktree Detection**: `wt a` must run from inside a worktree (not project root) for hooks to trigger.
1202. **Idempotent Add**: Running `wt a <branch>` when worktree exists prints path and succeeds (exit 0), with hints if run from root.
1213. **Branch Existence Checking**: Adding without `-b` checks both local and all remotes. Fails if branch exists on multiple remotes (ambiguous).
1224. **Bare HEAD Management**: During `wt init`, bare repo's HEAD moves to placeholder so default branch can be checked out in worktree.
1235. **Config File Format**: Both `return { ... }` and bare table `{ ... }` are accepted (loader tries prepending `return`).
1246. **Test Module Export**: `src/main.lua` uses `pcall(debug.getlocal, 4, 1)` to detect if it was `require`d and exports internal functions for testing.
125
126## External Dependencies
127- `gum` - interactive prompts (choose, confirm)
128- Standard POSIX: `mkdir`, `cp`, `ln`, `rm`, `mv`, `pwd`, `mktemp`
129- git