Merge pull request 'ka-promote: auto-normalise git format-patch trailers (closes #31)' (#32) from noether/ka-promote-normalise-trailers into main

Reviewed-on: #32
This commit was merged in pull request #32.
This commit is contained in:
2026-05-19 04:33:03 +00:00
2 changed files with 84 additions and 4 deletions
+39 -2
View File
@@ -27,6 +27,7 @@ import argparse
import glob import glob
import hashlib import hashlib
import os import os
import re
import subprocess import subprocess
import sys import sys
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -37,6 +38,17 @@ VERSION = 1
SCHEMA_VERSION = 1 SCHEMA_VERSION = 1
COVER_LETTER = "0000-cover-letter.patch" COVER_LETTER = "0000-cover-letter.patch"
# git format-patch trailer: "-- \n<MAJOR>.<MINOR>(.<PATCH>)?\n" at EOF,
# possibly with trailing blank line(s). Strip from each source patch so
# that the cumulative is always well-formed regardless of include order.
# See issue #31.
_TRAILER_RE = re.compile(rb'\n-- \n\d+\.\d+(?:\.\d+)?\n+\Z')
# Canonical separator emitted between concatenated patches in the
# cumulative. Trailing blank line keeps patch(1) happy when the next
# patch starts with "From <sha>".
_CANONICAL_TRAILER = b'-- \n2.54.0\n\n'
def die(msg, code=1): def die(msg, code=1):
print(f"ka-promote: error: {msg}", file=sys.stderr) print(f"ka-promote: error: {msg}", file=sys.stderr)
@@ -124,11 +136,36 @@ def resolve_includes(includes, patches_root):
return resolved return resolved
def strip_trailer(data):
"""Strip any trailing git format-patch sentinel from a patch.
Accepts patches in either canonical shape:
- WITH trailer: "...\n-- \n2.54.0\n\n"
- WITHOUT trailer: "...\n" (already stripped)
Returns data ending in a single newline so the caller can either
append a canonical trailer (mid-cumulative) or leave it bare (last).
"""
stripped = _TRAILER_RE.sub(b'\n', data)
if not stripped.endswith(b'\n'):
stripped += b'\n'
return stripped
def write_cumulative(resolved, out_path): def write_cumulative(resolved, out_path):
with open(out_path, "wb") as out: with open(out_path, "wb") as out:
for r in resolved: n = len(resolved)
for i, r in enumerate(resolved):
with open(r["src"], "rb") as src: with open(r["src"], "rb") as src:
out.write(src.read()) data = src.read()
data = strip_trailer(data)
out.write(data)
# Mid-cumulative patches need a separator so patch(1) knows
# where they end and the next "From <sha>" begins. Last
# patch stays bare — a trailing orphan sentinel reads as
# the start of a malformed new patch at EOF (issue #31).
if i != n - 1:
out.write(_CANONICAL_TRAILER)
with open(out_path, "rb") as f: with open(out_path, "rb") as f:
b2 = hashlib.blake2b(f.read()).hexdigest() b2 = hashlib.blake2b(f.read()).hexdigest()
size = os.path.getsize(out_path) size = os.path.getsize(out_path)
+45 -2
View File
@@ -12,8 +12,10 @@ set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
fixtures="${repo_root}/tests/ka-promote/fixtures" fixtures="${repo_root}/tests/ka-promote/fixtures"
# Phase-3 ground truth — recorded 2026-05-18, fresnel cumulative b2sum. # Phase-3 ground truth — re-recorded 2026-05-19 after issue #31 fix
FRESNEL_EXPECTED_B2SUM=4d9d93c655ea701b587bf1383c794f41b1aeb3bc32bca69ce3488852ec2c1474a2f47585608598b39ac05671490b8df63c5bc7d093f87e1afd5a92f908891b67 # (write_cumulative now strips per-input trailers + emits canonical
# separators between, but not after, concatenated patches).
FRESNEL_EXPECTED_B2SUM=9c21751cc48ab57cdf48058cc4309752de169c567bbb898c342ff3e4a5cc79add53e3fd4217c2ae2ae7c16b0f19518cf1791907367e1ea9ef16458e1e90c05e0
pass=0 pass=0
fail=0 fail=0
@@ -110,6 +112,47 @@ echo
echo "Running ka-promote test suite from $repo_root" echo "Running ka-promote test suite from $repo_root"
echo echo
# ----- unit: strip_trailer + write_cumulative shape (issue #31) -----
echo "::: strip_trailer + cumulative shape (issue #31)"
python3 - "$repo_root" <<'PY'
import importlib.util, pathlib, sys, tempfile, os
root = pathlib.Path(sys.argv[1])
from importlib.machinery import SourceFileLoader
mod = SourceFileLoader("ka_promote", str(root/"bin"/"ka-promote")).load_module()
# strip_trailer accepts both shapes and yields newline-terminated body
assert mod.strip_trailer(b"...body...\n-- \n2.54.0\n\n") == b"...body...\n"
assert mod.strip_trailer(b"...body...\n-- \n2.53.0\n\n") == b"...body...\n"
assert mod.strip_trailer(b"...body...\n-- \n2.20\n\n") == b"...body...\n"
assert mod.strip_trailer(b"...body...\n") == b"...body...\n"
assert mod.strip_trailer(b"...body...") == b"...body...\n"
# Multiple trailing blanks after the version still strip
assert mod.strip_trailer(b"x\n-- \n2.54.0\n\n\n") == b"x\n"
# write_cumulative: 3 inputs (mix of with-/without-trailer), check ordering
with tempfile.TemporaryDirectory() as d:
p1 = os.path.join(d, "a.patch"); open(p1,"wb").write(b"PA\n-- \n2.54.0\n\n")
p2 = os.path.join(d, "b.patch"); open(p2,"wb").write(b"PB\n") # already bare
p3 = os.path.join(d, "c.patch"); open(p3,"wb").write(b"PC\n-- \n2.40.1\n\n")
out = os.path.join(d, "out.patch")
resolved = [{"src": p1}, {"src": p2}, {"src": p3}]
mod.write_cumulative(resolved, out)
body = open(out,"rb").read()
assert body == b"PA\n-- \n2.54.0\n\nPB\n-- \n2.54.0\n\nPC\n", repr(body)
# Last patch (PC) must NOT carry an orphan trailer at EOF
assert not body.rstrip(b"\n").endswith(b"2.40.1"), \
f"last patch's trailer leaked into cumulative: {body[-40:]!r}"
print("PASS")
PY
if [ $? -eq 0 ]; then
results+=("PASS strip_trailer + cumulative shape (issue #31)")
pass=$((pass+1))
else
results+=("FAIL strip_trailer + cumulative shape (issue #31)")
fail=$((fail+1))
fi
echo
# Use the real fleet/fresnel.yaml — copy into a sandbox so the test is hermetic. # Use the real fleet/fresnel.yaml — copy into a sandbox so the test is hermetic.
mkdir -p /tmp/ka-promote-parity-fixture mkdir -p /tmp/ka-promote-parity-fixture
cp "$repo_root/fleet/fresnel.yaml" /tmp/ka-promote-parity-fixture/fresnel.yaml cp "$repo_root/fleet/fresnel.yaml" /tmp/ka-promote-parity-fixture/fresnel.yaml