Skip to content

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 same and keep their date. --check runs 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.
  • replace writes 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.