Files

190 lines
6.2 KiB
Python
Executable File

#!/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()