Files

179 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# APP-3709: Auto-Generate Worktree Branch Names — Tech Spec
## Problem
`generate_worktree_branch_name()` in `new_worktree_modal.rs` uses a global `AtomicU32` counter to produce `worktree-1`, `worktree-2`, etc. These names are forgettable, collide across sessions, and provide no uniqueness guarantee against existing branches in the target repo. The product spec calls for a themed naming function that combines two random words from a 198-word desert/southwest vocabulary and guarantees uniqueness by escalating to more words on collision.
## Relevant Code
- `warp_util/src/worktree_names.rs` — pure naming module (word list, `generate_name`, `generate_unique_name`, `generate_worktree_branch_name`)
- `app/src/util/git.rs:57-78``list_local_branches_sync()` (synchronous branch listing)
- `app/src/tab_configs/tab_config.rs:62-91``TabConfigPaneNode` struct (`worktree_name_autogenerated`)
- `app/src/tab_configs/tab_config.rs:139-174``render_tab_config()` (threads `worktree_branch_name` through to resolution)
- `app/src/tab_configs/tab_config.rs:299-324``resolve_pane_node()` leaf branch (template variable substitution)
- `app/src/tab_configs/tab_config.rs:350-403``build_worktree_config_toml()` (TOML generation for both manual and autogenerate modes)
- `app/src/workspace/view.rs:7138-7153``maybe_generate_worktree_name()` (scans panes, fetches branches, generates name)
- `app/src/workspace/view.rs:7163-7230``handle_new_worktree_submit()` (writes TOML, opens config)
- `app/src/workspace/view.rs:4892-4958``open_tab_config_with_params()` / `open_tab_config()` (re-open path)
- `app/src/tab_configs/new_worktree_modal.rs:224-257``try_submit()` (emits `None` for autogenerate, `Some(name)` for manual)
## Current State
**Modal flow**: When the user clicks "Open" with autogenerate checked, `try_submit()` calls `generate_worktree_branch_name()` which returns `worktree-{N}`. The workspace handler then bakes this name directly into the TOML commands (`git worktree add -b worktree-1 ../worktree-1 main`) and writes it to `~/.warp/tab_configs/`.
**Saved config re-open flow**: When a saved worktree config is clicked in the menu, `open_tab_config()``open_tab_config_with_params()``render_tab_config()``resolve_pane_tree()` renders the commands. Currently the branch name is hardcoded in the TOML, so re-opening always tries to create the same branch — which fails if it already exists.
**Branch listing**: `DiffStateModel::get_all_branches()` in `diff_state.rs` runs `git for-each-ref --sort=-committerdate --format=%(refname:short) refs/heads` asynchronously. The `BranchPicker` uses this. There is no synchronous branch-listing utility.
**Rand usage**: The codebase uses `rand::seq::SliceRandom` with `rand::thread_rng()` (e.g. `agent_tips.rs:467`). `rand` is already a workspace dependency in `app/Cargo.toml`.
## Proposed Changes
### 1. Pure naming module in `warp_util`
New file: `warp_util/src/worktree_names.rs`. This module has zero I/O — it takes a set of existing branch names and returns a unique name. Keeping it in `warp_util` isolates it from the `app` crate's compile graph and keeps it trivially testable.
`rand` needs to be added as a dependency of `warp_util` (it's already a workspace dep).
**Word list**: A `const WORDS: &[&str]` array with the 198 desert/southwest words from the product spec, sorted alphabetically within each category section for readability.
**Core function** (pure, deterministic with a seeded RNG):
```rust
fn generate_name(
word_count: usize,
existing_branches: &HashSet<&str>,
rng: &mut impl rand::Rng,
) -> Option<String>
```
- Picks `word_count` distinct words at random from `WORDS`.
- Joins them with `-`.
- If the result is in `existing_branches`, retries (bounded by `MAX_RETRIES_PER_LEVEL = 2`).
- Returns `Some(name)` on success, `None` if all retries collided.
**Escalating uniqueness wrapper** (pure, deterministic with a seeded RNG):
```rust
pub fn generate_unique_name(
existing_branches: &HashSet<&str>,
rng: &mut impl rand::Rng,
) -> String
```
- Starts at `word_count = 2`.
- Calls `generate_name(word_count, existing_branches, rng)`.
- If `None` (all retries collided), increments `word_count` and retries.
- Cap at `word_count = 5` (198^5 ≈ 2.9 × 10^11 possibilities) as a safety bound, then fall back to appending a random numeric suffix.
**Convenience entry point** (uses thread_rng):
```rust
pub fn generate_worktree_branch_name(
existing_branches: &HashSet<&str>,
) -> String
```
- Calls `generate_unique_name(existing_branches, &mut rand::thread_rng())`.
- This is the API call sites use. The `rng`-parameterized version exists for deterministic testing.
### 2. Synchronous git branch listing in `app/src/util/git.rs`
New function alongside the existing async helpers:
```rust
#[cfg(feature = "local_fs")]
pub fn list_local_branches_sync(repo_path: &Path) -> HashSet<String>
```
- Runs `git branch --list --format=%(refname:short)` via `std::process::Command`.
- Returns a `HashSet<String>` of local branch names.
- On failure (not a git repo, git not found, etc.) returns an empty set.
**Why synchronous**: The call sites (`handle_new_worktree_submit`, `maybe_generate_worktree_name` in `open_tab_config`) are synchronous view handlers. `git branch --list` is a local filesystem read (packed-refs + loose refs) that completes in single-digit milliseconds even for repos with thousands of branches. A synchronous `std::process::Command` is the simplest approach and avoids threading async through the view layer.
### 3. Template variable substitution in commands
The `commands` array on `TabConfigPaneNode` already exists. The change is in how commands are rendered at open time.
When `worktree_branch_name` is provided, `render_tab_config` injects it into both the unquoted and quoted Handlebars context maps under the key `autogenerated_branch_name`. The existing `handlebars::render_template(cmd, quoted)` call in `resolve_pane_node` handles substitution — no custom `.replace()` needed. This is consistent with how all other tab config params (`{{branch}}`, `{{repo}}`, etc.) work.
The `commands` array in the TOML uses `{{autogenerated_branch_name}}` as a Handlebars template variable. Users can freely edit the commands — reorder them, add their own (e.g. `gt branch create`, `npm install`), or use `{{autogenerated_branch_name}}` in custom commands. The default commands written by the modal work out-of-the-box without any user editing.
### 4. Plumbing the generated name through `open_tab_config`
Modify `open_tab_config()` in `workspace/view.rs`: before rendering, scan the config's panes for any with `worktree_name_autogenerated = true`. If found:
1. Read the pane's `cwd` to determine the repo path.
2. Call `list_local_branches_sync(repo_path)` to get existing branches.
3. Call `generate_worktree_branch_name(&existing_branches)` to get a fresh name.
4. Pass the generated name into `render_tab_config` (new parameter), which injects it into the Handlebars context so `{{autogenerated_branch_name}}` in commands gets substituted.
`render_tab_config` gains an optional `worktree_branch_name: Option<&str>` parameter. When `Some`, the name is added to the template context; when `None`, no extra context is injected. This keeps the API change minimal for all non-worktree configs.
### 5. TOML generation in `handle_new_worktree_submit`
Change `handle_new_worktree_submit()` in `workspace/view.rs` to write the config with template variables in the commands:
```toml
[[panes]]
id = "main"
type = "terminal"
cwd = "/path/to/repo"
worktree_name_autogenerated = true
commands = [
"git worktree add -b {{autogenerated_branch_name}} ../{{autogenerated_branch_name}} main",
"cd ../{{autogenerated_branch_name}}",
]
```
The `commands` array uses `{{autogenerated_branch_name}}` — the same Handlebars syntax as all other tab config params. Users can edit the TOML to add custom commands (e.g. `gt branch create`, `npm install`) or reorder them.
### 6. Update modal and workspace handler call sites
In `new_worktree_modal.rs`:
- Remove `WORKTREE_COUNTER` and the old `generate_worktree_branch_name()`.
- `try_submit()` emits `worktree_branch_name: None` when autogenerate is on, `Some(name)` when the user typed a name manually. The modal does not generate names — it delegates that to the workspace handler.
In `workspace/view.rs`:
- `handle_new_worktree_submit()` receives `worktree_branch_name: Option<&str>`. When `None`, it calls `list_local_branches_sync` then `generate_worktree_branch_name` to produce a fresh name. This name is used both as the TOML filename hint and as the branch name for the initial open.
- `maybe_generate_worktree_name()` is a shared helper used by both `open_tab_config()` (re-open path) and `handle_tab_config_params_modal_body_event()` (params modal submit). It scans panes for `worktree_name_autogenerated = true`, fetches branches, and generates a name.
### 7. Module registration
Add `pub mod worktree_names;` to `warp_util/src/lib.rs`.
## End-to-End Flow
### Modal flow (new worktree)
1. User clicks "Open" with autogenerate checked.
2. `try_submit()` emits `Submit { repo, branch: "main", worktree_branch_name: None }`.
3. `handle_new_worktree_submit()` receives `None`, calls `list_local_branches_sync(repo_path)` to get existing branches.
4. Passes the branch set to `generate_worktree_branch_name(&branches)`. Naming function generates a 2-word name; on collision, retries at the same word count then escalates.
5. Returns e.g. `mesa-coyote`.
6. Workspace handler writes TOML with `worktree_name_autogenerated = true` and `commands` containing `{{autogenerated_branch_name}}` template variables via `build_worktree_config_toml`.
7. Parses the TOML back into a `TabConfig` and calls `open_tab_config_with_params` directly with the just-generated name — no second generation pass.
### Re-open flow (saved config)
1. User clicks a saved worktree config in the menu.
2. `open_tab_config()` scans panes, finds `worktree_name_autogenerated = true`.
3. Reads `cwd` from the pane to determine the repo path.
4. Calls `list_local_branches_sync(repo_path)``HashSet`.
5. Calls `generate_worktree_branch_name(&branches)` → e.g. `obsidian-monsoon`.
6. Passes the name into `render_tab_config(..., Some("obsidian-monsoon"))`.
7. `render_tab_config` injects `obsidian-monsoon` into the Handlebars context, and `resolve_pane_node` renders `{{autogenerated_branch_name}}``obsidian-monsoon` in all commands via normal Handlebars substitution.
8. New tab opens with the substituted commands.
## Risks and Mitigations
- **Synchronous git call on main thread**: `git branch --list` is a local filesystem read (packed-refs + loose refs). Completes in single-digit milliseconds even for repos with thousands of branches. If this ever becomes a concern, the function can be made async without changing the API surface.
- **Word list staleness**: The 198-word list is a compile-time constant. No runtime mechanism to update it. This is intentional — the list is a curated vocabulary, not a growing dictionary.
## Testing and Validation
- **Unit tests in `warp_util/src/worktree_names.rs`**:
- `generate_name` with a seeded RNG produces deterministic output.
- Generated names match `{word}-{word}` format, both words are in `WORDS`, words are distinct.
- `generate_unique_name` avoids all names in a provided `existing_branches` set.
- Escalation: pre-fill `existing_branches` with enough 2-word combos to force a 3-word name, verify output has 3 words.
- All words in `WORDS` are valid git branch name components (no spaces, no `..`, no control chars, no leading `-`).
- **Unit test for template substitution**: Verify that a pane with `worktree_name_autogenerated = true` and commands containing `{{autogenerated_branch_name}}` produces the expected substituted commands.
- **Unit test for custom commands**: Verify that user-added commands (with and without `{{autogenerated_branch_name}}`) are preserved and substituted correctly.
- **`cargo check`**: Verify no compilation errors after all changes.
## Follow-ups
- Add a test that exercises the synchronous `git branch --list` path against a real temporary git repo.
- Consider supporting a `worktree_path_template` field on the pane for users who want worktrees in a non-default location (e.g. `~/worktrees/{name}` instead of `../{name}`).