"""Text diffs between stored versions or a version and the working copy.""" from __future__ import annotations import difflib import re from typing import Any _WS = re.compile(r"\s+") def _lines(content: bytes) -> list[str]: return content.decode("utf-8", errors="replace").splitlines(keepends=True) def _normalize(line: str, ignore_whitespace: bool, ignore_eol: bool) -> str: if ignore_whitespace: return _WS.sub(" ", line).strip() if ignore_eol: return line.rstrip("\r\n") return line def hunks( a: bytes, b: bytes, context: int = 3, ignore_whitespace: bool = False, ignore_eol: bool = False, ) -> list[dict[str, Any]]: a_lines, b_lines = _lines(a), _lines(b) a_norm = [_normalize(x, ignore_whitespace, ignore_eol) for x in a_lines] b_norm = [_normalize(x, ignore_whitespace, ignore_eol) for x in b_lines] matcher = difflib.SequenceMatcher(None, a_norm, b_norm, autojunk=False) result = [] for group in matcher.get_grouped_opcodes(context): first, last = group[0], group[-1] lines = [] for tag, i1, i2, j1, j2 in group: if tag == "equal": lines += [{"op": " ", "text": x} for x in a_lines[i1:i2]] continue if tag in ("replace", "delete"): lines += [{"op": "-", "text": x} for x in a_lines[i1:i2]] if tag in ("replace", "insert"): lines += [{"op": "+", "text": x} for x in b_lines[j1:j2]] result.append({ "old_start": first[1] + 1, "old_lines": last[2] - first[1], "new_start": first[3] + 1, "new_lines": last[4] - first[3], "lines": lines, }) return result def unified(hunk_list: list[dict[str, Any]], from_label: str, to_label: str) -> str: if not hunk_list: return "" out = [f"--- {from_label}\n", f"+++ {to_label}\n"] for h in hunk_list: out.append(f"@@ -{h['old_start']},{h['old_lines']} +{h['new_start']},{h['new_lines']} @@\n") for line in h["lines"]: text = line["text"] out.append(line["op"] + text + ("" if text.endswith("\n") else "\n\\ No newline at end of file\n")) return "".join(out) def side_by_side_html(a: bytes, b: bytes, from_label: str, to_label: str, context: int = 3) -> str: return difflib.HtmlDiff(wrapcolumn=100).make_file( _lines(a), _lines(b), from_label, to_label, context=True, numlines=context )