```git-check-assertions run test/mechanicaldiff.bats assert_success git checkout HEAD~ bin run test/mechanicaldiff.bats assert_failure assert_output --partial "not ok 9 keeps matching change with an extra removed line" assert_output --partial "not ok 10 keeps matching change with an extra added line" ```
151 lines
4 KiB
Python
Executable file
151 lines
4 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import re
|
|
import sys
|
|
|
|
|
|
def trim_blank_ends(lines: list[str]) -> list[str]:
|
|
start = 0
|
|
end = len(lines)
|
|
while start < end and lines[start] == "\n":
|
|
start += 1
|
|
while end > start and lines[end - 1] == "\n":
|
|
end -= 1
|
|
return lines[start:end]
|
|
|
|
|
|
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 trim_blank_ends(transformed_removed) == trim_blank_ends(added_lines):
|
|
return change_lines
|
|
|
|
kept_lines = []
|
|
max_len = max(len(removed_lines), len(added_lines))
|
|
for idx in range(max_len):
|
|
removed = removed_lines[idx] if idx < len(removed_lines) else None
|
|
added = added_lines[idx] if idx < len(added_lines) else None
|
|
if removed is None:
|
|
break
|
|
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()
|