Skip to content
Open
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,9 @@ git gtr config add gtr.copy.include "**/.env.example"
# Run setup after creating worktrees
git gtr config add gtr.hook.postCreate "npm install"

# Inherit sparse-checkout from the base worktree (default: on; --no-sparse to opt out)
git gtr config set gtr.sparse.inherit true

# Re-source environment after gtr cd or gtr new --cd (runs in current shell)
git gtr config add gtr.hook.postCd "source ./vars.sh"

Expand Down
6 changes: 4 additions & 2 deletions completions/_git-gtr
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ _git-gtr() {
'--no-copy[Skip file copying]' \
'--no-fetch[Skip git fetch]' \
'--no-hooks[Skip post-create hooks]' \
'--sparse[Inherit sparse-checkout from base worktree]' \
'--no-sparse[Force a full checkout]' \
'--force[Allow same branch in multiple worktrees]' \
'--name[Custom folder name suffix]:name:' \
'--folder[Custom folder name (replaces default)]:folder:' \
Expand Down Expand Up @@ -187,15 +189,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.defaultRemote 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.sparse.inherit 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.defaultRemote 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.sparse.inherit gtr.defaultBranch gtr.defaultRemote gtr.provider gtr.ui.color)'
;;
esac
fi
Expand Down
3 changes: 3 additions & 0 deletions completions/git-gtr.fish
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ complete -c git -n '__fish_git_gtr_using_command new' -l track -d 'Track mode' -
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'
complete -c git -n '__fish_git_gtr_using_command new' -l no-hooks -d 'Skip post-create hooks'
complete -c git -n '__fish_git_gtr_using_command new' -l sparse -d 'Inherit sparse-checkout from base worktree'
complete -c git -n '__fish_git_gtr_using_command new' -l no-sparse -d 'Force a full checkout'
complete -c git -n '__fish_git_gtr_using_command new' -l force -d 'Allow same branch in multiple worktrees'
complete -c git -n '__fish_git_gtr_using_command new' -l name -d 'Custom folder name suffix' -r
complete -c git -n '__fish_git_gtr_using_command new' -l folder -d 'Custom folder name (replaces default)' -r
Expand Down Expand Up @@ -143,6 +145,7 @@ complete -f -c git -n '__fish_git_gtr_using_command config' -a "
gtr.ai.default 'Default AI tool'
gtr.worktrees.dir 'Worktrees base directory'
gtr.worktrees.prefix 'Worktree folder prefix'
gtr.sparse.inherit 'gtr.sparse.inherit'
gtr.defaultBranch 'Default branch'
gtr.defaultRemote 'Default remote'
gtr.provider 'Hosting provider (github, gitlab)'
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 --remote --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 --sparse --no-sparse --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.defaultRemote 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.sparse.inherit 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.defaultRemote 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.sparse.inherit gtr.defaultBranch gtr.defaultRemote gtr.provider gtr.ui.color" -- "$cur"))
fi
;;
esac
Expand Down
29 changes: 29 additions & 0 deletions docs/advanced-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- [CI/CD Integration](#cicd-integration)
- [Multiple Worktrees Same Branch](#multiple-worktrees-same-branch)
- [Parallel AI Development](#parallel-ai-development)
- [Sparse-Checkout Inheritance](#sparse-checkout-inheritance)

---

Expand Down Expand Up @@ -202,4 +203,32 @@ git gtr ai feature-auth-tests -- --message "Write integration tests"

---

## Sparse-Checkout Inheritance

When working in a large monorepo, a base worktree often uses [sparse-checkout](https://git-scm.com/docs/git-sparse-checkout) to materialize only a slice of the tree. `gtr` carries that slice into the worktrees you branch off it, so feature worktrees stay lean instead of exploding into the full repo.

```bash
# my-app is a sparse worktree checking out only apps/my-app + packages.
# A feature branch off it inherits the same cone automatically:
git gtr new my-app-feature-xyz --from my-app

# The new worktree contains only the inherited sparse slice:
ls "$(git gtr go my-app-feature-xyz)"
git -C "$(git gtr go my-app-feature-xyz)" sparse-checkout list

# Opt out for a single command (full checkout):
git gtr new big-refactor --from my-app --no-sparse
```

**How it works:**

- gtr inspects the worktree holding the base ref (`--from`, falling back to the current worktree). If it has sparse-checkout enabled, the new worktree is created with `--no-checkout` and the same cone (or pattern set) is applied — the full tree is never written to disk.
- Controlled by `gtr.sparse.inherit` (default on). Use `--sparse` / `--no-sparse` to override per command.
- Full-checkout repositories are unaffected — they always get a full checkout.

> [!NOTE]
> Sparse-checkout is per-worktree, not per-branch. Inheriting "from `my-app`" copies the live sparse settings of the `my-app` worktree, not anything stored on the branch itself.

---

[Back to README](../README.md) | [Configuration](configuration.md) | [Troubleshooting](troubleshooting.md)
26 changes: 26 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,32 @@ echo "/.worktrees/" >> .gitignore

---

## Sparse-Checkout Settings

If the worktree you branch from uses [sparse-checkout](https://git-scm.com/docs/git-sparse-checkout) (e.g. a slice of a large monorepo), `gtr` can give the new worktree the same narrowed working tree instead of an expensive full checkout.

```bash
# Inherit sparse-checkout from the base worktree (default: true)
gtr.sparse.inherit = true
```

When enabled, `git gtr new` looks at the worktree holding the base ref (the `--from` target, falling back to the current worktree). If that worktree has sparse-checkout on, the new worktree is created with `--no-checkout` and the same cone (or pattern set) is applied — so the full tree is never materialized. Full-checkout repositories are unaffected.

Per-command overrides:

```bash
# Force inheritance even if gtr.sparse.inherit is off
git gtr new feature-xyz --from my-app --sparse

# Force a full checkout even if gtr.sparse.inherit is on
git gtr new feature-xyz --from my-app --no-sparse
```

> [!NOTE]
> Sparse-checkout is stored per-worktree, not per-branch. "Inherit from `my-app`" means inherit the live sparse settings of the `my-app` *worktree*.

---

## Provider Settings

The `clean --merged` and `clean --closed` commands auto-detect your hosting provider from the `origin` remote URL (`github.com` → GitHub, `gitlab.com` → GitLab). For self-hosted instances, set the provider explicitly:
Expand Down
42 changes: 41 additions & 1 deletion lib/commands/create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ cmd_create() {
--no-copy
--no-fetch
--no-hooks
--sparse
--no-sparse
--yes
--force
--name: value
Expand All @@ -110,6 +112,8 @@ cmd_create() {
local skip_copy="${_arg_no_copy:-0}"
local skip_fetch="${_arg_no_fetch:-0}"
local skip_hooks="${_arg_no_hooks:-0}"
local sparse_flag="${_arg_sparse:-0}"
local no_sparse_flag="${_arg_no_sparse:-0}"
local yes_mode="${_arg_yes:-0}"
local force="${_arg_force:-0}"
local custom_name="${_arg_name:-}"
Expand Down Expand Up @@ -156,6 +160,27 @@ cmd_create() {
# Determine from_ref with precedence: --from > --from-current > default
from_ref=$(_create_resolve_from_ref "$from_ref" "$from_current" "$repo_root" "$remote")

# Decide whether to inherit sparse-checkout from the base worktree.
# Precedence: --no-sparse > --sparse > gtr.sparse.inherit (default on).
local sparse_inherit=0
if [ "$no_sparse_flag" -eq 1 ]; then
sparse_inherit=0
elif [ "$sparse_flag" -eq 1 ]; then
sparse_inherit=1
elif cfg_bool gtr.sparse.inherit true; then
sparse_inherit=1
fi

local sparse_source="" no_checkout=0
if [ "$sparse_inherit" -eq 1 ]; then
sparse_source=$(_resolve_sparse_source "$from_ref")
if [ -n "$sparse_source" ]; then
no_checkout=1
elif [ "$sparse_flag" -eq 1 ]; then
log_warn "No sparse-checkout source found for '$from_ref' — creating a full checkout"
fi
fi

# Construct folder name for display
local folder_name
if [ -n "$folder_override" ]; then
Expand All @@ -172,10 +197,25 @@ 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" "$remote"); then
if ! worktree_path=$(create_worktree "$base_dir" "$prefix" "$branch_name" "$from_ref" "$track_mode" "$skip_fetch" "$force" "$custom_name" "$folder_override" "$remote" "$no_checkout"); then
exit 1
fi

# Inherit sparse-checkout before copying so copied files land in the narrowed tree.
# The worktree was created with --no-checkout, so a failed inheritance would leave
# it empty: fall back to a full checkout (and hard-fail if even that does not work)
# before the copy/hooks/success path continues.
if [ -n "$sparse_source" ]; then
if ! apply_inherited_sparse "$worktree_path" "$sparse_source"; then
log_warn "Sparse-checkout inheritance failed — falling back to a full checkout"
git -C "$worktree_path" sparse-checkout disable >/dev/null 2>&1 || true
if ! git -C "$worktree_path" checkout >/dev/null 2>&1; then
log_error "Could not populate worktree at $worktree_path"
exit 1
fi
fi
fi

# Copy files based on patterns
if [ "$skip_copy" -eq 0 ]; then
_post_create_copy "$repo_root" "$worktree_path"
Expand Down
2 changes: 2 additions & 0 deletions lib/commands/help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Options:
--no-copy Skip file copying (gtr.copy.include patterns)
--no-fetch Skip git fetch before creating
--no-hooks Skip post-create hooks
--sparse Inherit sparse-checkout from the base worktree
--no-sparse Force a full checkout (override gtr.sparse.inherit)
--force Allow same branch in multiple worktrees
(requires --name or --folder to distinguish them)
--name <suffix> Custom folder name suffix (appended after branch name)
Expand Down
1 change: 1 addition & 0 deletions lib/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ _CFG_KEY_MAP=(
"gtr.ai.default|defaults.ai"
"gtr.worktrees.dir|worktrees.dir"
"gtr.worktrees.prefix|worktrees.prefix"
"gtr.sparse.inherit|sparse.inherit"
"gtr.defaultBranch|defaults.branch"
"gtr.defaultRemote|defaults.remote"
"gtr.provider|defaults.provider"
Expand Down
Loading