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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this

### Fixed

- `.gtrconfig` trust works on systems that have `sha256sum` but not `shasum`, such as Alpine and Fedora/RHEL with only `git-core`. Hashing falls back to `sha256sum`, so `git gtr trust` no longer fails and the generated `gtr cd` helpers recognize trusted hooks. The shell-integration cache schema is now `init=7`, so caches generated by older versions are refreshed; update the `init=6` check in your shell rc to `init=7`.
- The integration test helper now canonicalizes its temporary repository path. On macOS `mktemp -d` returns a `/var` path while git records the resolved `/private/var` one, so `cmd_clean --merged uses nested registered worktree path` compared two spellings of the same directory and failed locally while passing in CI.

## [2.11.1] - 2026-09-14
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,17 +272,17 @@ cd "$(git gtr go 1)" # Navigate to main repo
```bash
# Bash (add to ~/.bashrc)
_gtr_init="${XDG_CACHE_HOME:-$HOME/.cache}/gtr/init-gtr.bash"
[[ -f "$_gtr_init" ]] && head -n 1 "$_gtr_init" | grep -q ' init=6 ' || eval "$(git gtr init bash)" || true
[[ -f "$_gtr_init" ]] && head -n 1 "$_gtr_init" | grep -q ' init=7 ' || eval "$(git gtr init bash)" || true
source "$_gtr_init" 2>/dev/null || true; unset _gtr_init

# Zsh (add to ~/.zshrc)
_gtr_init="${XDG_CACHE_HOME:-$HOME/.cache}/gtr/init-gtr.zsh"
[[ -f "$_gtr_init" ]] && head -n 1 "$_gtr_init" | grep -q ' init=6 ' || eval "$(git gtr init zsh)" || true
[[ -f "$_gtr_init" ]] && head -n 1 "$_gtr_init" | grep -q ' init=7 ' || eval "$(git gtr init zsh)" || true
source "$_gtr_init" 2>/dev/null || true; unset _gtr_init

# Fish (add to ~/.config/fish/config.fish)
set -l _gtr_init (test -n "$XDG_CACHE_HOME" && echo $XDG_CACHE_HOME || echo $HOME/.cache)/gtr/init-gtr.fish
test -f "$_gtr_init"; and head -n 1 "$_gtr_init" | string match -q '* init=6 *'; or git gtr init fish >/dev/null 2>&1
test -f "$_gtr_init"; and head -n 1 "$_gtr_init" | string match -q '* init=7 *'; or git gtr init fish >/dev/null 2>&1
source "$_gtr_init" 2>/dev/null

# Then navigate with:
Expand Down
6 changes: 3 additions & 3 deletions lib/commands/help.sh
Original file line number Diff line number Diff line change
Expand Up @@ -427,17 +427,17 @@ Options:
Setup (sources cached output directly for fast startup):
# Bash (add to ~/.bashrc)
_gtr_init="${XDG_CACHE_HOME:-$HOME/.cache}/gtr/init-gtr.bash"
[[ -f "$_gtr_init" ]] && head -n 1 "$_gtr_init" | grep -q ' init=6 ' || eval "$(git gtr init bash)" || true
[[ -f "$_gtr_init" ]] && head -n 1 "$_gtr_init" | grep -q ' init=7 ' || eval "$(git gtr init bash)" || true
source "$_gtr_init" 2>/dev/null || true; unset _gtr_init

# Zsh (add to ~/.zshrc)
_gtr_init="${XDG_CACHE_HOME:-$HOME/.cache}/gtr/init-gtr.zsh"
[[ -f "$_gtr_init" ]] && head -n 1 "$_gtr_init" | grep -q ' init=6 ' || eval "$(git gtr init zsh)" || true
[[ -f "$_gtr_init" ]] && head -n 1 "$_gtr_init" | grep -q ' init=7 ' || eval "$(git gtr init zsh)" || true
source "$_gtr_init" 2>/dev/null || true; unset _gtr_init

# Fish (add to ~/.config/fish/config.fish)
set -l _gtr_init (test -n "$XDG_CACHE_HOME" && echo $XDG_CACHE_HOME || echo $HOME/.cache)/gtr/init-gtr.fish
test -f "$_gtr_init"; and head -n 1 "$_gtr_init" | string match -q '* init=6 *'; or git gtr init fish >/dev/null 2>&1
test -f "$_gtr_init"; and head -n 1 "$_gtr_init" | string match -q '* init=7 *'; or git gtr init fish >/dev/null 2>&1
source "$_gtr_init" 2>/dev/null

# Custom function name (avoids conflict with coreutils gtr)
Expand Down
31 changes: 26 additions & 5 deletions lib/commands/init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ cmd_init() {
# Generate output (cached to ~/.cache/gtr/, auto-invalidates on shell integration changes)
local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/gtr"
local cache_file="$cache_dir/init-${func_name}.${shell}"
local cache_schema="${GTR_INIT_CACHE_VERSION:-6}"
local cache_schema="${GTR_INIT_CACHE_VERSION:-7}"
local cache_stamp="# gtr-cache: version=${GTR_VERSION:-unknown} init=${cache_schema} func=$func_name shell=$shell"

# Return cached output if version matches
Expand All @@ -95,6 +95,17 @@ _init_emit_posix_trust_helpers() {
local template

template=$(cat <<'EOF'
__FUNC___sha256() {
__SHELL_PRELUDE__
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 | cut -d' ' -f1
elif command -v sha256sum >/dev/null 2>&1; then
sha256sum | cut -d' ' -f1
else
return 1
fi
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
__FUNC___gtrconfig_path() {
__SHELL_PRELUDE__
local _gtr_git_common_dir _gtr_repo_root
Expand All @@ -114,7 +125,7 @@ __SHELL_PRELUDE__
local _gtr_config_file="$1"
local _gtr_hook_defs
_gtr_hook_defs="$(git config -f "$_gtr_config_file" --get-regexp '^hooks\.|^defaults\.editor$|^defaults\.ai$' 2>/dev/null)" || return 1
printf '%s\n' "$_gtr_hook_defs" | shasum -a 256 | cut -d' ' -f1
printf '%s\n' "$_gtr_hook_defs" | __FUNC___sha256
}

__FUNC___hooks_repo_root() {
Expand All @@ -137,7 +148,7 @@ __SHELL_PRELUDE__
local _gtr_hook_hash _gtr_repo_root
_gtr_hook_hash="$(__FUNC___hooks_current_content_hash "$_gtr_config_file" 2>/dev/null)" || return 1
_gtr_repo_root="$(__FUNC___hooks_repo_root "$_gtr_config_file" 2>/dev/null)" || return 1
printf '%s\n%s\n' "$_gtr_repo_root" "$_gtr_hook_hash" | shasum -a 256 | cut -d' ' -f1
printf '%s\n%s\n' "$_gtr_repo_root" "$_gtr_hook_hash" | __FUNC___sha256
}

__FUNC___hooks_marker_matches_config() {
Expand Down Expand Up @@ -167,6 +178,16 @@ EOF

_init_emit_fish_trust_helpers() {
cat <<'FISH'
function __FUNC___sha256
if command -q shasum
shasum -a 256 | cut -d' ' -f1
else if command -q sha256sum
sha256sum | cut -d' ' -f1
else
return 1
end
end

function __FUNC___gtrconfig_path
set -l _gtr_git_common_dir (git rev-parse --git-common-dir 2>/dev/null)
test -n "$_gtr_git_common_dir"; or return 1
Expand All @@ -182,7 +203,7 @@ end

function __FUNC___hooks_current_content_hash
set -l _gtr_config_file "$argv[1]"
set -l _gtr_hook_defs (git config -f "$_gtr_config_file" --get-regexp '^hooks\.|^defaults\.editor$|^defaults\.ai$' 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
set -l _gtr_hook_defs (git config -f "$_gtr_config_file" --get-regexp '^hooks\.|^defaults\.editor$|^defaults\.ai$' 2>/dev/null | __FUNC___sha256)
test $pipestatus[1] -eq 0; or return 1
test -n "$_gtr_hook_defs"; or return 1
printf '%s\n' "$_gtr_hook_defs"
Expand All @@ -206,7 +227,7 @@ function __FUNC___hooks_current_trust_key
test -n "$_gtr_hook_hash"; or return 1
set -l _gtr_repo_root (__FUNC___hooks_repo_root "$_gtr_config_file" 2>/dev/null)
test -n "$_gtr_repo_root"; or return 1
printf '%s\n%s\n' "$_gtr_repo_root" "$_gtr_hook_hash" | shasum -a 256 | cut -d' ' -f1
printf '%s\n%s\n' "$_gtr_repo_root" "$_gtr_hook_hash" | __FUNC___sha256
end

function __FUNC___hooks_marker_matches_config
Expand Down
18 changes: 16 additions & 2 deletions lib/hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,26 @@ _hooks_read_definitions() {
printf '%s\n' "$hook_content"
}

# Print the SHA-256 hex digest of stdin.
# Falls back to sha256sum where Perl's shasum is not installed
# (for example Alpine or minimal Fedora/RHEL images).
# Usage: printf '%s' "$data" | _hooks_sha256
_hooks_sha256() {
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 | cut -d' ' -f1
elif command -v sha256sum >/dev/null 2>&1; then
sha256sum | cut -d' ' -f1
else
return 1
fi
}

# Compute a content hash for trusted command definitions.
# Usage: _hooks_content_hash <hook_content>
_hooks_content_hash() {
local hook_content="$1"
[ -n "$hook_content" ] || return 1
printf '%s\n' "$hook_content" | shasum -a 256 | cut -d' ' -f1
printf '%s\n' "$hook_content" | _hooks_sha256
}

# Compute a content hash of all current trusted command entries in a .gtrconfig file.
Expand Down Expand Up @@ -73,7 +87,7 @@ _hooks_reviewed_trust_key() {
local hash repo_root
hash=$(_hooks_content_hash "$hook_content") || return 1
repo_root=$(_hooks_repo_root "$config_file") || return 1
printf '%s\n%s\n' "$repo_root" "$hash" | shasum -a 256 | cut -d' ' -f1
printf '%s\n%s\n' "$repo_root" "$hash" | _hooks_sha256
}

# Compute the repo-scoped trust key for the current trusted command content
Expand Down
26 changes: 25 additions & 1 deletion tests/hooks.bats
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ setup() {
}

teardown() {
rm -rf "$XDG_CONFIG_HOME"
rm -rf "$XDG_CONFIG_HOME" "$BATS_TMPDIR/no-shasum-bin-$$"
teardown_integration_repo
}

Expand Down Expand Up @@ -69,6 +69,30 @@ EOF
[ "$(_hooks_file_hash "$TEST_REPO/.gtrconfig")" = "$expected" ]
}

@test "trust markers work when shasum is not installed" {
cat > "$TEST_REPO/.gtrconfig" <<'EOF'
[hooks]
postCd = echo hi
EOF

local expected
expected="$(_hooks_file_hash "$TEST_REPO/.gtrconfig")"

local no_shasum_path
no_shasum_path="$(path_without_command "$BATS_TMPDIR/no-shasum-bin-$$" shasum)"

(
PATH="$no_shasum_path"
if command -v shasum >/dev/null 2>&1; then
exit 1
fi

[ "$(_hooks_file_hash "$TEST_REPO/.gtrconfig")" = "$expected" ]
_hooks_mark_trusted "$TEST_REPO/.gtrconfig"
_hooks_are_trusted "$TEST_REPO/.gtrconfig"
)
}

@test "_hooks_mark_trusted creates a marker recognized by _hooks_are_trusted" {
cat > "$TEST_REPO/.gtrconfig" <<'EOF'
[hooks]
Expand Down
57 changes: 51 additions & 6 deletions tests/init.bats
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,15 @@ SCRIPT

run_generated_fish_gtrconfig_post_cd_hooks() {
local trust_state="$1"
local root repo wt xdg init_file
local without_shasum="${2:-0}"
local root repo wt xdg init_file fish_path

root=$(mktemp -d)
repo="$root/repo"
wt="$root/wt"
xdg="$root/xdg"
init_file="$root/gtr.fish"
fish_path="$PATH"

git init --quiet "$repo"
git -C "$repo" config user.name "Test User"
Expand All @@ -145,7 +147,11 @@ CFG
)
fi

XDG_CONFIG_HOME="$xdg" fish -c '
if [ "$without_shasum" -eq 1 ]; then
fish_path=$(path_without_command "$root/bin" shasum)
fi

PATH="$fish_path" XDG_CONFIG_HOME="$xdg" fish -c '
source "$argv[1]"
set -l wt "$argv[2]"
set -l root "$argv[3]"
Expand Down Expand Up @@ -336,6 +342,33 @@ require_runtime_shell() {
[[ "$output" == *"git gtr list --porcelain"* ]]
}

@test "generated bash wrapper recognizes trust markers when shasum is not installed" {
local root repo xdg no_shasum_path
root=$(mktemp -d)
repo="$root/repo"
xdg="$root/xdg"

git init --quiet "$repo"
cat > "$repo/.gtrconfig" <<'CFG'
[hooks]
postCd = echo hi
CFG

(
export XDG_CONFIG_HOME="$xdg"
# shellcheck disable=SC1091
. "$PROJECT_ROOT/lib/hooks.sh"
_hooks_mark_trusted "$repo/.gtrconfig"
)

no_shasum_path=$(path_without_command "$root/bin" shasum)
run env PATH="$no_shasum_path" bash -c 'eval "$1" && gtr_hooks_are_trusted "$2" "$3"' \
_ "$(cmd_init bash)" "$repo/.gtrconfig" "$xdg/gtr/trusted"
rm -rf "$root"

[ "$status" -eq 0 ]
}

@test "zsh output validates trust marker contents" {
run cmd_init zsh
[ "$status" -eq 0 ]
Expand Down Expand Up @@ -420,6 +453,18 @@ require_runtime_shell() {
[ "$hook_pwd_line" = "$wt_line" ]
}

@test "fish generated postCd trusts gtrconfig hooks when shasum is unavailable" {
require_runtime_shell fish
command -v sha256sum >/dev/null 2>&1 || skip "sha256sum is not installed"

run run_generated_fish_gtrconfig_post_cd_hooks trusted 1

[ "$status" -eq 0 ]
[[ "$output" == *"RC=0"* ]]
[[ "$output" == *"HOOK_ORDER=second"* ]]
[[ "$output" == *"STDERR="* ]]
}

# ── new/pr --cd wrapper support ─────────────────────────────────────────────

@test "bash output intercepts new/pr --cd and strips flag before delegating" {
Expand Down Expand Up @@ -952,27 +997,27 @@ require_runtime_shell() {

@test "cache invalidates old default shell integration schema" {
mkdir -p "$XDG_CACHE_HOME/gtr"
printf '%s\n%s\n' '# gtr-cache: version=test init=5 func=gtr shell=bash' old-cache > "$XDG_CACHE_HOME/gtr/init-gtr.bash"
printf '%s\n%s\n' '# gtr-cache: version=test init=6 func=gtr shell=bash' old-cache > "$XDG_CACHE_HOME/gtr/init-gtr.bash"

run cmd_init bash

[ "$status" -eq 0 ]
[ "$output" != "old-cache" ]
local stamp
stamp="$(head -1 "$XDG_CACHE_HOME/gtr/init-gtr.bash")"
[[ "$stamp" == *"init=6"* ]]
[[ "$stamp" == *"init=7"* ]]
}

@test "documented bash setup regenerates old schema cache" {
mkdir -p "$XDG_CACHE_HOME/gtr"
local _gtr_init="$XDG_CACHE_HOME/gtr/init-gtr.bash"
printf '%s\n%s\n' '# gtr-cache: version=test init=5 func=gtr shell=bash' old-cache > "$_gtr_init"
printf '%s\n%s\n' '# gtr-cache: version=test init=6 func=gtr shell=bash' old-cache > "$_gtr_init"

cmd_init bash >/dev/null

local stamp
stamp="$(head -1 "$_gtr_init")"
[[ "$stamp" == *"init=6"* ]]
[[ "$stamp" == *"init=7"* ]]
}

@test "cache uses --as func name in cache key" {
Expand Down
20 changes: 20 additions & 0 deletions tests/test_helper.bash
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,23 @@ SCRIPT
chmod +x "$MOCK_BIN_DIR/$name"
export PATH="$MOCK_BIN_DIR:$PATH"
}

# Mirror every command on PATH into a new directory, leaving out one command,
# so tests can simulate systems where that command is not installed.
# Usage: path_without_command <bin_dir> <command>
# Prints: the directory to use as PATH
path_without_command() {
local bin_dir="$1" omitted="$2" dir entry name
mkdir -p "$bin_dir"
local IFS=:
for dir in $PATH; do
[ -d "$dir" ] || continue
for entry in "$dir"/*; do
[ -e "$entry" ] || continue
name="${entry##*/}"
[ "$name" = "$omitted" ] && continue
[ -e "$bin_dir/$name" ] || ln -s "$entry" "$bin_dir/$name"
done
done
printf "%s" "$bin_dir"
}