Skip to content

feat: add -m/-M flag to rename a worktree's directory and branch#195

Merged
k1LoW merged 11 commits into
mainfrom
feat/move-worktree
Jun 4, 2026
Merged

feat: add -m/-M flag to rename a worktree's directory and branch#195
k1LoW merged 11 commits into
mainfrom
feat/move-worktree

Conversation

@k1LoW

@k1LoW k1LoW commented Jun 4, 2026

Copy link
Copy Markdown
Owner

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 (via git branch -M) and allows moving worktrees with uncommitted changes (via git worktree move --force).

Design decisions

  • Argument shape: mirrors git branch -m (1-arg form acts on the current branch; 2-arg form takes old/new). Two args are always <old> <new>.
  • Mismatched dir/branch (created via -b): both directory and branch are renamed to <new>, treating the worktree as a single named entity.
  • Detached HEAD: error. There is no branch to rename, and silently renaming just the directory felt like the wrong default.
  • Main working tree: cannot be renamed. The -m <new> form only acts on linked worktrees.
  • Default branch protection: reuses --allow-delete-default. Without the flag, attempting to rename a worktree whose branch is the default fails.
  • Slash-style branches (e.g. feat/foo): empty parent directories under wt.basedir are cleaned up after the move. The cleanup also removes the .gitignore / README.md files that initBaseDir plants in every intermediate directory; without that, .wt/feat/ would never look "empty" after renaming the only child out.
  • Conflicts: target directory existence and target branch existence are checked up front and produce a clear error pointing at -M. With -M, git worktree move --force and git branch -M handle the rest.
  • Hooks: wt.hook and wt.deletehook do not fire on rename. Rename is neither a creation nor a deletion.
  • Shell integration: when the current worktree is renamed, the new path is printed on the last line so the shell wrapper cds there. The bash/zsh/fish/powershell wrappers now detect -m/-M in argv so wt.nocd=create still allows the cd (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, -m rejects -b, rejects 3+ args.
  • TestE2E_MoveCurrentWorktree: prints new path on last line under GIT_WT_SHELL_INTEGRATION=1, stays silent otherwise, and bash/zsh/fish wrappers actually cd to the new path.
  • make test and make lint clean.

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.
@github-actions

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.
@github-actions

This comment has been minimized.

This comment was marked as outdated.

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".
@github-actions

This comment has been minimized.

This comment was marked as outdated.

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.
@github-actions

This comment has been minimized.

This comment was marked as outdated.

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.
@github-actions

This comment has been minimized.

This comment was marked as outdated.

…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.
@github-actions

This comment has been minimized.

This comment was marked as outdated.

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`).
@github-actions

This comment has been minimized.

This comment was marked as outdated.

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.
@github-actions

This comment has been minimized.

This comment was marked as outdated.

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.
@github-actions

This comment has been minimized.

This comment was marked as outdated.

`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.
@github-actions

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@k1LoW k1LoW added enhancement New feature or request tagpr:minor labels Jun 4, 2026
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.
@k1LoW k1LoW marked this pull request as ready for review June 4, 2026 04:33
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Code Metrics Report

main (243cb53) #195 (f67f10e) +/-
Coverage 38.8% 33.0% -5.8%
Code to Test Ratio 1:2.2 1:2.2 -0.1
Test Execution Time 18s 20s +2s
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%)

Files Coverage +/- Status
cmd/init.go 0.0% 0.0% modified
cmd/root.go 0.0% 0.0% modified
internal/git/branch.go 51.0% -8.6% modified
internal/git/repo_context.go 78.6% -1.4% modified
internal/git/worktree.go 44.1% -14.1% modified

Reported by octocov

@k1LoW k1LoW merged commit 3fe5731 into main Jun 4, 2026
3 checks passed
@k1LoW k1LoW deleted the feat/move-worktree branch June 4, 2026 05:28
@github-actions github-actions Bot mentioned this pull request May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request tagpr:minor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: support renaming a worktree (directory + branch) in a single operation by -m (-M) flag

2 participants