diff options
| author | Loek Le Blansch <loek@pipeframe.xyz> | 2025-10-25 09:13:56 +0200 |
|---|---|---|
| committer | Loek Le Blansch <loek@pipeframe.xyz> | 2025-10-25 09:13:56 +0200 |
| commit | 5aac30996d6f4c99a752e6d1e83573e8962d4eea (patch) | |
| tree | c49acc0355b908a396f1c86cbcf795bd07295d35 | |
| parent | 7b703051076bdec99592a31cd78a83ac79e34fdb (diff) | |
add extended git dif header lines
| -rw-r--r-- | patchtree/context.py | 10 | ||||
| -rw-r--r-- | patchtree/diff.py | 52 | ||||
| -rw-r--r-- | patchtree/patch.py | 19 | ||||
| -rw-r--r-- | patchtree/process.py | 23 |
4 files changed, 75 insertions, 29 deletions
diff --git a/patchtree/context.py b/patchtree/context.py index a91fb36..57200a5 100644 --- a/patchtree/context.py +++ b/patchtree/context.py @@ -33,6 +33,8 @@ class Context: ) def __del__(self): + # patch must have a trailing newline + self.output.write("\n") self.output.flush() if self.options.in_place: @@ -58,6 +60,14 @@ class Context: return None return here.read_bytes().decode() + def get_mode(self, file: str) -> int: + # TODO + return 0 + here = self.fs.joinpath(file) + if not here.exists(): + return 0 + return here.stat().st_mode + def get_fs(self) -> Path: target: str = self.options.target diff --git a/patchtree/diff.py b/patchtree/diff.py index 9058b02..a3442ae 100644 --- a/patchtree/diff.py +++ b/patchtree/diff.py @@ -1,6 +1,16 @@ +from dataclasses import dataclass from difflib import unified_diff +@dataclass +class DiffFile: + content: str | None + mode: int + + def lines(self) -> list[str]: + return (self.content or "").splitlines() + + class Diff: """ The base Diff class just produces a regular diff from the (possibly absent) @@ -10,24 +20,44 @@ class Diff: file: str - content_a: str | None - content_b: str = "" + a: DiffFile + b: DiffFile def __init__(self, file: str): self.file = file def compare(self) -> str: + """ + Generate patch text in "git-diff-files -p" format (see https: + //git-scm.com/docs/diff-format#generate_patch_text_with_p) + """ + a = self.a + b = self.b fromfile = f"a/{self.file}" tofile = f"b/{self.file}" - if self.content_a is None: + assert (a.content or b.content) is not None + + delta = f"diff --git {fromfile} {tofile}\n" + + if a.content is None: fromfile = "/dev/null" - self.content_a = "" + delta += f"new file mode {b.mode:06o}\n" + + if b.content is None: + tofile = "/dev/null" + delta += f"deleted file mode {a.mode:06o}\n" + + if a.content is not None and b.content is not None and a.mode != b.mode: + delta += f"old mode {a.mode:06o}\n" + delta += f"new mode {b.mode:06o}\n" + + lines_a = a.lines() + lines_b = b.lines() + diff = unified_diff(lines_a, lines_b, fromfile, tofile, lineterm="") + delta += "".join(f"{line}\n" for line in diff) - a = self.content_a.splitlines() - b = self.content_b.splitlines() - diff = unified_diff(a, b, fromfile, tofile, lineterm="") - return "\n".join(diff) + "\n" + return delta def diff(self) -> str: return self.compare() @@ -41,12 +71,12 @@ class IgnoreDiff(Diff): """ def diff(self): - lines_a = (self.content_a or "").splitlines() - lines_b = self.content_b.splitlines() + lines_a = self.a.lines() + lines_b = self.b.lines() add_lines = set(lines_b) - set(lines_a) - self.content_b = "\n".join( + self.b.content = "\n".join( ( *lines_a, *add_lines, diff --git a/patchtree/patch.py b/patchtree/patch.py index 415cf26..0b1f69c 100644 --- a/patchtree/patch.py +++ b/patchtree/patch.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING from pathlib import Path -from .diff import Diff +from .diff import Diff, DiffFile if TYPE_CHECKING: from .process import Process @@ -63,17 +63,18 @@ class Patch: diff = diff_class(self.file) - # read file A contents - content_a = context.get_content(self.file) + diff.a = DiffFile( + content=context.get_content(self.file), + mode=context.get_mode(self.file), + ) - # read file B contents - content_b = self.patch.read_text() + diff.b = DiffFile( + content=self.patch.read_text(), + mode=self.patch.stat().st_mode, + ) for processor_class in processor_classes: processor = processor_class(context) - content_b = processor.transform(content_a, content_b) - - diff.content_a = content_a - diff.content_b = content_b + diff.b = processor.transform(diff.a, diff.b) delta = diff.diff() context.output.write(delta) diff --git a/patchtree/process.py b/patchtree/process.py index b8b87bd..a44886d 100644 --- a/patchtree/process.py +++ b/patchtree/process.py @@ -6,6 +6,8 @@ from jinja2 import Environment from subprocess import Popen from pathlib import Path +from .diff import DiffFile + if TYPE_CHECKING: from .context import Context @@ -16,8 +18,8 @@ class Process: def __init__(self, context: Context): self.context = context - def transform(self, content_a: str | None, content_b: str) -> str: - return content_b + def transform(self, a: DiffFile, b: DiffFile) -> DiffFile: + return b class ProcessJinja2(Process): @@ -26,20 +28,23 @@ class ProcessJinja2(Process): lstrip_blocks=True, ) - def transform(self, content_a, content_b) -> str: + def transform(self, a, b): template_vars = self.get_template_vars() - return self.environment.from_string(content_b).render(**template_vars) + assert b.content is not None + b.content = self.environment.from_string(b.content).render(**template_vars) + return b def get_template_vars(self) -> dict[str, Any]: return {} class ProcessCoccinelle(Process): - def transform(self, content_a, content_b) -> str: - content_a = content_a or "" + def transform(self, a, b): + content_a = a.content or "" + content_b = b.content or "" if len(content_b.strip()) == 0: - return content_a + return a temp_a = Path(mkstemp()[1]) temp_b = Path(mkstemp()[1]) @@ -60,10 +65,10 @@ class ProcessCoccinelle(Process): coccinelle = Popen(cmd) coccinelle.wait() - content_b = temp_b.read_text() + b.content = temp_b.read_text() temp_a.unlink() temp_b.unlink() temp_sp.unlink() - return content_b + return b |