#!/usr/bin/env python3 import re import sys def filter_change_block( change_lines: list[str], search: str, replace: str ) -> list[str] | None: removed_lines = [] added_lines = [] phase = "minus" for line in change_lines: if line.startswith("-"): if phase != "minus": raise ValueError( "Non-consecutive '-' and '+' lines in change block." ) removed_lines.append(line[1:]) continue if line.startswith("+"): phase = "plus" added_lines.append(line[1:]) continue raise ValueError("Unexpected non-change line in change block.") transformed_removed = [ re.sub(search, replace, line) for line in removed_lines ] if transformed_removed == added_lines: return change_lines if len(removed_lines) != len(added_lines): return None kept_lines = [] for removed, added in zip(removed_lines, added_lines): if re.sub(search, replace, removed) == added: kept_lines.append(f"-{removed}") kept_lines.append(f"+{added}") else: kept_lines.append(f" {removed}") if not kept_lines: return None return kept_lines def filter_hunk( hunk_lines: list[str], search: str, replace: str ) -> list[str] | None: if not hunk_lines: return None, False header = hunk_lines[0] body = hunk_lines[1:] output_body: list[str] = [] i = 0 while i < len(body): line = body[i] if line.startswith(("+", "-")): block = [] while i < len(body) and body[i].startswith(("+", "-")): block.append(body[i]) i += 1 kept_block = filter_change_block(block, search, replace) if kept_block: output_body.extend(kept_block) continue output_body.append(line) i += 1 if not any(line.startswith(("+", "-")) for line in output_body): return None return [header] + output_body def main() -> None: if len(sys.argv) != 3: raise SystemExit("Usage: mechanicaldiff ") search = sys.argv[1] replace = sys.argv[2] lines = sys.stdin.read().splitlines(keepends=True) preamble_lines = [] sections = [] current = None for line in lines: if line.startswith("diff --git "): if current is not None: sections.append(current) current = {"header": [line], "hunks": []} continue if current is None: preamble_lines.append(line) continue if line.startswith("@@ "): current["hunks"].append([line]) else: if current["hunks"]: current["hunks"][-1].append(line) else: current["header"].append(line) if current is not None: sections.append(current) output_lines = [] output_lines.extend(preamble_lines) left_out = False for section in sections: if not section["hunks"]: output_lines.extend(section["header"]) continue kept_hunks = [] for hunk_lines in section["hunks"]: filtered = filter_hunk(hunk_lines, search, replace) if filtered: if filtered != hunk_lines: left_out = True kept_hunks.append(filtered) elif hunk_lines: left_out = True if not kept_hunks: continue output_lines.extend(section["header"]) for hunk_lines in kept_hunks: output_lines.extend(hunk_lines) sys.stdout.write("".join(output_lines)) if left_out: raise SystemExit(1) if __name__ == "__main__": main()