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 882cbd8..040e5de 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 bc355c6..3878798 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 77e75b3..0d1e3b3 100644 --- a/lib/commands/init.sh +++ b/lib/commands/init.sh @@ -136,6 +136,17 @@ _init_emit_posix_trust_helpers() { _init_emit_snapshot_helpers 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 @@ -158,7 +169,7 @@ __SHELL_PRELUDE__ _gtr_hook_defs="$(__FUNC___hooks_read_definitions "$_gtr_config_file")" || return 1 fi [ -n "$_gtr_hook_defs" ] || 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() { @@ -202,7 +213,7 @@ __SHELL_PRELUDE__ _gtr_hook_hash="$(__FUNC___hooks_current_content_hash "$_gtr_config_file" "$_gtr_definitions" 2>/dev/null)" || return 1 _gtr_repo_root="$(__FUNC___hooks_repo_root "$_gtr_config_file" 2>/dev/null)" || return 1 _gtr_repo_identity="$(__FUNC___hooks_repo_identity "$_gtr_repo_root" 2>/dev/null)" || return 1 - printf 'v3\n%s\n%s\n%s\n' "$_gtr_repo_root" "$_gtr_repo_identity" "$_gtr_hook_hash" | shasum -a 256 | cut -d' ' -f1 + printf 'v3\n%s\n%s\n%s\n' "$_gtr_repo_root" "$_gtr_repo_identity" "$_gtr_hook_hash" | __FUNC___sha256 } __FUNC___hooks_marker_matches_config() { @@ -239,6 +250,16 @@ _init_emit_fish_trust_helpers() { printf "function __FUNC___hooks_read_definitions\n command bash -c '%s\n_hooks_read_definitions \"\$1\"' bash \"\$argv[1]\"\nend\n" "$snapshot_helpers" printf "function __FUNC___hooks_definitions_get\n command bash -c '%s\n_hooks_definitions_get \"\$1\" \"\$2\"' bash \"\$argv[1]\" \"\$argv[2]\"\nend\n" "$snapshot_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 @@ -259,7 +280,7 @@ function __FUNC___hooks_current_content_hash set _gtr_hook_defs (__FUNC___hooks_read_definitions "$_gtr_config_file") end test -n "$_gtr_hook_defs"; or return 1 - printf '%s\n' "$_gtr_hook_defs" | shasum -a 256 | cut -d' ' -f1 + printf '%s\n' "$_gtr_hook_defs" | __FUNC___sha256 end function __FUNC___hooks_repo_root @@ -303,7 +324,7 @@ function __FUNC___hooks_current_trust_key set -l _gtr_repo_identity (__FUNC___hooks_repo_identity "$_gtr_repo_root" 2>/dev/null | string collect) test $pipestatus[1] -eq 0; or return 1 test -n "$_gtr_repo_identity"; or return 1 - printf 'v3\n%s\n%s\n%s\n' "$_gtr_repo_root" "$_gtr_repo_identity" "$_gtr_hook_hash" | shasum -a 256 | cut -d' ' -f1 + printf 'v3\n%s\n%s\n%s\n' "$_gtr_repo_root" "$_gtr_repo_identity" "$_gtr_hook_hash" | __FUNC___sha256 end function __FUNC___hooks_marker_matches_config diff --git a/lib/hooks.sh b/lib/hooks.sh index 128f33d..f346f63 100644 --- a/lib/hooks.sh +++ b/lib/hooks.sh @@ -57,12 +57,26 @@ _hooks_snapshot_is_trusted() { _hooks_marker_matches_config "$trust_path" "$config_file" } +# 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. @@ -131,7 +145,7 @@ _hooks_reviewed_trust_key() { if [ -z "$repo_identity" ]; then repo_identity=$(_hooks_repo_identity "$repo_root") || return 1 fi - printf 'v3\n%s\n%s\n%s\n' "$repo_root" "$repo_identity" "$hash" | shasum -a 256 | cut -d' ' -f1 + printf 'v3\n%s\n%s\n%s\n' "$repo_root" "$repo_identity" "$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 0eadb0a..129c0a6 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 afcffcf..0acbefc 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]" @@ -390,6 +396,33 @@ run_generated_replacement_repo_hooks() ( [[ "$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" "$(gtr_hooks_read_definitions "$2")"' \ + _ "$(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 ] @@ -474,6 +507,18 @@ run_generated_replacement_repo_hooks() ( [ "$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" { 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" +}