mechanicaldiff/bin/mechanicaldiff.py

139 lines
3.6 KiB
Python
Executable file

#!/usr/bin/env python3
import re
import sys
def filter_change_block(
change_lines: list[str], search: str, replace: str
) -> list[str]:
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 []
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 []
return kept_lines
def filter_hunk(
hunk_lines: list[str], search: str, replace: str
) -> list[str]:
if not hunk_lines:
return []
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 []
return [header] + output_body
def main() -> None:
if len(sys.argv) != 3:
raise SystemExit("Usage: mechanicaldiff <search> <replace>")
search = sys.argv[1]
replace = sys.argv[2]
input_data = sys.stdin.read()
lines = input_data.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)
for section in sections:
kept_hunks = []
for hunk_lines in section["hunks"]:
filtered = filter_hunk(hunk_lines, search, replace)
if filtered:
kept_hunks.append(filtered)
if not kept_hunks:
continue
output_lines.extend(section["header"])
for hunk_lines in kept_hunks:
output_lines.extend(hunk_lines)
output_data = "".join(output_lines)
sys.stdout.write(output_data)
if output_data != input_data:
raise SystemExit(1)
if __name__ == "__main__":
main()