patchette

test-hermetic.sh

#!/bin/bash
## Hermeticity: hostile repo-local git config and attributes, in the subject
## 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
cd "$(dirname "$0")"
source ./lib.sh

work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT

make_corpus "$work/corpus"
source "$work/corpus/FACTS"
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);
# `$VAR=value` sets env for the patchette invocations only.
cases=(
	diff.context=1
	diff.context=8
	diff.interHunkContext=10
	diff.algorithm=histogram
	diff.algorithm=minimal
	diff.indentHeuristic=false
	diff.renames=copies
	diff.noprefix=true
	diff.mnemonicPrefix=true
	'diff.srcPrefix=old/|diff.dstPrefix=new/'
	diff.relative=true
	diff.suppressBlankEmpty=true
	"diff.orderFile=$work/orderfile"
	diff.wsErrorHighlight=all
	color.ui=always
	color.diff=always
	core.abbrev=12
	core.quotePath=false
	core.bigFileThreshold=1
	core.autocrlf=true
	core.autocrlf=input
	core.eol=crlf
	'core.autocrlf=true|core.safecrlf=true'
	core.fileMode=false
	core.symlinks=false
	core.ignoreCase=true
	core.precomposeUnicode=true
	core.whitespace=trailing-space,space-before-tab,cr-at-eol,blank-at-eof
	core.splitIndex=true
	index.version=4
	apply.whitespace=fix
	apply.whitespace=error
	apply.ignoreWhitespace=change
	i18n.logOutputEncoding=ISO-8859-1
	log.showSignature=true
	format.pretty=oneline
	'@* -diff'
	'@* binary'
	'@*.c diff=cpp'
	'@* diff=hostile|diff.hostile.xfuncname=^/\*.*$'
	'@* diff=hostile|diff.hostile.textconv=tr a-z A-Z'
	'@* diff=hostile|diff.hostile.binary=true'
	'@* filter=hostile|filter.hostile.clean=tr a-z A-Z|filter.hostile.smudge=cat'
	'@* text eol=crlf'
	'@* text=auto'
	'@* whitespace=trailing-space,cr-at-eol'
	'%*.c diff=cpp'
	'%* -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" ;;
		'!'*)
			printf '%s\n' "${item#!}" >>"$repo/.gitattributes"
			git -C "$repo" switch -qc hostile-attrs
			git -C "$repo" add .gitattributes
			git -C "$repo" commit -qm "hostile attributes"
			;;
		*) git -C "$repo" config "${item%%=*}" "${item#*=}" ;;
		esac
	done
}

n=0
for c in "${cases[@]}"; do
	n=$((n + 1))
	d="$work/case$n"
	mkdir "$d"

	# Command failures here surface on stderr just above the FAIL they cause.
	cp -a "$work/corpus/dev" "$d/subject"
	hostilize "$d/subject" "$c"
	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" -- \
		env "${HOSTILE_ENV[@]}" "$PATCHETTE" verify "$d/subject" "$work/clean"

	git clone -q "$work/corpus/upstream" "$d/target"
	hostilize "$d/target" "$c"
	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

exit "$RESULT"

tests/test-hermetic.sh · 5.3 KiB · 040980822a