patchette

lib.sh

#!/bin/bash
## Shared helpers for the patchette tests.  Sourced, not executed.
## Provides a hermetic, fully-pinned git environment (fixed identities and a
## tick clock belong HERE, not in the tool: tests author commits, patchette
## never does), the test corpus builder, and PASS/FAIL reporting helpers.

set -euo pipefail

export GIT_CONFIG_GLOBAL=/dev/null
export GIT_CONFIG_SYSTEM=/dev/null
export GIT_CONFIG_NOSYSTEM=1
export GIT_ATTR_NOSYSTEM=1
export LC_ALL=C.UTF-8
export TZ=UTC

export GIT_AUTHOR_NAME="Ada Downstream"
export GIT_AUTHOR_EMAIL="ada@downstream.example"
export GIT_COMMITTER_NAME="Ada Downstream"
export GIT_COMMITTER_EMAIL="ada@downstream.example"

PATCHETTE=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/bin/patchette

RESULT=0

## Report one passing check.
##
## - @param $@ description
pass() {
	echo "PASS  $*"
}

## Report one failing check and mark the test script failed.
##
## - @param $@ description
## - @state RESULT -- set to 1
fail() {
	echo "FAIL  $*"
	RESULT=1
}

## Run a command expected to refuse, and check it does so for the right reason.
##
## PASSes only when the command exits nonzero AND its output contains the
## expected message fragment: an exit code alone could come from any guard
## (or any crash), and we want each test tied to its specific refusal.
##
## - @param $1 check description
## - @param $2 message fragment the combined output must contain
## - @param $@ the command
expect_refusal() {
	local desc=$1 want=$2 out rc=0
	shift 2
	out=$("$@" 2>&1) || rc=$?
	if [[ $rc -eq 0 ]]; then
		fail "$desc: expected a refusal, but the command succeeded"
	elif [[ $out != *"$want"* ]]; then
		fail "$desc: refused, but not with the expected message '$want'; got: $out"
	else
		pass "$desc"
	fi
}

_TICK=1700000000

## Advance the fixed clock by one minute, for the next commit.
##
## - @state GIT_AUTHOR_DATE, GIT_COMMITTER_DATE -- set to the new clock value.
tick() {
	_TICK=$((_TICK + 60))
	export GIT_AUTHOR_DATE="@${_TICK} +0000"
	export GIT_COMMITTER_DATE="@${_TICK} +0000"
}

## Build the test corpus: an upstream repo, and a dev clone carrying two
## to-be-tracked branches.  Every commit is hostile on purpose.
##
## `topic/fancy` exercises: linear commits, a subject-slug collision, a rename,
## a merge (with an in-range side branch), an extra commit header in the
## jj `change-id` style, an empty commit, a body with a blank line, and a
## message without a final newline.
## `docs-polish` exists to make the layout's branch mapping visible.
##
## - @param $1 output directory; created.  Gets `upstream/`, `dev/`, and a
##   sourceable `FACTS` file recording the base SHA and branch tips.
make_corpus() (
	local out=$1 base first raw new
	mkdir -p "$out"
	out=$(cd "$out" && pwd)

	# --- upstream ---
	git init -q -b main "$out/upstream"
	cd "$out/upstream"
	{
		printf 'The frobnicator.\n'
		for i in 1 2 3 4 5 6 7 8; do printf 'gear %s turns smoothly\n' "$i"; done
	} >frobnicator.txt
	mkdir widgets
	printf 'widget: mk1\nstate: dusty\n' >widgets/widget.txt
	printf '# Machinery\n\nUpstream docs.\n' >README.md
	git add -A
	tick
	git commit -q -m "Initial machinery"
	printf '\nMore upstream docs.\n' >>README.md
	git add -A
	tick
	git commit -q -m "Document the machinery"
	base=$(git rev-parse HEAD)

	# --- dev clone, branch topic/fancy ---
	git clone -q "$out/upstream" "$out/dev"
	cd "$out/dev"
	git switch -qc topic/fancy

	# plain linear change
	sed -i 's/gear 3 turns smoothly/gear 3 turns BETTER/' frobnicator.txt
	git add -A
	tick
	git commit -q -m "Fix the frobnicator"
	first=$(git rev-parse HEAD)

	# same subject again: forces the slug collision rule to show itself
	sed -i 's/gear 5 turns smoothly/gear 5 turns BETTER/' frobnicator.txt
	git add -A
	tick
	git commit -q -m "Fix the frobnicator"

	# rename plus edit
	mkdir gizmos
	git mv widgets/widget.txt gizmos/widget.txt
	sed -i 's/state: dusty/state: polished/' gizmos/widget.txt
	git add -A
	tick
	git commit -q -m "Move widget into place"

	# a side branch forked in-range, then merged: exercises parents-as-slugs
	git switch -qc sprocket-work "$first"
	printf '# Sprockets\n\nThey spin.\n' >SPROCKETS.md
	git add -A
	tick
	git commit -q -m "Add sprocket docs"
	git switch -q topic/fancy
	tick
	git merge -q --no-ff -m "Merge sprocket work" sprocket-work
	git branch -qD sprocket-work

	# extra commit header, jj change-id style; handcrafted via raw object rewrite.
	# The body (with its blank line) exercises message framing in the exports.
	sed -i 's/gear 7 turns smoothly/gear 7 hums quietly/' frobnicator.txt
	git add -A
	tick
	git commit -q -m "Teach gizmo to hum" -m $'The hum is load-bearing.\nDo not ask why.'
	raw=$(mktemp)
	git cat-file commit HEAD >"$raw"
	new=$(sed '/^committer /a change-id kpqxwvuzyntsmrlokqjihvgzfyexwdcu' "$raw" |
		git hash-object -t commit -w --stdin)
	rm "$raw"
	git update-ref refs/heads/topic/fancy "$new"

	# empty commit
	tick
	git commit -q --allow-empty -m "Note to self"

	# message without a final newline: porcelain never makes one, but the object
	# model allows it, and total fidelity means total
	sed -i 's/gear 8 turns smoothly/gear 8 clicks twice/' frobnicator.txt
	git add -A
	tick
	git commit -q -m "Scribble scribble"
	new=$(git cat-file commit HEAD | head -c -1 | git hash-object -t commit -w --stdin)
	git update-ref refs/heads/topic/fancy "$new"

	# --- second branch, small on purpose ---
	git switch -qc docs-polish main
	printf '\nDownstream notes: be kind to the machinery.\n' >>README.md
	git add -A
	tick
	git commit -q -m "Polish the readme"

	{
		printf 'base=%s\n' "$base"
		printf 'tip_topic_fancy=%s\n' "$(git rev-parse topic/fancy)"
		printf 'tip_docs_polish=%s\n' "$(git rev-parse docs-polish)"
	} >"$out/FACTS"
)

tests/lib.sh · 5.6 KiB · 94fd195743