#!/usr/bin/env python3 import re import sys def filter_change_block( change_lines: list[str], search: str, replace: str ) -> tuple[list[str] | None, bool]: 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, False if len(removed_lines) != len(added_lines): return None, True 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, True return kept_lines, True def filter_hunk( hunk_lines: list[str], search: str, replace: str ) -> tuple[list[str] | None, bool]: if not hunk_lines: return None, False header = hunk_lines[0] body = hunk_lines[1:] output_body: list[str] = [] left_out = False 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, block_left_out = filter_change_block( block, search, replace ) if kept_block: output_body.extend(kept_block) else: left_out = True if block_left_out: left_out = True continue output_body.append(line) i += 1 if not any(line.startswith(("+", "-")) for line in output_body): return None, True return [header] + output_body, left_out 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, hunk_left_out = filter_hunk( hunk_lines, search, replace ) if filtered: kept_hunks.append(filtered) else: left_out = True if hunk_left_out: 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()