ka-promote: auto-normalise git format-patch trailers (closes #31)

write_cumulative() now strips any "-- \n<MAJOR>.<MINOR>(.<PATCH>)?\n" sentinel
from each input patch and emits a single canonical separator between, but not
after, concatenated patches. Source patches in patches/<scope>/ can therefore
keep their original git format-patch shape regardless of their position in
fleet/<host>.yaml — the brittle "trailer flip-flop on include reorder" mode
from PR #28 (commits 84734ba ↔ ceec602) is gone.

Tests:
- new unit covers strip_trailer + write_cumulative shape with mixed
  trailer states + asserts no orphan trailer leaks at EOF
- fresnel parity b2sum re-recorded after the shape change
  (4d9d93c6... -> 9c21751c...) — the cumulative is byte-identical
  modulo per-patch trailer normalisation; git apply --check on the
  v7.0 baseline still passes
- existing series-dir, bad-include, missing-patch, duplicate-include
  rejections unchanged
This commit is contained in:
2026-05-19 06:30:38 +02:00
parent 7a86ebb587
commit 2f119a3fb7
2 changed files with 84 additions and 4 deletions
+39 -2
View File
@@ -27,6 +27,7 @@ import argparse
import glob
import hashlib
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
@@ -37,6 +38,17 @@ VERSION = 1
SCHEMA_VERSION = 1
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):
print(f"ka-promote: error: {msg}", file=sys.stderr)
@@ -124,11 +136,36 @@ def resolve_includes(includes, patches_root):
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):
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:
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:
b2 = hashlib.blake2b(f.read()).hexdigest()
size = os.path.getsize(out_path)