patchset: patches that name their own targets, applied all or nothing¶
The problem¶
Local changes to code you do not own (a vendored library, an unpacked app, a generated file) must be re-applied on every upstream release. A build script that lists "patch X goes on file Y" drifts from the patches, and a run that dies halfway leaves the tree half-patched with no record of which files changed.
The method¶
The patch carries its own address, in a header near its top:
# @patch-target: lib/app.js a path, or a glob that must match exactly one file
# @patch-type: run run (an executable that edits the path given) | replace (bytes)
Dropping a file in the folder is the whole registration. Then stage, then commit:
- Each target is copied to a temp file beside itself; its patches run on the copy in name order.
- Only when every patch in the folder succeeded is each copy renamed over its target. One failure anywhere and every file stays as it was.
- Before each rename the target's size, time and inode are compared with the staged moment; a file edited meanwhile is left alone and named.
- A glob matching 0 or 2+ files is an error, not "use the first". Targets
outside the tree (absolute,
.., symlinks pointing out) are refused. - Unchanged files are reported
sameand keep their date.--checkruns everything and writes nothing.
Sample¶
for target, patches in groups.items():
tmp = temp_beside(target); shutil.copy2(target, tmp)
for patch in patches: # name order
if subprocess.run([patch, tmp], timeout=60).returncode:
fail(patch); break # nothing renamed yet
if not failed:
for target, tmp, before in staged:
if stamp(target) == before: # nobody edited it meanwhile
os.replace(tmp, target)
Use it:
patchset --check patches/ vendor/app # do they still apply? writes nothing
patchset patches/ vendor/app # change / create / same per file
Limits¶
- Patches must be safe to rerun: check for the applied state first, or the second run fails on a missing anchor.
- All or nothing is per run, not a transaction: the final renames are one per file, so a power cut between two leaves some applied. Idempotent patches make a rerun finish the job.
replacewrites the patch's bytes with its header, so it suits formats with comments.
Code¶
methods/patchset/ in the methods repository: the script, a README and a
50-check test. Tried on a copy of a real npm package: patched, loaded with the
change, and a rerun showed why patches must check their own applied state.