From 7e483b8a7f59cd7f34352d61161803920f35bd80 Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Wed, 11 Mar 2026 21:54:45 +0100 Subject: [PATCH 1/6] Add support for multiple git-check-assertions blocks --- bin/git-check-assertions | 12 ++++++------ default.nix | 3 +++ flake.nix | 1 + test/git-check-assertions.bats | 21 +++++++++++++++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/bin/git-check-assertions b/bin/git-check-assertions index 24bbfdf..11e0b44 100755 --- a/bin/git-check-assertions +++ b/bin/git-check-assertions @@ -106,11 +106,11 @@ for commit_hash in "${commits[@]}"; do echo "Checking out $commit_hash" git -c advice.detachedHead=false checkout -q "$commit_hash" commit_msg="$(git log -1 --format=%B "$commit_hash")" - # shellcheck disable=SC2016 - block="$(printf '%s\n' "$commit_msg" | - sed -n '/^```git-check-assertions[[:space:]]*$/,/^```[[:space:]]*$/p' | - sed '1d;$d')" - if [ -n "$block" ]; then + mapfile -d '' -t blocks < <( + printf '%s\n' "$commit_msg" | + perl -0777 -ne 'while (/^```git-check-assertions\s*\n(.*?)^```\s*$/msg){ print $1, "\0" }' + ) + for block in "${blocks[@]}"; do echo "git-check-assertions block in $commit_hash:" printf '%s\n' "$block" | sed 's/^/> /' if ! bash -euo pipefail -c "$block"; then @@ -121,7 +121,7 @@ for commit_hash in "${commits[@]}"; do exit 1 fi restore - fi + done printf '%s\n' "$commit_hash" >>.git-check-assertions-cache echo done diff --git a/default.nix b/default.nix index 12dcadf..4f9f1bd 100644 --- a/default.nix +++ b/default.nix @@ -9,6 +9,7 @@ gitMinimal, resholve, shfmt, + perl, }: let @@ -40,6 +41,7 @@ resholve.mkDerivation { ])) gitMinimal shfmt + perl ]; checkPhase = '' @@ -66,6 +68,7 @@ resholve.mkDerivation { gitMinimal gnused bash + perl ]; execer = [ # Not true at all, but ¯\_(ツ)_/¯ diff --git a/flake.nix b/flake.nix index e82af69..06dda90 100644 --- a/flake.nix +++ b/flake.nix @@ -22,6 +22,7 @@ p.bats-support p.bats-file ])) + pkgs.perl pkgs.shfmt ]; }; diff --git a/test/git-check-assertions.bats b/test/git-check-assertions.bats index e706f41..8805d2f 100755 --- a/test/git-check-assertions.bats +++ b/test/git-check-assertions.bats @@ -124,6 +124,27 @@ commit_with_assertion() { assert_output "feature" } +@test "multiple git-check-assertions blocks in one commit message run in order" { + git checkout -b feature + git commit --allow-empty -F - <<-EOF + test + + \`\`\`git-check-assertions + touch ../test1 + \`\`\` + + \`\`\`git-check-assertions + touch ../test2 + \`\`\` + EOF + + run git-check-assertions + + assert_success + assert_file_exists ../test1 + assert_file_exists ../test2 +} + @test "should stop at the first failing assertion block and return to the original branch" { git checkout -b feature commit_with_assertion "touch ../test1 && exit 3" From 35584c9aa75fc58a56ceeb5ebcf58ff4689e34d9 Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Wed, 11 Mar 2026 22:09:39 +0100 Subject: [PATCH 2/6] Restore the commit between assertion blocks ```git-check-assertions run ./test/git-check-assertions.bats assert_success git checkout HEAD~ bin run ./test/git-check-assertions.bats assert_failure assert_output --partial "not ok 10 should restore the commit between assertion blocks in one commit message" ``` --- bin/git-check-assertions | 1 + test/git-check-assertions.bats | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/bin/git-check-assertions b/bin/git-check-assertions index 11e0b44..75f9dc1 100755 --- a/bin/git-check-assertions +++ b/bin/git-check-assertions @@ -121,6 +121,7 @@ for commit_hash in "${commits[@]}"; do exit 1 fi restore + git -c advice.detachedHead=false checkout -q "$commit_hash" done printf '%s\n' "$commit_hash" >>.git-check-assertions-cache echo diff --git a/test/git-check-assertions.bats b/test/git-check-assertions.bats index 8805d2f..5ff0798 100755 --- a/test/git-check-assertions.bats +++ b/test/git-check-assertions.bats @@ -145,6 +145,31 @@ commit_with_assertion() { assert_file_exists ../test2 } +@test "should restore the commit between assertion blocks in one commit message" { + git checkout -b feature + echo old >readme + git add readme + git commit -m "old" + + echo new >readme + git add readme + git commit -F - <<-EOF + update + + \`\`\`git-check-assertions + git checkout HEAD~ + \`\`\` + + \`\`\`git-check-assertions + grep new readme + \`\`\` + EOF + + run git-check-assertions + + assert_success +} + @test "should stop at the first failing assertion block and return to the original branch" { git checkout -b feature commit_with_assertion "touch ../test1 && exit 3" From e5baffa6989635e626d6248df7b1e6d9fd8c190c Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Wed, 11 Mar 2026 22:35:18 +0100 Subject: [PATCH 3/6] Document support for multiple git-check-assertion blocks --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2611a30..b3d4095 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ dotnet test --no-build ``` ~~~ -This script will be run with `set -euo pipefail`, and the commit will be considered correct if the script exits successfully. +This script will be run with `set -euo pipefail` on the version of the code that is in the commit, and the commit will be considered correct if the script exits successfully. You can technically assert that a command fails by writing `! ... || exit 1`, or write assertions about a command's output by piping it to `grep`, but doing so won't lead to very useful error messages when things go wrong. To make those things easier, there are some helper functions included, which are inspired by [bats](https://github.com/bats-core/bats-core) and [bats-assert](https://github.com/bats-core/bats-assert): @@ -77,6 +77,10 @@ You can technically assert that a command fails by writing `! ... || exit 1`, or I'm considering taking `bats-assert` as a dependency, but for now, this very minimal set of functions with a similar interface should get you on your way. +## Multiple blocks in one commit message + +You can include multiple `git-check-assertions` blocks in a single commit message. When you do, each individual block will run with a 'fresh' version of the commit, in which all changes that you made in the previous block have been undone. + ## Cache of successful commits After a commit's assertions run successfully, `git-check-assertions` appends the commit hash to `.git-check-assertions-cache` in the repo root. On the next run, any commit listed there is skipped. From 32871dc261e9287b7c27504eccfcdd54e05f1ab2 Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Wed, 11 Mar 2026 22:38:37 +0100 Subject: [PATCH 4/6] Remove redundant newline --- bin/git-check-assertions | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/git-check-assertions b/bin/git-check-assertions index 75f9dc1..4633914 100755 --- a/bin/git-check-assertions +++ b/bin/git-check-assertions @@ -112,7 +112,7 @@ for commit_hash in "${commits[@]}"; do ) for block in "${blocks[@]}"; do echo "git-check-assertions block in $commit_hash:" - printf '%s\n' "$block" | sed 's/^/> /' + printf '%s' "$block" | sed 's/^/> /' if ! bash -euo pipefail -c "$block"; then echo "git-check-assertions block failed in $commit_hash" >&2 restore From 69f8acad8ffdd99bb48e31c99b6f29179304e301 Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Wed, 11 Mar 2026 22:41:14 +0100 Subject: [PATCH 5/6] Print block number and remove redundant commit hash --- bin/git-check-assertions | 4 +++- test/git-check-assertions.bats | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bin/git-check-assertions b/bin/git-check-assertions index 4633914..685b591 100755 --- a/bin/git-check-assertions +++ b/bin/git-check-assertions @@ -110,8 +110,10 @@ for commit_hash in "${commits[@]}"; do printf '%s\n' "$commit_msg" | perl -0777 -ne 'while (/^```git-check-assertions\s*\n(.*?)^```\s*$/msg){ print $1, "\0" }' ) + block_num=0 for block in "${blocks[@]}"; do - echo "git-check-assertions block in $commit_hash:" + block_num=$((block_num + 1)) + echo "git-check-assertions block $block_num:" printf '%s' "$block" | sed 's/^/> /' if ! bash -euo pipefail -c "$block"; then echo "git-check-assertions block failed in $commit_hash" >&2 diff --git a/test/git-check-assertions.bats b/test/git-check-assertions.bats index 5ff0798..be3b275 100755 --- a/test/git-check-assertions.bats +++ b/test/git-check-assertions.bats @@ -141,6 +141,8 @@ commit_with_assertion() { run git-check-assertions assert_success + assert_output --partial "git-check-assertions block 1:" + assert_output --partial "git-check-assertions block 2:" assert_file_exists ../test1 assert_file_exists ../test2 } From 3dc9930dbb74e9a13925a1ba02ed6b6628a5dd08 Mon Sep 17 00:00:00 2001 From: Sven van Heugten Date: Wed, 11 Mar 2026 22:46:10 +0100 Subject: [PATCH 6/6] Make `run` stream the output --- bin/git-check-assertions | 7 +++++-- default.nix | 3 +++ flake.nix | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/bin/git-check-assertions b/bin/git-check-assertions index 685b591..1195884 100755 --- a/bin/git-check-assertions +++ b/bin/git-check-assertions @@ -27,9 +27,12 @@ set -euo pipefail # helper functions inspired by bats/bats-assert run() { set +e - output="$("$@" 2>&1)" - status=$? + tmp_output="$(mktemp)" + "$@" 2>&1 | tee "$tmp_output" + status=${PIPESTATUS[0]} set -e + output="$(cat "$tmp_output")" + rm -f "$tmp_output" printf '%s\n' "$output" return 0 } diff --git a/default.nix b/default.nix index 4f9f1bd..c260e4c 100644 --- a/default.nix +++ b/default.nix @@ -10,6 +10,7 @@ resholve, shfmt, perl, + coreutils, }: let @@ -42,6 +43,7 @@ resholve.mkDerivation { gitMinimal shfmt perl + coreutils ]; checkPhase = '' @@ -69,6 +71,7 @@ resholve.mkDerivation { gnused bash perl + coreutils ]; execer = [ # Not true at all, but ¯\_(ツ)_/¯ diff --git a/flake.nix b/flake.nix index 06dda90..1fabe89 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,7 @@ ])) pkgs.perl pkgs.shfmt + pkgs.coreutils ]; }; }