usage: a blank line between commands, and a shared prelude for sessions
commit 271bda88650c9277dc32bc9ae90190f595935100 · parent bd494f7d30
· browse this tree
Ximianth, 2026-10-26 11:01:26 UTC
Sessions were hard to scan: each command ran straight on from the previous command's output. The runner now puts a blank line before each prompt after the first. Five of the sessions opened with the same three lines of setup (cd, git init, pair). That setup is now a `paired/prelude` hunk in the doc, shown once, right after the section that walks through it; sessions named `paired/NAME/session` start after it has run, and `fresh/NAME/session` ones start from the bare sandbox. The prelude's lines run in the session's own shell, so its `cd` carries over, and they print nothing unless one fails. Sessions share no state: each still gets its own sandbox, and runs the prelude for itself. A failing prelude command fails the case, saying which command failed and what it printed, and the session is passed through untouched, so regen can't overwrite it. Without that, a typo in the prelude would have blanked every paired session under regen -- their commands with them. The test script no longer sets -euo pipefail at its top: under set -e, testmark-bash reports any failing case as "report to the runner was garbled or cut short" rather than its real failure. (Minimal repro: a test script with `set -e` and a rule whose function fails.) Cases run under set -euo pipefail regardless; the source of testmark-bash gets an explicit `|| exit 1` instead. Every session's output is unchanged apart from the blank lines and the lines the prelude took over.
diff --git a/docs/userguide/USAGE.md b/docs/userguide/USAGE.md index 79c185a..df3e303 100644 --- a/docs/userguide/USAGE.md +++ b/docs/userguide/USAGE.md @@ -16,13 +16,16 @@ Setting up The patches live in a repo of their own, beside your clone. Make one, then pair your clone with it, so patchette knows where to look: -[testmark]:# (setting-up/session) +[testmark]:# (fresh/setting-up/session) ``` ~$ cd src/frob + ~/src/frob$ git init -b main ../frob-patches Initialized empty Git repository in ~/src/frob-patches/.git/ + ~/src/frob$ patchette pair ../frob-patches paired: '~/src/frob' -> patches dir '../frob-patches' + ~/src/frob$ patchette pair paired: patches dir is '../frob-patches' (relative to '~/src/frob') ``` @@ -30,6 +33,16 @@ paired: patches dir is '../frob-patches' (relative to '~/src/frob') Pairing is the one thing patchette keeps in your repo: a single value, in `.git/patchette`, never pushed. Delete that file to un-pair. +The sessions in the rest of this guide each start as though you'd just done that +(apart from the last two sections, which start unpaired, from `~`): + +[testmark]:# (paired/prelude) +```bash +cd ~/src/frob +git init -q -b main ../frob-patches +patchette pair ../frob-patches +``` + Taking snapshots ---------------- @@ -37,19 +50,18 @@ Taking snapshots Work on your branches however you like; then export them, and commit the result in the patches repo. Export takes the upstream commit your branches are based on, then the branches: -[testmark]:# (snapshotting/session) +[testmark]:# (paired/snapshotting/session) ``` -~$ cd src/frob -~/src/frob$ git init -q -b main ../frob-patches -~/src/frob$ patchette pair ../frob-patches -paired: '~/src/frob' -> patches dir '../frob-patches' ~/src/frob$ patchette export origin/main topic/fancy docs-polish export: wrote ~/src/frob-patches/topic¦fancy (8 patches) export: wrote ~/src/frob-patches/docs-polish (1 patches) + ~/src/frob$ ls ../frob-patches/docs-polish STACK polish-the-readme.patch + ~/src/frob$ git -C ../frob-patches add . + ~/src/frob$ git -C ../frob-patches commit -q -m "first snapshot" ``` @@ -58,21 +70,24 @@ Each branch becomes a folder: a `STACK` file saying where it's based and what's The payoff comes when your patches change. Here, a commit on `docs-polish` gets reworked; export again, and the patches repo shows what changed about the change: -[testmark]:# (evolving/session) +[testmark]:# (paired/evolving/session) ``` -~$ cd src/frob -~/src/frob$ git init -q -b main ../frob-patches -~/src/frob$ patchette pair ../frob-patches -paired: '~/src/frob' -> patches dir '../frob-patches' ~/src/frob$ patchette export origin/main docs-polish export: wrote ~/src/frob-patches/docs-polish (1 patches) + ~/src/frob$ git -C ../frob-patches add . + ~/src/frob$ git -C ../frob-patches commit -q -m "first snapshot" + ~/src/frob$ git switch -q docs-polish + ~/src/frob$ sed -i 's/be kind to/be gentle with/' README.md + ~/src/frob$ git commit -q -a --amend --no-edit + ~/src/frob$ patchette export origin/main docs-polish export: wrote ~/src/frob-patches/docs-polish (1 patches) + ~/src/frob$ git -C ../frob-patches diff diff --git a/docs-polish/STACK b/docs-polish/STACK index d187142..eb8cce4 100644 @@ -123,18 +138,16 @@ Checking a snapshot It never touches your branches. Name branches to check just those: -[testmark]:# (verifying/session) +[testmark]:# (paired/verifying/session) ``` -~$ cd src/frob -~/src/frob$ git init -q -b main ../frob-patches -~/src/frob$ patchette pair ../frob-patches -paired: '~/src/frob' -> patches dir '../frob-patches' ~/src/frob$ patchette export origin/main topic/fancy docs-polish export: wrote ~/src/frob-patches/topic¦fancy (8 patches) export: wrote ~/src/frob-patches/docs-polish (1 patches) + ~/src/frob$ patchette verify verify: ok: refs/heads/docs-polish reproduces recorded tip 485a9d568a1be975c873793612c74733df10dd30 verify: ok: refs/heads/topic/fancy reproduces recorded tip cc261dc9f0b878a78e2b80aeb60de397205f61ad + ~/src/frob$ patchette verify topic/fancy verify: ok: refs/heads/topic/fancy reproduces recorded tip cc261dc9f0b878a78e2b80aeb60de397205f61ad ``` @@ -146,25 +159,28 @@ Rebuilding branches in a fresh clone The patches repo is enough to bring your branches back, exactly, in a clone that never had them. Pair the new clone, and import: -[testmark]:# (recreating/session) +[testmark]:# (paired/recreating/session) ``` -~$ cd src/frob -~/src/frob$ git init -q -b main ../frob-patches -~/src/frob$ patchette pair ../frob-patches -paired: '~/src/frob' -> patches dir '../frob-patches' ~/src/frob$ patchette export origin/main topic/fancy docs-polish export: wrote ~/src/frob-patches/topic¦fancy (8 patches) export: wrote ~/src/frob-patches/docs-polish (1 patches) + ~/src/frob$ cd .. + ~/src$ git clone -q ~/upstream/frob frob2 + ~/src$ cd frob2 + ~/src/frob2$ patchette pair ../frob-patches paired: '~/src/frob2' -> patches dir '../frob-patches' + ~/src/frob2$ patchette import import: created refs/heads/docs-polish at 485a9d568a1be975c873793612c74733df10dd30 import: created refs/heads/topic/fancy at cc261dc9f0b878a78e2b80aeb60de397205f61ad + ~/src/frob2$ git rev-parse topic/fancy cc261dc9f0b878a78e2b80aeb60de397205f61ad + ~/src/frob2$ git -C ../frob rev-parse topic/fancy cc261dc9f0b878a78e2b80aeb60de397205f61ad ``` @@ -178,32 +194,37 @@ Then rebuild the branch from them. `import` won't replace a branch that already exists unless you say `-f`, and even then, not the branch you have checked out: switch away first. -[testmark]:# (hand-editing/session) +[testmark]:# (paired/hand-editing/session) ``` -~$ cd src/frob -~/src/frob$ git init -q -b main ../frob-patches -~/src/frob$ patchette pair ../frob-patches -paired: '~/src/frob' -> patches dir '../frob-patches' ~/src/frob$ patchette export origin/main topic/fancy export: wrote ~/src/frob-patches/topic¦fancy (8 patches) + ~/src/frob$ sed -i 's/^> The hum is load-bearing.$/> The hum is load-bearing, and tuned./' ../frob-patches/topic¦fancy/teach-gizmo-to-hum.patch + ~/src/frob$ patchette import topic/fancy patchette: 'refs/heads/topic/fancy' already exists in '~/src/frob'; refusing to touch it (-f updates it instead) [exit status: 1] + ~/src/frob$ git switch -q topic/fancy + ~/src/frob$ patchette import -f topic/fancy patchette: 'refs/heads/topic/fancy' is checked out in '~/src/frob'; refusing to move it under that checkout, even with -f [exit status: 1] + ~/src/frob$ git switch -q main + ~/src/frob$ patchette import -f topic/fancy import: updated refs/heads/topic/fancy to 57d05b16d9ad81fd92af19d210f0884dc9745e01 (previous tip cc261dc9f0b878a78e2b80aeb60de397205f61ad remains in its reflog) import: note: replayed tip differs from recorded tip cc261dc9f0b878a78e2b80aeb60de397205f61ad (patches edited since the snapshot?) + ~/src/frob$ patchette verify topic/fancy verify: MISMATCH: refs/heads/topic/fancy replays to 57d05b16d9ad81fd92af19d210f0884dc9745e01, but records tip cc261dc9f0b878a78e2b80aeb60de397205f61ad verify: either the snapshot was unfaithful, or the patches were edited since (patches repo is ahead of its snapshot) [exit status: 1] + ~/src/frob$ patchette export origin/main topic/fancy export: wrote ~/src/frob-patches/topic¦fancy (8 patches) + ~/src/frob$ patchette verify topic/fancy verify: ok: refs/heads/topic/fancy reproduces recorded tip 57d05b16d9ad81fd92af19d210f0884dc9745e01 ``` @@ -219,11 +240,13 @@ Pairing is a convenience. From anywhere, `--subject` names your repo and `--patches` the patches repo; each one, given, wins over the pairing. -[testmark]:# (flags/session) +[testmark]:# (fresh/flags/session) ``` ~$ git init -q -b main src/frob-patches + ~$ patchette export --subject=src/frob --patches=src/frob-patches origin/main docs-polish export: wrote ~/src/frob-patches/docs-polish (1 patches) + ~$ patchette verify --subject=src/frob --patches=src/frob-patches verify: ok: refs/heads/docs-polish reproduces recorded tip 485a9d568a1be975c873793612c74733df10dd30 ``` @@ -232,27 +255,37 @@ verify: ok: refs/heads/docs-polish reproduces recorded tip 485a9d568a1be975c8737 When something's wrong ---------------------- -[testmark]:# (refusals/session) +[testmark]:# (fresh/refusals/session) ``` ~$ cd src/frob + ~/src/frob$ patchette verify patchette: --subject/--patches not given, and '~/src/frob' is not paired; run 'patchette pair <patches-dir>' there, or pass the flags [exit status: 1] + ~/src/frob$ cd /tmp + /tmp$ patchette verify --subject="$HOME/src/frob" patchette: --subject/--patches not given, and the cwd is not inside a git repository [exit status: 1] + /tmp$ cd ~/src/frob + ~/src/frob$ mkdir ../frob-patches + ~/src/frob$ patchette pair ../frob-patches paired: '~/src/frob' -> patches dir '../frob-patches' + ~/src/frob$ patchette export origin/main docs-polish export: wrote ~/src/frob-patches/docs-polish (1 patches) + ~/src/frob$ patchette verify docs-polish --patches=../frob-patches patchette: no branch folder for '--patches=../frob-patches' in '~/src/frob-patches' [exit status: 1] + ~/src/frob$ patchette verify --patches= verify: ok: refs/heads/docs-polish reproduces recorded tip 485a9d568a1be975c873793612c74733df10dd30 + ~/src/frob$ patchette export patchette: usage: patchette export [--sigil=quote|--sigil=tab] [--subject=<repo>] [--patches=<dir>] <base-rev> <branch>... [exit status: 1] diff --git a/tests/test-usage.sh b/tests/test-usage.sh index 1830961..e6a975a 100755 --- a/tests/test-usage.sh +++ b/tests/test-usage.sh @@ -1,21 +1,48 @@ #!/bin/bash -## Runs the usage guide's examples. Each `CASE/session` hunk in docs/userguide/USAGE.md +## Runs the usage guide's examples. Each `.../session` hunk in docs/userguide/USAGE.md ## is a terminal session: its prompt lines (`~/dir$ command`, or `/dir$ command`) are run in order, in a fresh sandbox, ## and the whole session as it ran -- prompts, commands, output -- must match the hunk. +## A `paired/NAME/session` starts after the doc's `paired/prelude` hunk has run; a `fresh/NAME/session` starts from the bare sandbox. ## Run with TESTMARK_REGEN=1 to rewrite the sessions from what patchette does now; review with `git diff`. -set -euo pipefail -source "${TESTMARK_BASH:-$(dirname "${BASH_SOURCE[0]}")/../../../warplibs/testmark-bash}/lib/testmark.bash" +# No set -e up here: testmark-bash mis-reports failing cases under it. Cases run under set -euo pipefail regardless. +source "${TESTMARK_BASH:-$(dirname "${BASH_SOURCE[0]}")/../../../warplibs/testmark-bash}/lib/testmark.bash" || exit 1 + +## run_prelude runs a prelude script's commands in this shell, one line each, quietly: +## a session's starting point, so its `cd`s and settings carry into the session. +## Blank lines and `#` comments are skipped. +## A failing command stops it, printing the command and its output on stderr, so the case fails, even under regen; +## the caller then passes the session through untouched, so regen can't write over it. +## +## - @param $1 path to the prelude script +## - @stderr on failure only: the failed command, its output, and its exit status +run_prelude() { + local line rc capture + capture=$(mktemp) + while IFS= read -r line; do + [[ -n $line && $line != '#'* ]] || continue + rc=0 + eval "$line" </dev/null >"$capture" 2>&1 || rc=$? + if [[ $rc -ne 0 ]]; then + >&2 printf 'prelude command failed, exit status %s: %s\n' "$rc" "$line" + >&2 cat "$capture" + rm -f "$capture" + return 1 + fi + done <"$1" + rm -f "$capture" +} ## replay_session runs a session's commands in this shell, in order, and prints the session as it ran. ## Each command is printed after a prompt naming the cwd (as `~`-relative), then what it printed, ## with the home dir shown as `~` there too; a nonzero exit adds an `[exit status: N]` line. -## The old session's other lines (its old output) are dropped: they're what gets regenerated. +## A blank line separates each command from the output before it. +## The old session's other lines (its old output, and blank lines) are dropped: they're what gets regenerated. ## Commands get /dev/null on stdin, so nothing can wait on input. ## ## - @stdin the session, as written ## - @stdout the session, as it ran replay_session() { - local prompt_re='^[~/][^$]*\$ (.*)$' line cmd rc out + local prompt_re='^[~/][^$]*\$ (.*)$' line cmd rc out sep='' local -a lines local capture capture=$(mktemp) @@ -23,7 +50,8 @@ replay_session() { for line in "${lines[@]}"; do [[ $line =~ $prompt_re ]] || continue cmd=${BASH_REMATCH[1]} - printf '%s$ %s\n' "${PWD/#"$HOME"/"~"}" "$cmd" + printf '%s%s$ %s\n' "$sep" "${PWD/#"$HOME"/"~"}" "$cmd" + sep=$'\n' rc=0 eval "$cmd" </dev/null >"$capture" 2>&1 || rc=$? out=$(<"$capture") @@ -33,11 +61,13 @@ replay_session() { rm -f "$capture" } -## exercise_session runs one session in a fresh sandbox home: -## `~/upstream/frob` is the upstream, and `~/src/frob` a clone of it, with `main` checked out, +## run_in_sandbox runs the case's session in a fresh sandbox home, after a prelude hunk, if one is named. +## The sandbox: `~/upstream/frob` is the upstream, and `~/src/frob` a clone of it, with `main` checked out, ## carrying the branches `topic/fancy`, `docs-polish`, and `odd-bytes` (see make_corpus). ## Git's identity and clock are pinned, so commits made in a session are reproducible. -exercise_session() { +## +## - @param $1 optional: the doc hunk holding the prelude +run_in_sandbox() { source "$TESTCASE_DIR/lib.sh" testcase_withtmpdir make_corpus corpus @@ -49,12 +79,24 @@ exercise_session() { git -C "$HOME/src/frob" switch -q main export PATH="${PATCHETTE%/patchette}:$PATH" export GIT_AUTHOR_DATE='@1700086400 +0000' GIT_COMMITTER_DATE='@1700086400 +0000' + local prelude=$PWD/prelude.sh + : >"$prelude" + if [[ -n ${1:-} ]]; then testcase_doc_hunk "$1" >"$prelude"; fi cd "$HOME" testcase_hunk session | - bash --norc --noprofile -c "$(declare -f replay_session); replay_session" | + bash --norc --noprofile -c "$(declare -f run_prelude replay_session); if run_prelude \"\$1\"; then replay_session; else cat; exit 1; fi" _ "$prelude" | testcase_output session } -testmarktest_provide replay_session -testmarktest_rule '*/session' exercise_session +exercise_fresh() { + run_in_sandbox +} + +exercise_paired() { + run_in_sandbox paired/prelude +} + +testmarktest_provide run_prelude replay_session run_in_sandbox +testmarktest_rule 'fresh/*/session' exercise_fresh +testmarktest_rule 'paired/*/session' exercise_paired testmarktest_run ../docs/userguide/USAGE.md