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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,9 @@ Trust requires a known filesystem birth time for the Git directory. If `stat` ca

All configuration is stored via `git config`. For team settings, create a `.gtrconfig` file in your repository root.

The CLI uses the bare Git directory itself as the root for bare repositories; see
[configuration sources](docs/configuration.md#configuration-sources) for config and copy locations.

### Quick Setup

```bash
Expand Down
7 changes: 7 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ All configuration is stored via `git config`, making it easy to manage per-repos
5. Environment variables
6. Default values

For the CLI, a bare repository's root is the bare Git directory itself, including
when a command is run from one of its linked worktrees. `.gtrconfig`,
`.worktreeinclude`, relative worktree locations, and the default copy source use
that root, never its parent directory. The default worktree location is a sibling
named `<bare-directory-name>-worktrees`. Files stored only in Git history are not
copy sources; use `git gtr copy --from <worktree>` to copy from a checkout.

---

## Team Configuration (.gtrconfig)
Expand Down
4 changes: 4 additions & 0 deletions lib/commands/help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Each _help_<command>() provides detailed help for a single command.
# Dispatched by cmd_help when a command argument is provided.

# Print usage, options, examples, and repository-root semantics for git gtr new.
_help_new() {
cat <<'EOF'
git gtr new - Create a new worktree
Expand All @@ -14,6 +15,9 @@ Creates a new git worktree with the given branch name. The worktree folder
is named after the branch (slashes and special chars become hyphens, e.g.,
feature/user-auth becomes folder "feature-user-auth").

For bare repositories, configuration and copied files come from the bare Git
directory itself. The default worktree directory is its sibling <name>-worktrees.

Options:
--from <ref> Create from a specific ref (default: default branch)
--from-current Create from the current branch (for parallel variants)
Expand Down
21 changes: 20 additions & 1 deletion lib/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,32 @@

# Resolve the main repo root from the current git context.
# Works from the main repo root, a subdirectory, or a linked worktree.
# A bare repository is its own root; its parent is not a checkout.
# Returns: absolute path to main repo root or empty on failure
_resolve_main_repo_root() {
local git_common_dir repo_root
local git_common_dir repo_root is_bare
git_common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || return 1

[ -n "$git_common_dir" ] || return 1

# Query the common directory, not the current worktree: a linked worktree
# reports false even when its main repository is bare. Do not infer this
# from the directory name (a bare repository can itself be named .git).
# GIT_WORK_TREE and GIT_COMMON_DIR can force a false result. Discovery above
# already resolved the common directory; ignore these only for this probe.
is_bare=$(
unset GIT_WORK_TREE GIT_COMMON_DIR
git --git-dir="$git_common_dir" rev-parse --is-bare-repository 2>/dev/null
) || return 1
if [ "$is_bare" = "true" ]; then
repo_root=$(
unset CDPATH
cd -P -- "$git_common_dir" 2>/dev/null && pwd -P
) || return 1
printf "%s" "$repo_root"
return
fi

case "$git_common_dir" in
/*)
repo_root="${git_common_dir%/.git}"
Expand Down
210 changes: 210 additions & 0 deletions tests/bare_repositories.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
#!/usr/bin/env bats
# Repository-root and copy isolation for bare repositories and their worktrees.

load test_helper

setup() {
TEST_FIXTURE=$(cd -P "$(mktemp -d)" && pwd)
TEST_PARENT="$TEST_FIXTURE/parent space"
TEST_BARE="$TEST_PARENT/repository.git"
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
export XDG_CONFIG_HOME="$TEST_FIXTURE/user-config"
export XDG_CACHE_HOME="$TEST_FIXTURE/cache"
export GIT_TEMPLATE_DIR="$TEST_FIXTURE/template"
export GTR_DIR="$PROJECT_ROOT"
mkdir -p "$TEST_PARENT" "$GIT_TEMPLATE_DIR"
git init --quiet "$TEST_FIXTURE/seed"
git -C "$TEST_FIXTURE/seed" -c user.name=Test -c user.email=test@example.com \
commit --allow-empty -m init --quiet
git clone --bare --quiet "$TEST_FIXTURE/seed" "$TEST_BARE"
source_gtr_libs
cd "$TEST_BARE"
}

teardown() {
cd /
rm -rf "$TEST_FIXTURE"
}

@test "bare root and subdirectories resolve to the bare repository" {
local context
for context in "$TEST_BARE" "$TEST_BARE/objects"; do
cd "$context"
[ "$(_resolve_main_repo_root)" = "$TEST_BARE" ]
[ "$(_gtrconfig_path)" = "$TEST_BARE/.gtrconfig" ]
done
}

@test "relative and absolute GIT_DIR resolve to the same bare root" {
cd "$TEST_PARENT"
local git_dir
for git_dir in repository.git "$TEST_BARE"; do
[ "$(GIT_DIR="$git_dir" _resolve_main_repo_root)" = "$TEST_BARE" ]
done
}

@test "bare root probe ignores GIT_WORK_TREE without changing the caller environment" {
export GIT_DIR=. GIT_WORK_TREE=.
[ "$(git rev-parse --is-bare-repository)" = false ]

# Call directly so a global unset cannot be hidden by command substitution.
_resolve_main_repo_root > "$TEST_FIXTURE/resolved-root"

[ "$(cat "$TEST_FIXTURE/resolved-root")" = "$TEST_BARE" ]
[ "$(_gtrconfig_path)" = "$TEST_BARE/.gtrconfig" ]
[ "$GIT_DIR" = . ]
[ "$GIT_WORK_TREE" = . ]
[ "$(git rev-parse --show-toplevel)" = "$TEST_BARE" ]
}

@test "new with GIT_DIR and GIT_WORK_TREE uses bare config and copy source" {
local worktrees="$TEST_FIXTURE/worktrees"
git config gtr.worktrees.dir "$worktrees"
git config -f "$TEST_PARENT/.gtrconfig" copy.include secret.txt
git config -f "$TEST_BARE/.gtrconfig" copy.include own.txt
printf 'benign parent fixture\n' > "$TEST_PARENT/secret.txt"
printf 'bare fixture\n' > "$TEST_BARE/own.txt"

run env GIT_DIR=. GIT_WORK_TREE=. \
"$PROJECT_ROOT/bin/gtr" new env-isolated --from HEAD --no-fetch --porcelain

[ "$status" -eq 0 ]
[ -f "$worktrees/env-isolated/.git" ]
[ ! -e "$worktrees/env-isolated/secret.txt" ]
[ "$(cat "$worktrees/env-isolated/own.txt")" = "bare fixture" ]
}

@test "bare root probe ignores GIT_COMMON_DIR without changing the caller environment" {
export GIT_DIR=. GIT_COMMON_DIR=. GIT_WORK_TREE=.
[ "$(git --git-dir=. rev-parse --is-bare-repository)" = false ]

# The initial discovery must honor GIT_COMMON_DIR; only the probe ignores it.
_resolve_main_repo_root > "$TEST_FIXTURE/resolved-root"

[ "$(cat "$TEST_FIXTURE/resolved-root")" = "$TEST_BARE" ]
[ "$(_gtrconfig_path)" = "$TEST_BARE/.gtrconfig" ]
[ "$GIT_DIR" = . ]
[ "$GIT_COMMON_DIR" = . ]
[ "$GIT_WORK_TREE" = . ]
[ "$(git rev-parse --git-common-dir)" = . ]
}

@test "new with GIT_DIR and GIT_COMMON_DIR uses bare config and copy source" {
local worktrees="$TEST_FIXTURE/worktrees"
# A relative GIT_COMMON_DIR breaks Git's own checkout in the new worktree.
# Use an absolute override and a .git name to expose the parent-root fallback.
TEST_BARE="$TEST_PARENT/.git"
git clone --bare --quiet "$TEST_FIXTURE/seed" "$TEST_BARE"
cd "$TEST_BARE"
git config gtr.worktrees.dir "$worktrees"
git config -f "$TEST_PARENT/.gtrconfig" copy.include secret.txt
git config -f "$TEST_BARE/.gtrconfig" copy.include own.txt
printf 'benign parent fixture\n' > "$TEST_PARENT/secret.txt"
printf 'bare fixture\n' > "$TEST_BARE/own.txt"

run env GIT_DIR=. GIT_COMMON_DIR="$TEST_BARE" \
"$PROJECT_ROOT/bin/gtr" new common-isolated --from HEAD --no-fetch --porcelain

[ "$status" -eq 0 ]
[ -f "$worktrees/common-isolated/.git" ]
[ ! -e "$worktrees/common-isolated/secret.txt" ]
[ "$(cat "$worktrees/common-isolated/own.txt")" = "bare fixture" ]
}

@test "bare repositories named .git keep their own root from linked worktrees" {
local bare="$TEST_PARENT/.git" linked="$TEST_FIXTURE/linked"
git clone --bare --quiet "$TEST_FIXTURE/seed" "$bare"
git -C "$bare" worktree add --quiet -b linked "$linked" HEAD
mkdir -p "$linked/subdir"

local context
for context in "$bare" "$bare/objects" "$linked" "$linked/subdir"; do
cd "$context"
[ "$(_resolve_main_repo_root)" = "$bare" ]
[ "$(_gtrconfig_path)" = "$bare/.gtrconfig" ]
done
}

@test "bare root determines default and relative worktree locations" {
[ "$(resolve_base_dir "$(_resolve_main_repo_root)")" = "${TEST_BARE}-worktrees" ]
git config gtr.worktrees.dir local-worktrees
[ "$(resolve_base_dir "$(_resolve_main_repo_root)" 2>/dev/null)" = "$TEST_BARE/local-worktrees" ]
}

@test "new from bare repo ignores parent config and copy patterns" {
local worktrees="$TEST_FIXTURE/worktrees"
git config gtr.worktrees.dir "$worktrees"
printf 'benign parent fixture\n' > "$TEST_PARENT/secret.txt"
printf 'parent include fixture\n' > "$TEST_PARENT/parent-include.txt"
mkdir "$TEST_PARENT/parent-assets"
printf 'parent asset fixture\n' > "$TEST_PARENT/parent-assets/asset.txt"
git config -f "$TEST_PARENT/.gtrconfig" copy.include secret.txt
git config -f "$TEST_PARENT/.gtrconfig" copy.includeDirs parent-assets
git config -f "$TEST_PARENT/.gtrconfig" hooks.postCreate 'touch parent-hook-ran'
printf 'parent-include.txt\n' > "$TEST_PARENT/.worktreeinclude"

run "$PROJECT_ROOT/bin/gtr" new isolated --from HEAD --no-fetch --porcelain

[ "$status" -eq 0 ]
[ -f "$worktrees/isolated/.git" ]
[ ! -e "$worktrees/isolated/secret.txt" ]
[ ! -e "$worktrees/isolated/parent-include.txt" ]
[ ! -e "$worktrees/isolated/parent-assets" ]
[ ! -e "$worktrees/isolated/parent-hook-ran" ]
[[ "$output" == *$'hook_status\tnone'* ]]
}

@test "bare and linked worktree creation use the bare root config and copy source" {
printf 'bare fixture\n' > "$TEST_BARE/own.txt"
printf 'bare include fixture\n' > "$TEST_BARE/own-include.txt"
mkdir "$TEST_BARE/own-assets"
printf 'bare asset fixture\n' > "$TEST_BARE/own-assets/asset.txt"
git config -f "$TEST_BARE/.gtrconfig" copy.include own.txt
git config -f "$TEST_BARE/.gtrconfig" copy.includeDirs own-assets
printf 'own-include.txt\n' > "$TEST_BARE/.worktreeinclude"

run "$PROJECT_ROOT/bin/gtr" new first --from HEAD --no-fetch --porcelain
[ "$status" -eq 0 ]
[ "$(cat "${TEST_BARE}-worktrees/first/own.txt")" = "bare fixture" ]
[ -f "${TEST_BARE}-worktrees/first/own-include.txt" ]
[ -f "${TEST_BARE}-worktrees/first/own-assets/asset.txt" ]

mkdir "${TEST_BARE}-worktrees/first/subdir"
cd "${TEST_BARE}-worktrees/first/subdir"
# A linked worktree reports --is-bare-repository=false, even for a bare common dir.
[ "$(git rev-parse --is-bare-repository)" = false ]
[ "$(_resolve_main_repo_root)" = "$TEST_BARE" ]
printf 'linked fixture\n' > ../own.txt
git config -f ../.gtrconfig copy.include should-not-copy.txt
run "$PROJECT_ROOT/bin/gtr" new second --from HEAD --no-fetch --porcelain
[ "$status" -eq 0 ]
[ "$(cat "${TEST_BARE}-worktrees/second/own.txt")" = "bare fixture" ]
[ -f "${TEST_BARE}-worktrees/second/own-include.txt" ]
[ -f "${TEST_BARE}-worktrees/second/own-assets/asset.txt" ]
}

@test "normal repo and linked worktrees retain main config and copy source" {
local normal="$TEST_PARENT/normal" linked="$TEST_FIXTURE/linked"
git clone --quiet "$TEST_FIXTURE/seed" "$normal"
git -C "$normal" worktree add --quiet -b linked "$linked" HEAD
mkdir -p "$normal/subdir" "$linked/subdir"
git config -f "$TEST_PARENT/.gtrconfig" copy.include parent.txt
printf 'parent fixture\n' > "$TEST_PARENT/parent.txt"
git config -f "$normal/.gtrconfig" copy.include main.txt
printf 'main fixture\n' > "$normal/main.txt"
git config -f "$linked/.gtrconfig" copy.include linked.txt
printf 'linked fixture\n' > "$linked/linked.txt"

local context branch=0
for context in "$normal" "$normal/subdir" "$linked" "$linked/subdir"; do
cd "$context"
[ "$(_resolve_main_repo_root)" = "$normal" ]
[ "$(_gtrconfig_path)" = "$normal/.gtrconfig" ]
branch=$((branch + 1))
run "$PROJECT_ROOT/bin/gtr" new "control-$branch" --from HEAD --no-fetch --porcelain
[ "$status" -eq 0 ]
[ "$(cat "${normal}-worktrees/control-$branch/main.txt")" = "main fixture" ]
[ ! -e "${normal}-worktrees/control-$branch/parent.txt" ]
[ ! -e "${normal}-worktrees/control-$branch/linked.txt" ]
done
}
Loading