#!/usr/bin/env python3 """Minimal `patch` compatible shim (enough for the libwg-go Makefile). Supports: patch [-pN] [-f] [-N] [-r-] [-d DIR] [-s] (diff read from stdin) Handles unified diffs with multiple files/hunks (git-style or plain). """ import sys import os import re def parse_args(argv): strip = 1 dir_ = "." args = list(argv) i = 0 while i < len(args): a = args[i] if a == "-p": strip = int(args[i + 1]); i += 2; continue elif a.startswith("-p") and a[2:].isdigit(): strip = int(a[2:]); i += 1; continue elif a == "-d": dir_ = args[i + 1]; i += 2; continue elif a in ("-f", "-N", "-s", "-t", "-i"): i += 1; continue elif a == "-r-": i += 1; continue elif a.startswith("-"): i += 1; continue else: i += 1 return strip, dir_ def strip_path(p, strip): parts = p.split("/") if strip and len(parts) > strip: parts = parts[strip:] return "/".join(parts) def apply_hunks(lines, hunks): """Apply parsed hunks (list of (old_start, old_lines, new_lines)) to lines, in order.""" out = list(lines) offset = 0 for old_start, old_count, old_content, new_content in hunks: idx = old_start - 1 + offset # 0-based # find match allowing drift best = None for d in range(0, 200): for sign in (1, -1): cand = idx + d * sign if cand < 0 or cand + old_count > len(out): continue if out[cand:cand + old_count] == old_content: best = cand break if best is not None: break if best is None: # try whitespace-insensitive def norm(l): return l.rstrip("\n") for d in range(0, 400): for sign in (1, -1): cand = idx + d * sign if cand < 0 or cand + old_count > len(out): continue if [norm(x) for x in out[cand:cand + old_count]] == [norm(x) for x in old_content]: best = cand break if best is not None: break if best is None: raise SystemExit(f"patch: hunk at line {old_start} failed to match") out[best:best + old_count] = new_content offset += len(new_content) - old_count return out def process_file(text, fname, strip, dir_): # text: the file's section (after "diff --git" or first "---"), up to next file lines = text.splitlines() # find hunks hunks = [] old_content_all = [] # We'll re-parse the raw section for hunk headers i = 0 in_hunk = False old_block = [] new_block = [] old_start = 0 while i < len(lines): ln = lines[i] m = re.match(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", ln) if m: if in_hunk and old_block: old_count = len(old_block) new_count = len(new_block) hunks.append((old_start, old_count, old_block, new_block)) old_start = int(m.group(1)) old_count_expect = int(m.group(2) or 1) new_count_expect = int(m.group(4) or 1) old_block = [] new_block = [] in_hunk = True i += 1 while i < len(lines) and len(old_block) < old_count_expect and len(new_block) < new_count_expect: l = lines[i] if l.startswith("+"): new_block.append(l[1:] + "\n") elif l.startswith("-"): old_block.append(l[1:] + "\n") elif l.startswith(" "): old_block.append(l[1:] + "\n") new_block.append(l[1:] + "\n") elif l.startswith("\\"): pass else: break i += 1 continue i += 1 if in_hunk and old_block: old_count = len(old_block) new_count = len(new_block) hunks.append((old_start, old_count, old_block, new_block)) target = os.path.join(dir_, strip_path(fname, strip)) if not os.path.exists(target): raise SystemExit(f"patch: target {target} not found") with open(target, encoding="utf-8") as f: content = f.read() out = apply_hunks(content.splitlines(True), hunks) with open(target, "w", encoding="utf-8") as f: f.writelines(out) return target def main(): strip, dir_ = parse_args(sys.argv[1:]) data = sys.stdin.read() # split into per-file sections # A new file starts at a line "diff --git " or, for plain unified diffs, at "--- " sections = [] cur = [] for ln in data.splitlines(keepends=True): if ln.startswith("diff --git "): if cur: sections.append("".join(cur)) cur = [ln] elif ln.startswith("--- ") and not any(c.startswith("diff --git") for c in cur): # plain unified diff, new file if cur: sections.append("".join(cur)) cur = [ln] else: cur.append(ln) if cur: sections.append("".join(cur)) for sec in sections: slines = sec.splitlines() fname = None for l in slines: if l.startswith("diff --git "): m = re.match(r"^diff --git (\S+) b/(\S+)", l) if m: fname = m.group(1) # keep the a/ prefix; strip_path removes it break if fname is None: for l in slines: if l.startswith("--- "): p = l[4:].strip() # drop timestamp (last whitespace-separated token if it starts with a date) parts = p.split("\t") p = parts[0].strip() if p == "/dev/null": continue fname = p break if fname is None: continue # section body starts after the header; pass whole section, parser skips headers process_file(sec, fname, strip, dir_) if __name__ == "__main__": main()