Merge branch 'hermetic-env'
commit 00e17472b7d6c044dc3c3f8f4a6344361d953894 · parent 46343bf21d · parent 040980822a
· browse this tree
warpfork, 2026-10-21 16:12:50 UTC
diff --git a/bin/patchette b/bin/patchette index 5355755..d17e45b 100755 --- a/bin/patchette +++ b/bin/patchette @@ -22,6 +22,11 @@ set -euo pipefail # Hermetic git: no user or system config, no system attributes, fixed locale. # Deliberately NO author/committer identities or dates: patchette never authors # commits; replayed commits carry their own bytes. +# Nor any git env inherited from a caller (hooks, aliases, `git -c`): +# git's own list of repo-local vars covers redirecting which repo we read and +# write, injected config, and grafts; the other two would perturb export bytes. +git_local_env=$(git rev-parse --local-env-vars) +unset $git_local_env GIT_ATTR_SOURCE GIT_DIFF_OPTS export GIT_CONFIG_GLOBAL=/dev/null export GIT_CONFIG_SYSTEM=/dev/null export GIT_CONFIG_NOSYSTEM=1 diff --git a/tests/test-hermetic.sh b/tests/test-hermetic.sh index f902f07..51835b5 100755 --- a/tests/test-hermetic.sh +++ b/tests/test-hermetic.sh @@ -1,9 +1,12 @@ #!/bin/bash ## Hermeticity: hostile repo-local git config and attributes, in the subject -## or the target repo, must not change what patchette writes or replays. +## or the target repo, or hostile git env inherited from a caller, +## must not change what patchette writes or replays. ## ## Nulling global and system config doesn't reach a repo's own .git/config, ## its info/attributes, or a worktree .gitattributes; git reads all of those. +## Nor does it reach env a parent git hands down (hooks, aliases, `git -c`), +## which can inject config or redirect which repo is read and written. ## Each case plants one hostile setup into a copy of a repo, then checks: ## export bytes match a clean export, verify passes, and import reproduces exact tips. set -euo pipefail @@ -19,12 +22,20 @@ branches=(topic/fancy docs-polish odd-bytes) want_tips=$(printf '%s\n' "$tip_topic_fancy" "$tip_docs_polish" "$tip_odd_bytes") "$PATCHETTE" export "$work/corpus/dev" "$work/clean" "$base" "${branches[@]}" >/dev/null printf 'odd/trailing.txt\n' >"$work/orderfile" +# Env-case fixtures: a tree holding a hostile .gitattributes (in the subject's +# object store), a graft that turns docs-polish's tip into a root, +# and a decoy repo that holds the base, so a redirected import could succeed there. +attrs_tree=$(printf '100644 blob %s\t.gitattributes\n' "$(printf '* -diff\n' | git -C "$work/corpus/dev" hash-object -w --stdin)" | + git -C "$work/corpus/dev" mktree) +printf '%s\n' "$tip_docs_polish" >"$work/grafts" +git clone -q --bare "$work/corpus/upstream" "$work/decoy.git" # Each case is `|`-separated items: # `key=value` sets repo-local config; `@line` appends to the repo's info/attributes; # `%line` appends to an uncommitted worktree .gitattributes; # `!line` commits a .gitattributes on a new branch and checks it out -# (export bytes must not vary with which branch happens to be checked out). +# (export bytes must not vary with which branch happens to be checked out); +# `$VAR=value` sets env for the patchette invocations only. cases=( diff.context=1 diff.context=8 @@ -76,18 +87,30 @@ cases=( '%* -diff' '!* -diff' '!*.c diff=cpp' + '$GIT_CONFIG_COUNT=1|$GIT_CONFIG_KEY_0=diff.suppressBlankEmpty|$GIT_CONFIG_VALUE_0=true' + "\$GIT_CONFIG_PARAMETERS='core.quotepath'='false'" + "\$GIT_ATTR_SOURCE=$attrs_tree" + '$GIT_DIFF_OPTS=--unified=1' + "\$GIT_GRAFT_FILE=$work/grafts" + "\$GIT_DIR=$work/decoy.git" + "\$GIT_COMMON_DIR=$work/decoy.git" + "\$GIT_OBJECT_DIRECTORY=$work/decoy.git/objects" + "\$GIT_WORK_TREE=$work" ) ## Plant one hostile case into a repo. ## ## - @param $1 repo path (non-bare) ## - @param $2 case string, as described above the table +## - @state HOSTILE_ENV[] -- `VAR=value` words, for `env` to prefix patchette with hostilize() { local repo=$1 item local -a items + HOSTILE_ENV=() IFS='|' read -ra items <<<"$2" for item in "${items[@]}"; do case $item in + '$'*) HOSTILE_ENV+=("${item#\$}") ;; @*) printf '%s\n' "${item#@}" >>"$repo/.git/info/attributes" ;; %*) printf '%s\n' "${item#%}" >>"$repo/.gitattributes" ;; '!'*) @@ -110,15 +133,15 @@ for c in "${cases[@]}"; do # Command failures here surface on stderr just above the FAIL they cause. cp -a "$work/corpus/dev" "$d/subject" hostilize "$d/subject" "$c" - "$PATCHETTE" export "$d/subject" "$d/out" "$base" "${branches[@]}" >/dev/null || true + env "${HOSTILE_ENV[@]}" "$PATCHETTE" export "$d/subject" "$d/out" "$base" "${branches[@]}" >/dev/null || true EXPECT success "subject has $c: export bytes match a clean export" -- \ diff -r "$work/clean" "$d/out" EXPECT success "subject has $c: verify passes" -- \ - "$PATCHETTE" verify "$d/subject" "$work/clean" + env "${HOSTILE_ENV[@]}" "$PATCHETTE" verify "$d/subject" "$work/clean" git clone -q "$work/corpus/upstream" "$d/target" hostilize "$d/target" "$c" - "$PATCHETTE" import "$d/target" "$work/clean" >/dev/null || true + env "${HOSTILE_ENV[@]}" "$PATCHETTE" import "$d/target" "$work/clean" >/dev/null || true EXPECT equal "target has $c: import reproduces exact tips" -- \ "$(git -C "$d/target" rev-parse "${branches[@]/#/refs/heads/}" 2>/dev/null || true)" "$want_tips" done