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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ git gtr new my-feature --name descriptive-variant

- `--from <ref>`: Create from specific ref
- `--from-current`: Create from current branch (useful for parallel variant work)
- `--remote <name>`: Remote used for default base refs
- `--track <mode>`: Tracking mode (auto|remote|local|none)
- `--no-copy`: Skip file copying
- `--no-fetch`: Skip git fetch
Expand Down Expand Up @@ -388,6 +389,7 @@ git gtr config set gtr.ui.color never
[defaults]
editor = cursor
ai = claude
remote = upstream
```

**Configuration precedence** (highest to lowest):
Expand Down
5 changes: 3 additions & 2 deletions completions/_git-gtr
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ _git-gtr() {
'1:branch name:' \
'--from[Base ref]:ref:' \
'--from-current[Create from current branch]' \
'--remote[Remote used for default base refs]:remote:' \
'--track[Track mode]:mode:(auto remote local none)' \
'--no-copy[Skip file copying]' \
'--no-fetch[Skip git fetch]' \
Expand Down Expand Up @@ -183,15 +184,15 @@ _git-gtr() {
'--local[Use local git config]' \
'--global[Use global git config]' \
'--system[Use system git config]' \
'*:config key:(gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.provider gtr.ui.color)'
'*:config key:(gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.defaultRemote gtr.provider gtr.ui.color)'
;;
set|add|unset)
# Write operations only support --local and --global
# (--system may require root or appropriate file permissions)
_arguments \
'--local[Use local git config]' \
'--global[Use global git config]' \
'*:config key:(gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.provider gtr.ui.color)'
'*:config key:(gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.defaultRemote gtr.provider gtr.ui.color)'
;;
esac
fi
Expand Down
2 changes: 2 additions & 0 deletions completions/git-gtr.fish
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ complete -f -c git -n '__fish_git_gtr_needs_command' -a help -d 'Show help'
# New command options
complete -c git -n '__fish_git_gtr_using_command new' -l from -d 'Base ref' -r
complete -c git -n '__fish_git_gtr_using_command new' -l from-current -d 'Create from current branch'
complete -c git -n '__fish_git_gtr_using_command new' -l remote -d 'Remote used for default base refs' -r
complete -c git -n '__fish_git_gtr_using_command new' -l track -d 'Track mode' -r -a 'auto remote local none'
complete -c git -n '__fish_git_gtr_using_command new' -l no-copy -d 'Skip file copying'
complete -c git -n '__fish_git_gtr_using_command new' -l no-fetch -d 'Skip git fetch'
Expand Down Expand Up @@ -140,6 +141,7 @@ complete -f -c git -n '__fish_git_gtr_using_command config' -a "
gtr.worktrees.dir 'Worktrees base directory'
gtr.worktrees.prefix 'Worktree folder prefix'
gtr.defaultBranch 'Default branch'
gtr.defaultRemote 'Default remote'
gtr.provider 'Hosting provider (github, gitlab)'
gtr.ui.color 'Color output mode (auto, always, never)'
"
Expand Down
6 changes: 3 additions & 3 deletions completions/gtr.bash
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ _git_gtr() {
new)
# Complete flags
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "--from --from-current --track --no-copy --no-fetch --no-hooks --force --name --folder --yes --editor -e --ai -a" -- "$cur"))
COMPREPLY=($(compgen -W "--from --from-current --remote --track --no-copy --no-fetch --no-hooks --force --name --folder --yes --editor -e --ai -a" -- "$cur"))
elif [ "$prev" = "--track" ]; then
COMPREPLY=($(compgen -W "auto remote local none" -- "$cur"))
fi
Expand Down Expand Up @@ -138,15 +138,15 @@ _git_gtr() {
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "--local --global --system" -- "$cur"))
else
COMPREPLY=($(compgen -W "gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.provider gtr.ui.color" -- "$cur"))
COMPREPLY=($(compgen -W "gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.defaultRemote gtr.provider gtr.ui.color" -- "$cur"))
fi
;;
set|add|unset)
# Write operations only support --local and --global (--system requires root)
if [[ "$cur" == -* ]]; then
COMPREPLY=($(compgen -W "--local --global" -- "$cur"))
else
COMPREPLY=($(compgen -W "gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.provider gtr.ui.color" -- "$cur"))
COMPREPLY=($(compgen -W "gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.defaultRemote gtr.provider gtr.ui.color" -- "$cur"))
fi
;;
esac
Expand Down
6 changes: 6 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Create a `.gtrconfig` file in your repository root to share configuration across
[defaults]
editor = cursor
ai = claude
remote = upstream
```

> [!TIP]
Expand Down Expand Up @@ -92,6 +93,9 @@ gtr.worktrees.prefix = dev-

# Default branch (default: auto-detect)
gtr.defaultBranch = main

# Default remote for fetches, tracking, and default base refs (default: origin)
gtr.defaultRemote = upstream
```

> [!IMPORTANT]
Expand Down Expand Up @@ -440,6 +444,7 @@ git gtr config add gtr.hook.postCreate "pnpm run build"
# Set global preferences
git gtr config set gtr.editor.default cursor --global
git gtr config set gtr.ai.default claude --global
git gtr config set gtr.defaultRemote upstream --global
```

---
Expand All @@ -454,6 +459,7 @@ git gtr config set gtr.ai.default claude --global
| `GTR_EDITOR_CMD_NAME` | First word of `GTR_EDITOR_CMD` for availability checks | None |
| `GTR_AI_CMD` | Custom AI tool command (e.g., `copilot`) | None |
| `GTR_AI_CMD_NAME` | First word of `GTR_AI_CMD` for availability checks | None |
| `GTR_DEFAULT_REMOTE` | Remote used for default base refs and tracking | `origin` |
| `GTR_COLOR` | Override color output (`always`, `never`, `auto`) | `auto` |
| `GTR_PROVIDER` | Override hosting provider (`github` or `gitlab`) | Auto-detected from URL |
| `NO_COLOR` | Disable color output when set ([no-color.org](https://no-color.org)) | Unset |
Expand Down
16 changes: 9 additions & 7 deletions lib/commands/create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,22 @@ _post_create_next_steps() {
}

# Determine the base ref for worktree creation
# Usage: _create_resolve_from_ref <from_ref> <from_current> <repo_root>
# Usage: _create_resolve_from_ref <from_ref> <from_current> <repo_root> [remote]
# Prints: resolved ref
_create_resolve_from_ref() {
local from_ref="$1" from_current="$2" repo_root="$3"
local from_ref="$1" from_current="$2" repo_root="$3" remote="${4:-$(resolve_default_remote)}"

if [ -z "$from_ref" ]; then
if [ "$from_current" -eq 1 ]; then
from_ref=$(get_current_branch)
if [ -z "$from_ref" ] || [ "$from_ref" = "HEAD" ]; then
log_warn "Currently in detached HEAD state - falling back to default branch"
from_ref="origin/$(resolve_default_branch "$repo_root")"
from_ref="$remote/$(resolve_default_branch "$repo_root" "$remote")"
else
log_info "Creating from current branch: $from_ref"
fi
else
from_ref="origin/$(resolve_default_branch "$repo_root")"
from_ref="$remote/$(resolve_default_branch "$repo_root" "$remote")"
fi
fi

Expand All @@ -89,6 +89,7 @@ cmd_create() {
local _spec
_spec="--from: value
--from-current
--remote: value
--track: value
--no-copy
--no-fetch
Expand All @@ -104,6 +105,7 @@ cmd_create() {
local branch_name="${_pa_positional[0]:-}"
local from_ref="${_arg_from:-}"
local from_current="${_arg_from_current:-0}"
local remote="${_arg_remote:-$(resolve_default_remote)}"
local track_mode="${_arg_track:-auto}"
local skip_copy="${_arg_no_copy:-0}"
local skip_fetch="${_arg_no_fetch:-0}"
Expand Down Expand Up @@ -152,7 +154,7 @@ cmd_create() {
fi

# Determine from_ref with precedence: --from > --from-current > default
from_ref=$(_create_resolve_from_ref "$from_ref" "$from_current" "$repo_root")
from_ref=$(_create_resolve_from_ref "$from_ref" "$from_current" "$repo_root" "$remote")

# Construct folder name for display
local folder_name
Expand All @@ -170,7 +172,7 @@ cmd_create() {

# Create the worktree
local worktree_path
if ! worktree_path=$(create_worktree "$base_dir" "$prefix" "$branch_name" "$from_ref" "$track_mode" "$skip_fetch" "$force" "$custom_name" "$folder_override"); then
if ! worktree_path=$(create_worktree "$base_dir" "$prefix" "$branch_name" "$from_ref" "$track_mode" "$skip_fetch" "$force" "$custom_name" "$folder_override" "$remote"); then
exit 1
fi

Expand All @@ -196,4 +198,4 @@ cmd_create() {
if [ "$open_editor" -eq 0 ] && [ "$start_ai" -eq 0 ]; then
_post_create_next_steps "$branch_name" "$folder_name" "$folder_override" "$repo_root" "$base_dir" "$prefix"
fi
}
}
3 changes: 3 additions & 0 deletions lib/commands/help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ feature/user-auth becomes folder "feature-user-auth").
Options:
--from <ref> Create from a specific ref (default: default branch)
--from-current Create from the current branch (for parallel variants)
--remote <name> Remote used for default base refs (default: gtr.defaultRemote)
--track <mode> Branch tracking mode: auto|remote|local|none (default: auto)
auto: tries remote first, then local, then creates new
--no-copy Skip file copying (gtr.copy.include patterns)
Expand Down Expand Up @@ -473,6 +474,7 @@ CORE COMMANDS (daily workflow):
Create a new worktree (folder named after branch)
--from <ref>: create from specific ref
--from-current: create from current branch (for parallel variants)
--remote <name>: remote used for default base refs
--track <mode>: tracking mode (auto|remote|local|none)
--no-copy: skip file copying
--no-fetch: skip git fetch
Expand Down Expand Up @@ -636,6 +638,7 @@ CONFIGURATION OPTIONS:
gtr.worktrees.dir Worktrees base directory
gtr.worktrees.prefix Worktree folder prefix (default: "")
gtr.defaultBranch Default branch (default: auto)
gtr.defaultRemote Default remote (default: origin)
gtr.editor.default Default editor
Options: antigravity, atom, cursor, emacs,
idea, nano, nvim, pycharm, sublime, vim,
Expand Down
1 change: 1 addition & 0 deletions lib/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ _CFG_KEY_MAP=(
"gtr.worktrees.dir|worktrees.dir"
"gtr.worktrees.prefix|worktrees.prefix"
"gtr.defaultBranch|defaults.branch"
"gtr.defaultRemote|defaults.remote"
"gtr.provider|defaults.provider"
"gtr.ui.color|ui.color"
)
Expand Down
63 changes: 37 additions & 26 deletions lib/core.sh
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,17 @@ resolve_base_dir() {
printf "%s" "$base_dir"
}

# Resolve the default remote name
# Usage: resolve_default_remote
resolve_default_remote() {
cfg_default "gtr.defaultRemote" "GTR_DEFAULT_REMOTE" "origin"
}

# Resolve the default branch name
# Usage: resolve_default_branch [repo_root]
# Usage: resolve_default_branch [repo_root] [remote]
resolve_default_branch() {
local repo_root="${1:-$(pwd)}"
local remote="${2:-$(resolve_default_remote)}"
local default_branch
local configured_branch

Expand All @@ -130,18 +137,19 @@ resolve_default_branch() {
return 0
fi

# Auto-detect from origin/HEAD
default_branch=$(git symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||')
# Auto-detect from the selected remote's HEAD
default_branch=$(git symbolic-ref --quiet "refs/remotes/$remote/HEAD" 2>/dev/null || true)
default_branch="${default_branch#refs/remotes/"$remote"/}"

Comment thread
coderabbitai[bot] marked this conversation as resolved.
if [ -n "$default_branch" ]; then
printf "%s" "$default_branch"
return 0
fi

# Fallback: try common branch names
if git show-ref --verify --quiet "refs/remotes/origin/main"; then
if git show-ref --verify --quiet "refs/remotes/$remote/main"; then
printf "main"
elif git show-ref --verify --quiet "refs/remotes/origin/master"; then
elif git show-ref --verify --quiet "refs/remotes/$remote/master"; then
printf "master"
else
# Last resort: just use 'main'
Expand Down Expand Up @@ -370,34 +378,36 @@ _resolve_folder_name() {

# Check if a branch exists on remote and/or locally.
# Sets globals: _wt_remote_exists, _wt_local_exists (0 or 1)
# Usage: _check_branch_refs <branch_name>
# Usage: _check_branch_refs <branch_name> [remote]
declare _wt_remote_exists _wt_local_exists
_check_branch_refs() {
local branch_name="$1" remote="${2:-$(resolve_default_remote)}"

_wt_remote_exists=0
_wt_local_exists=0
git show-ref --verify --quiet "refs/remotes/origin/$1" && _wt_remote_exists=1
git show-ref --verify --quiet "refs/heads/$1" && _wt_local_exists=1
git show-ref --verify --quiet "refs/remotes/$remote/$branch_name" && _wt_remote_exists=1
git show-ref --verify --quiet "refs/heads/$branch_name" && _wt_local_exists=1
return 0
}

# Auto-track: create local tracking branch from remote if needed, then add worktree.
# Usage: _worktree_add_tracked <worktree_path> <branch_name> [force_args...]
# Usage: _worktree_add_tracked <worktree_path> <branch_name> [remote] [force_args...]
# shellcheck disable=SC2317 # Called indirectly from create_worktree
_worktree_add_tracked() {
local wt_path="$1" branch_name="$2"
shift 2
local wt_path="$1" branch_name="$2" remote="${3:-$(resolve_default_remote)}"
shift 3

log_step "Branch '$branch_name' exists on remote"
if git branch --track "$branch_name" "origin/$branch_name" >/dev/null 2>&1; then
log_info "Created local branch tracking origin/$branch_name"
log_step "Branch '$branch_name' exists on $remote"
if git branch --track "$branch_name" "$remote/$branch_name" >/dev/null 2>&1; then
log_info "Created local branch tracking $remote/$branch_name"
fi
_try_worktree_add "$wt_path" "" \
"Worktree created tracking origin/$branch_name" \
"Worktree created tracking $remote/$branch_name" \
"$@" "$branch_name"
}

# Create a new git worktree
# Usage: create_worktree base_dir prefix branch_name from_ref track_mode [skip_fetch] [force] [custom_name] [folder_override]
# Usage: create_worktree base_dir prefix branch_name from_ref track_mode [skip_fetch] [force] [custom_name] [folder_override] [remote]
# track_mode: auto, remote, local, or none
# skip_fetch: 0 (default, fetch) or 1 (skip)
# force: 0 (default, check branch) or 1 (allow same branch in multiple worktrees)
Expand All @@ -407,6 +417,7 @@ create_worktree() {
local base_dir="$1" prefix="$2" branch_name="$3" from_ref="$4"
local track_mode="${5:-auto}" skip_fetch="${6:-0}" force="${7:-0}"
local custom_name="${8:-}" folder_override="${9:-}"
local remote="${10:-$(resolve_default_remote)}"

local sanitized_name
sanitized_name=$(_resolve_folder_name "$branch_name" "$custom_name" "$folder_override") || return 1
Expand All @@ -424,31 +435,31 @@ create_worktree() {

if [ "$skip_fetch" -eq 0 ]; then
log_step "Fetching remote branches..."
git fetch origin 2>/dev/null || log_warn "Could not fetch from origin"
git fetch "$remote" 2>/dev/null || log_warn "Could not fetch from $remote"
fi

_check_branch_refs "$branch_name"
_check_branch_refs "$branch_name" "$remote"

# Resolve from_ref to a commit SHA to prevent git's guess-remote logic
# from overriding the -b flag when from_ref matches a remote branch name.
# Try the ref as-is first, then with origin/ prefix for remote-only refs.
# Try the ref as-is first, then with the selected remote prefix for remote-only refs.
local resolved_ref
resolved_ref=$(git rev-parse --verify "${from_ref}^{commit}" 2>/dev/null) \
|| resolved_ref=$(git rev-parse --verify "origin/${from_ref}^{commit}" 2>/dev/null) \
|| resolved_ref=$(git rev-parse --verify "$remote/${from_ref}^{commit}" 2>/dev/null) \
|| resolved_ref="$from_ref"

case "$track_mode" in
remote)
if [ "$_wt_remote_exists" -eq 1 ]; then
_try_worktree_add "$worktree_path" \
"Creating worktree from remote branch origin/$branch_name" \
"Worktree created tracking origin/$branch_name" \
"${force_args[@]}" -b "$branch_name" "origin/$branch_name" && return 0
"Creating worktree from remote branch $remote/$branch_name" \
"Worktree created tracking $remote/$branch_name" \
"${force_args[@]}" -b "$branch_name" "$remote/$branch_name" && return 0
_try_worktree_add "$worktree_path" "" \
"Worktree created tracking origin/$branch_name" \
"Worktree created tracking $remote/$branch_name" \
"${force_args[@]}" "$branch_name" && return 0
fi
log_error "Remote branch origin/$branch_name does not exist"
log_error "Remote branch $remote/$branch_name does not exist"
return 1
;;

Expand All @@ -474,7 +485,7 @@ create_worktree() {

auto|*)
if [ "$_wt_remote_exists" -eq 1 ] && [ "$_wt_local_exists" -eq 0 ]; then
_worktree_add_tracked "$worktree_path" "$branch_name" "${force_args[@]}" && return 0
_worktree_add_tracked "$worktree_path" "$branch_name" "$remote" "${force_args[@]}" && return 0
elif [ "$_wt_local_exists" -eq 1 ]; then
_try_worktree_add "$worktree_path" \
"Using existing local branch $branch_name" \
Expand Down
Loading
Loading