Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,20 @@ cd "$(./bin/gtr go test-feature)"
./bin/gtr run test-feature echo "Hello from worktree"
# Expected: Outputs "Hello from worktree"

# Test mv/rename command
./bin/gtr new test-old
./bin/gtr mv test-old test-new --yes
./bin/gtr list
# Expected: Shows test-new, not test-old
./bin/gtr mv 1 something --yes
# Expected: Error "Cannot rename main repository"
./bin/gtr new test-conflict
./bin/gtr mv test-new test-conflict --yes
# Expected: Error "Branch 'test-conflict' already exists"
./bin/gtr rename test-new test-renamed --yes
# Expected: Works (rename alias)
./bin/gtr rm test-renamed test-conflict

# Test copy patterns with include/exclude
git config --add gtr.copy.include "**/.env.example"
git config --add gtr.copy.exclude "**/.env"
Expand Down Expand Up @@ -418,8 +432,8 @@ When adding new commands or flags, update all three completion files:
The codebase includes fallbacks for different Git versions:

- **Git 2.22+**: Uses modern commands like `git branch --show-current`
- **Git 2.5-2.21**: Falls back to `git rev-parse --abbrev-ref HEAD`
- **Minimum**: Git 2.5+ (for basic `git worktree` support)
- **Git 2.17-2.21**: Falls back to `git rev-parse --abbrev-ref HEAD`
- **Minimum**: Git 2.17+ (for `git worktree move/remove` support)

When using Git commands, check if fallbacks exist (search for `git branch --show-current` in `lib/core.sh`) or add them for new features.

Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE.txt)
[![Bash](https://img.shields.io/badge/Bash-3.2%2B-green.svg)](https://www.gnu.org/software/bash/)
[![Git](https://img.shields.io/badge/Git-2.5%2B-orange.svg)](https://git-scm.com/)
[![Git](https://img.shields.io/badge/Git-2.17%2B-orange.svg)](https://git-scm.com/)
[![Platform](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey.svg)](#platform-support)

> A portable, cross-platform CLI for managing git worktrees with ease
Expand Down Expand Up @@ -142,7 +142,7 @@ While `git worktree` is powerful, it's verbose and manual. `git gtr` adds qualit

## Requirements

- **Git** 2.5+ (for `git worktree` support)
- **Git** 2.17+ (for `git worktree move/remove` support)
- **Bash** 3.2+ (macOS ships 3.2; 4.0+ recommended for advanced features)

## Commands
Expand Down Expand Up @@ -231,6 +231,20 @@ git gtr rm my-feature --delete-branch --force # Delete branch and force

**Options:** `--delete-branch`, `--force`, `--yes`

### `git gtr mv <old> <new> [--force] [--yes]`

Rename worktree directory and branch together. Aliases: `rename`

```bash
git gtr mv feature-wip feature-auth # Rename worktree and branch
git gtr mv old-name new-name --force # Force rename locked worktree
git gtr mv old-name new-name --yes # Skip confirmation
```

**Options:** `--force`, `--yes`

**Note:** Only renames the local branch. Remote branch remains unchanged.

### `git gtr copy <target>... [options] [-- <pattern>...]`

Copy files from main repo to existing worktree(s). Useful for syncing env files after worktree creation.
Expand Down Expand Up @@ -359,7 +373,7 @@ git gtr completion fish > ~/.config/fish/completions/git-gtr.fish
| **Linux** | Full support | Ubuntu, Fedora, Arch, etc. |
| **Windows** | Git Bash or WSL | Native PowerShell not supported |

Requires Git 2.5+ and Bash 3.2+.
Requires Git 2.17+ and Bash 3.2+.

> For troubleshooting, platform-specific notes, and architecture details, see [docs/troubleshooting.md](docs/troubleshooting.md)

Expand Down
133 changes: 133 additions & 0 deletions bin/gtr
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ main() {
rm)
cmd_remove "$@"
;;
mv|rename)
cmd_rename "$@"
;;
go)
cmd_go "$@"
;;
Expand Down Expand Up @@ -490,6 +493,125 @@ cmd_remove() {
done
}

# Rename command (rename worktree and branch)
cmd_rename() {
local old_identifier=""
local new_name=""
local force=0
local yes_mode=0

# Parse flags and arguments
while [ $# -gt 0 ]; do
case "$1" in
--force)
force=1
shift
;;
--yes)
yes_mode=1
shift
;;
-*)
log_error "Unknown flag: $1"
exit 1
;;
*)
if [ -z "$old_identifier" ]; then
old_identifier="$1"
elif [ -z "$new_name" ]; then
new_name="$1"
fi
shift
;;
esac
done

# Validate arguments
if [ -z "$old_identifier" ] || [ -z "$new_name" ]; then
log_error "Usage: git gtr mv <old> <new> [--force] [--yes]"
exit 1
fi

local repo_root base_dir prefix
repo_root=$(discover_repo_root) || exit 1
base_dir=$(resolve_base_dir "$repo_root")
prefix=$(cfg_default gtr.worktrees.prefix GTR_WORKTREES_PREFIX "")

# Resolve old worktree
local target is_main old_path old_branch
target=$(resolve_target "$old_identifier" "$repo_root" "$base_dir" "$prefix") || exit 1
is_main=$(echo "$target" | cut -f1)
old_path=$(echo "$target" | cut -f2)
old_branch=$(echo "$target" | cut -f3)

# Cannot rename main repository
if [ "$is_main" = "1" ]; then
log_error "Cannot rename main repository"
exit 1
fi

# Sanitize new name and construct new path
local new_sanitized new_path
new_sanitized=$(sanitize_branch_name "$new_name")
new_path="$base_dir/${prefix}${new_sanitized}"

# Check if new path already exists
if [ -d "$new_path" ]; then
log_error "Worktree already exists at: $new_path"
exit 1
fi

# Check if new branch name already exists
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/$new_name"; then
log_error "Branch '$new_name' already exists"
exit 1
fi

log_step "Renaming worktree"
echo "Branch: $old_branch → $new_name"
echo "Folder: $(basename "$old_path") → ${prefix}${new_sanitized}"

# Confirm unless --yes
if [ "$yes_mode" -eq 0 ]; then
if ! prompt_yes_no "Proceed with rename?"; then
log_info "Cancelled"
exit 0
fi
fi

# Rename the branch first
if ! git -C "$repo_root" branch -m "$old_branch" "$new_name"; then
log_error "Failed to rename branch"
exit 1
fi

# Move the worktree
local move_args=()
if [ "$force" -eq 1 ]; then
move_args+=(--force)
fi

if ! git -C "$repo_root" worktree move "$old_path" "$new_path" "${move_args[@]}"; then
# Rollback: rename branch back
log_warn "Worktree move failed, rolling back branch rename..."
git -C "$repo_root" branch -m "$new_name" "$old_branch" 2>/dev/null || true
log_error "Failed to move worktree"
exit 1
fi

echo ""
log_info "Renamed: $old_branch → $new_name"
log_info "Location: $new_path"

# Check if remote tracking branch exists and warn
if git -C "$repo_root" show-ref --verify --quiet "refs/remotes/origin/$old_branch"; then
echo ""
log_warn "Remote branch 'origin/$old_branch' still exists"
log_info "To update remote, run:"
echo " git push origin :$old_branch $new_name"
fi
}

# Go command (navigate to worktree - prints path for shell integration)
cmd_go() {
if [ $# -ne 1 ]; then
Expand Down Expand Up @@ -1594,6 +1716,17 @@ CORE COMMANDS (daily workflow):
--force: force removal (dirty worktree)
--yes: skip confirmation

mv <old> <new> [--force] [--yes]
Rename worktree and its branch
Aliases: rename
--force: force move (locked worktree)
--yes: skip confirmation
Note: Only renames local branch. Remote branch unchanged.

Examples:
git gtr mv feature-wip feature-auth
git gtr rename old-name new-name

copy <target>... [options] [-- <pattern>...]
Copy files from main repo to worktree(s)
-n, --dry-run: preview without copying
Expand Down
9 changes: 8 additions & 1 deletion completions/_git-gtr
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ _git-gtr() {
'run:Execute command in worktree'
'copy:Copy files between worktrees'
'rm:Remove worktree(s)'
'mv:Rename worktree and branch'
'rename:Rename worktree and branch'
'editor:Open worktree in editor'
'ai:Start AI coding tool'
'ls:List all worktrees'
Expand Down Expand Up @@ -83,7 +85,7 @@ _git-gtr() {
# Complete arguments to the subcommand
elif (( CURRENT == 4 )); then
case "$words[3]" in
go|run|rm|copy)
go|run|rm|mv|rename|copy)
_describe 'branch names' all_options
;;
editor)
Expand Down Expand Up @@ -116,6 +118,11 @@ _git-gtr() {
'--force[Force removal even if dirty]' \
'--yes[Non-interactive mode]'
;;
mv|rename)
_arguments \
'--force[Force move even if locked]' \
'--yes[Skip confirmation]'
;;
copy)
_arguments \
'-n[Dry-run preview]' \
Expand Down
10 changes: 10 additions & 0 deletions completions/git-gtr.fish
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ complete -f -c git -n '__fish_git_gtr_needs_command' -a new -d 'Create a new wor
complete -f -c git -n '__fish_git_gtr_needs_command' -a go -d 'Navigate to worktree'
complete -f -c git -n '__fish_git_gtr_needs_command' -a run -d 'Execute command in worktree'
complete -f -c git -n '__fish_git_gtr_needs_command' -a rm -d 'Remove worktree(s)'
complete -f -c git -n '__fish_git_gtr_needs_command' -a mv -d 'Rename worktree and branch'
complete -f -c git -n '__fish_git_gtr_needs_command' -a rename -d 'Rename worktree and branch'
complete -f -c git -n '__fish_git_gtr_needs_command' -a copy -d 'Copy files between worktrees'
complete -f -c git -n '__fish_git_gtr_needs_command' -a editor -d 'Open worktree in editor'
complete -f -c git -n '__fish_git_gtr_needs_command' -a ai -d 'Start AI coding tool'
Expand Down Expand Up @@ -68,6 +70,12 @@ complete -c git -n '__fish_git_gtr_using_command rm' -l delete-branch -d 'Delete
complete -c git -n '__fish_git_gtr_using_command rm' -l force -d 'Force removal even if dirty'
complete -c git -n '__fish_git_gtr_using_command rm' -l yes -d 'Non-interactive mode'

# Rename command options
complete -c git -n '__fish_git_gtr_using_command mv' -l force -d 'Force move even if locked'
complete -c git -n '__fish_git_gtr_using_command mv' -l yes -d 'Skip confirmation'
complete -c git -n '__fish_git_gtr_using_command rename' -l force -d 'Force move even if locked'
complete -c git -n '__fish_git_gtr_using_command rename' -l yes -d 'Skip confirmation'

# Copy command options
complete -c git -n '__fish_git_gtr_using_command copy' -s n -l dry-run -d 'Preview without copying'
complete -c git -n '__fish_git_gtr_using_command copy' -s a -l all -d 'Copy to all worktrees'
Expand Down Expand Up @@ -134,3 +142,5 @@ complete -f -c git -n '__fish_git_gtr_using_command copy' -a '(__gtr_worktree_br
complete -f -c git -n '__fish_git_gtr_using_command editor' -a '(__gtr_worktree_branches)'
complete -f -c git -n '__fish_git_gtr_using_command ai' -a '(__gtr_worktree_branches)'
complete -f -c git -n '__fish_git_gtr_using_command rm' -a '(__gtr_worktree_branches)'
complete -f -c git -n '__fish_git_gtr_using_command mv' -a '(__gtr_worktree_branches)'
complete -f -c git -n '__fish_git_gtr_using_command rename' -a '(__gtr_worktree_branches)'
7 changes: 5 additions & 2 deletions completions/gtr.bash
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ _git_gtr() {

# If we're completing the first argument after 'git gtr'
if [ "$cword" -eq 2 ]; then
COMPREPLY=($(compgen -W "new go run copy editor ai rm ls list clean doctor adapter config completion help version" -- "$cur"))
COMPREPLY=($(compgen -W "new go run copy editor ai rm mv rename ls list clean doctor adapter config completion help version" -- "$cur"))
return 0
fi

local cmd="${words[2]}"

# Commands that take branch names or '1' for main repo
case "$cmd" in
go|run|editor|ai|rm)
go|run|editor|ai|rm|mv|rename)
if [ "$cword" -eq 3 ]; then
# Complete with branch names and special ID '1' for main repo
local branches all_options
Expand All @@ -42,6 +42,9 @@ _git_gtr() {
rm)
COMPREPLY=($(compgen -W "--delete-branch --force --yes" -- "$cur"))
;;
mv|rename)
COMPREPLY=($(compgen -W "--force --yes" -- "$cur"))
;;
esac
fi
;;
Expand Down
11 changes: 6 additions & 5 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,12 @@ git-worktree-runner/

### Git Versions

| Version | Support Level |
| ------------ | ------------------------------------- |
| Git 2.25+ | Recommended |
| Git 2.22+ | Full support |
| Git 2.5-2.21 | Basic support (some features limited) |
| Version | Support Level |
| ------------- | ------------------------------------- |
| Git 2.25+ | Recommended |
| Git 2.22+ | Full support |
| Git 2.17-2.21 | Basic support (some features limited) |
| Git < 2.17 | Not supported |

### Known Limitations

Expand Down