feat: add -m/-M flag to rename a worktree's directory and branch#195
Merged
Conversation
Closes #184 Renaming a git-wt worktree previously required several manual steps: `git branch -m`, `git worktree move`, cleaning up empty parent directories left behind when branch names contain `/`, and re-`cd`-ing if invoked from inside the renamed worktree. This adds a single-step rename that completes the create/switch/delete vocabulary. - `git wt -m <new>` renames the current worktree. - `git wt -m <old> <new>` renames a specific worktree. - `-M` is the force variant (overwrites existing target branch, allows moving a dirty worktree). - Default branch is protected by `--allow-delete-default`, consistent with how `-d`/`-D` behave. - Empty intermediate basedir directories (e.g. `.wt/feat/` after renaming `feat/foo` to `flat`) are cleaned up, including the `.gitignore` and `README.md` decoration files that initBaseDir plants in each one. - Detached HEAD worktrees and the main working tree cannot be renamed. - Shell integration: when the current worktree is renamed, the new path is printed on the last line so the shell wrapper `cd`s there. The wrappers now recognize `-m`/`-M` so `wt.nocd=create` still cds (rename targets an existing worktree at a new location, not a creation), matching the semantics agreed on in the issue.
This comment has been minimized.
This comment has been minimized.
- Drop the unused `git.MainRepoRoot(ctx)` call in `moveWorktree`; its error path was firing without the value ever being read. - Harden `RemoveEmptyParents` against escaping basedir: the walk now refuses to start unless `startDir` is a strict descendant of `stopDir`, re-checks the relation after every step up, and only treats `.gitignore` / `README.md` as removable when their content is bit-identical to what `initBaseDir` wrote — so user-edited files in intermediate directories are no longer silently clobbered. - Stop claiming `-M` "overwrites the target directory": `git worktree move --force` does NOT overwrite an existing destination, its `--force` only relaxes dirty/locked-worktree checks. The target-dir pre-flight now runs unconditionally; `-M` is documented as "overwrite existing branch, allow moving dirty/locked worktrees". - Add `git.CheckBranchNameFormat` and call it before any filesystem work in `moveWorktree`. Previously an invalid new name like `foo..bar` would let `git worktree move` succeed and then leave the branch on the old name when `git branch -m` rejected the format.
This comment has been minimized.
This comment has been minimized.
Two Copilot-review findings on `-m`/`-M`: - `moveWorktree` was calling `git.LoadConfig(ctx)` directly, which bypasses the `--basedir` / `--copyignored` / etc. flag-based overrides that `loadConfig(ctx, cmd)` applies. As a result `git wt --basedir ... -m ...` would compute paths against the wrong basedir and could also clean up the wrong tree on the way out. Switch to the same helper deleteWorktrees / handleWorktree use. - The target-directory pre-flight assumed any existing `newPath` was a collision, but `newPath == oldPath` is a legitimate branch-only rename (e.g. a worktree created with `-b` whose directory is already named the new branch). Add a `samePath` guard that skips the collision check and the directory move when paths match, and surface non-`ENOENT` `os.Stat` errors instead of silently treating them as "does not exist".
This comment has been minimized.
This comment has been minimized.
Three Copilot-review findings on the previous fix: - `newPath` was built via `git.WorktreePathFor`, which re-expands `cfg.BaseDir` without resolving symlinks. On macOS that left `newPath` at `/var/...` while `oldPath` (and the resolved `baseDir`) was at `/private/var/...`, silently breaking the `samePath` check. Build `newPath` off the already-symlink-resolved `baseDir` directly. - When `--basedir` is overridden and the user queries by directory name (worktree created with `-b`, dir != branch), `git.FindWorktreeByBranchOrDir` failed to resolve because its basedir-relative match path uses internal `LoadConfig` and never sees the flag override. Add a fallback that retries the lookup with an absolute path under the already-resolved `baseDir`. - `git.RemoveEmptyParents` documented "absolute paths required" but only `filepath.Clean`'d its inputs. A caller handing in a relative path could trigger deletions against cwd; enforce the precondition with an explicit error.
This comment has been minimized.
This comment has been minimized.
Match the style of the other git helpers in this package so any explanation git emits about a malformed ref reaches the user. git check-ref-format is silent today on the common failures, but the suggestion costs nothing and future-proofs against new git output.
This comment has been minimized.
This comment has been minimized.
…fault) The previous wording said `-m`/`-M` and "without worktree" deletion were "blocked entirely", which contradicted the very next bullet that documents `--allow-delete-default` as an override. Restructure the note to lead with the override flag and describe each case as "refused by default" instead.
This comment has been minimized.
This comment has been minimized.
The 1-arg form already blocked the main working tree via rc.IsLinkedWorktree(), but the 2-arg form would resolve through FindWorktreeByBranchOrDir to the main worktree (e.g. `git wt -m . new` or by passing its path) and then defer the error to `git worktree move`, which produces an opaque message. Add a post-resolve guard that compares the resolved source path to MainRepoRoot and surfaces the reason directly. E2E coverage added in two_arg_rejects_main_working_tree (covering both the `.` path query and the branch-name query with `--allow-delete-default`).
This comment has been minimized.
This comment has been minimized.
Two Copilot edge-case findings on the basedir cleanup logic: - `isStrictDescendant` used `strings.HasPrefix(rel, \"..\")` to detect paths escaping the basedir, which also rejected legitimate child names beginning with two dots (e.g. `..cache`). Match only genuine parent traversals: `rel == \"..\"` or `rel` starting with `\"..\"` + the path separator. - `onlyUntouchedDecorationFiles` relied on `DirEntry.IsDir()` to skip subdirectories, but symlinks report `IsDir()==false` regardless of their target. A symlink named `.gitignore` / `README.md` whose target's content matched would have been considered removable, and `os.RemoveAll` would follow the symlink semantics unpredictably. Treat any symlink in the directory as making it non-removable.
This comment has been minimized.
This comment has been minimized.
Two more Copilot findings: - `RenameBranch` now passes branch names after `--` so a name beginning with `-` (e.g. `-q`) is not parsed as an option by `git branch`. `CheckBranchNameFormat` would accept such names, so without `--` the rename would fail later with a confusing error. - The user-facing message in `moveWorktree` had a third case it didn't cover: when `samePath` is true (branch-only rename, e.g. a worktree created with `-b` whose directory already matches the new name), it still said "Renamed worktree to <path>" with `<path>` unchanged. Add a dedicated branch-only message so the output accurately reflects what happened.
This comment has been minimized.
This comment has been minimized.
`completeBranches` was suggesting refs (branches/tags) as the second positional argument under `-m`/`-M`, but in rename mode the second arg is the *new name* (free text), not a ref. Suggesting refs would mislead users into picking an existing branch and then hitting the "branch already exists (use -M to force)" pre-flight. - Extend the start-point guard to also exclude `moveFlag` / `forceMoveFlag` so the suggestion path is skipped under rename. - After the first positional arg is already in place under rename, return `ShellCompDirectiveNoFileComp` so the shell does not fall back to filename completion either.
This comment has been minimized.
This comment has been minimized.
Add two e2e subtests under TestE2E_MoveCurrentWorktree that pin the shell-integration semantics the maintainer specified in #184's comment thread: - nocd_create_still_cds_on_rename: with `wt.nocd=create`, renaming the current worktree must still cd to the new path because rename targets an existing worktree at a new location, not a fresh creation. Covers bash/zsh/fish. - nocd_flag_suppresses_cd_on_rename: `--nocd` always wins over `-m`/`-M`, mirroring the wrapper precedence (nocd_flag is checked before rename_flag). Covers bash/zsh/fish.
Contributor
Code Metrics Report
Details | | main (243cb53) | #195 (f67f10e) | +/- |
|---------------------|----------------|----------------|-------|
- | Coverage | 38.8% | 33.0% | -5.8% |
| Files | 13 | 13 | 0 |
| Lines | 1186 | 1390 | +204 |
- | Covered | 461 | 460 | -1 |
- | Code to Test Ratio | 1:2.2 | 1:2.2 | -0.1 |
| Code | 2505 | 2837 | +332 |
+ | Test | 5714 | 6407 | +693 |
- | Test Execution Time | 18s | 20s | +2s |Code coverage of files in pull request scope (31.4% → 24.7%)
Reported by octocov |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #184
Summary
Renaming a git-wt worktree previously required several manual steps:
git branch -m,git worktree move, cleaning up empty parent directories (notably when branch names contain/), and re-cd-ing if you were inside the renamed worktree. This PR adds a single-step rename that completes the existing create / switch / delete vocabulary.git wt -m <new>renames the current worktree's directory and branch in one go.git wt -m <old> <new>renames an explicit worktree.git wt -M ...is the force variant: overwrites an existing target branch (viagit branch -M) and allows moving worktrees with uncommitted changes (viagit worktree move --force).Design decisions
git branch -m(1-arg form acts on the current branch; 2-arg form takes old/new). Two args are always<old> <new>.-b): both directory and branch are renamed to<new>, treating the worktree as a single named entity.-m <new>form only acts on linked worktrees.--allow-delete-default. Without the flag, attempting to rename a worktree whose branch is the default fails.feat/foo): empty parent directories underwt.basedirare cleaned up after the move. The cleanup also removes the.gitignore/README.mdfiles thatinitBaseDirplants in every intermediate directory; without that,.wt/feat/would never look "empty" after renaming the only child out.-M. With-M,git worktree move --forceandgit branch -Mhandle the rest.wt.hookandwt.deletehookdo not fire on rename. Rename is neither a creation nor a deletion.cds there. The bash/zsh/fish/powershell wrappers now detect-m/-Min argv sowt.nocd=createstill allows thecd(rename targets an existing worktree at a new location, not a creation), matching the semantics agreed on in the issue thread.Test plan
TestE2E_MoveWorktree: two-arg rename, one-arg rename of current, refuses outside a linked worktree, target directory/branch conflicts (safe and force), slash-style branch with parent cleanup, default branch protection (blocked, then allowed with--allow-delete-default), detached HEAD rejected,-mrejects-b, rejects 3+ args.TestE2E_MoveCurrentWorktree: prints new path on last line underGIT_WT_SHELL_INTEGRATION=1, stays silent otherwise, and bash/zsh/fish wrappers actuallycdto the new path.make testandmake lintclean.