diff --git a/CHANGELOG.md b/CHANGELOG.md index b90fd6b..4e55da8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index c7b88f8..77f6019 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/lib/commands/help.sh b/lib/commands/help.sh index ed0bab1..34f4f24 100644 --- a/lib/commands/help.sh +++ b/lib/commands/help.sh @@ -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) diff --git a/lib/commands/init.sh b/lib/commands/init.sh index e526744..1db18ec 100644 --- a/lib/commands/init.sh +++ b/lib/commands/init.sh @@ -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 @@ -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 +} + __FUNC___gtrconfig_path() { __SHELL_PRELUDE__ local _gtr_git_common_dir _gtr_repo_root @@ -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() { @@ -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() { @@ -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 @@ -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" @@ -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 diff --git a/lib/hooks.sh b/lib/hooks.sh index 4622a1c..af7aa54 100644 --- a/lib/hooks.sh +++ b/lib/hooks.sh @@ -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 _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. @@ -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 diff --git a/tests/hooks.bats b/tests/hooks.bats index a94543e..58fdcc0 100644 --- a/tests/hooks.bats +++ b/tests/hooks.bats @@ -10,7 +10,7 @@ setup() { } teardown() { - rm -rf "$XDG_CONFIG_HOME" + rm -rf "$XDG_CONFIG_HOME" "$BATS_TMPDIR/no-shasum-bin-$$" teardown_integration_repo } @@ -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] diff --git a/tests/init.bats b/tests/init.bats index 2d759fa..63b0efd 100644 --- a/tests/init.bats +++ b/tests/init.bats @@ -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" @@ -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]" @@ -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 ] @@ -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" { @@ -952,7 +997,7 @@ 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 @@ -960,19 +1005,19 @@ require_runtime_shell() { [ "$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" { diff --git a/tests/test_helper.bash b/tests/test_helper.bash index 6e4ada1..84c61db 100644 --- a/tests/test_helper.bash +++ b/tests/test_helper.bash @@ -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 +# 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" +}