diff options
| -rw-r--r-- | patchtree/__init__.py | 4 | ||||
| -rw-r--r-- | patchtree/config.py | 14 | ||||
| -rw-r--r-- | patchtree/diff.py | 51 | ||||
| -rw-r--r-- | patchtree/patch.py | 72 | ||||
| -rw-r--r-- | patchtree/process.py | 88 |
5 files changed, 125 insertions, 104 deletions
diff --git a/patchtree/__init__.py b/patchtree/__init__.py index f2cc78a..2e304a6 100644 --- a/patchtree/__init__.py +++ b/patchtree/__init__.py @@ -1,4 +1,4 @@ from .config import Config, Header -from .diff import Diff, IgnoreDiff +from .diff import Diff, File from .context import Context -from .process import ProcessCoccinelle, ProcessJinja2 +from .process import ProcessCoccinelle, ProcessJinja2, ProcessIdentity, ProcessExec, ProcessMerge diff --git a/patchtree/config.py b/patchtree/config.py index 21cef8b..94b7cc9 100644 --- a/patchtree/config.py +++ b/patchtree/config.py @@ -10,15 +10,11 @@ from .process import * from .diff import * DEFAULT_PROCESSORS: dict[str, type[Process]] = { - "jinja": ProcessJinja2, + "id": ProcessIdentity, "cocci": ProcessCoccinelle, - "smpl": ProcessCoccinelle, - "touch": ProcessTouch, + "jinja": ProcessJinja2, "exec": ProcessExec, -} - -DEFAULT_DIFFS: dict[str, type[Diff]] = { - ".gitignore": IgnoreDiff, + "merge": ProcessMerge, } @@ -89,9 +85,6 @@ class Config: processors: dict[str, type[Process]] = field(default_factory=lambda: DEFAULT_PROCESSORS) """Maps processor specification string to :type:`Process` class type.""" - diff_strategies: dict[str, type[Diff]] = field(default_factory=lambda: DEFAULT_DIFFS) - """Maps processor specification string to :type:`Process` class type.""" - header: type[Header] = Header """Header class type.""" @@ -104,4 +97,3 @@ class Config: def __post_init__(self): self.processors = {**DEFAULT_PROCESSORS, **self.processors} - self.diff_strategies = {**DEFAULT_DIFFS, **self.diff_strategies} diff --git a/patchtree/diff.py b/patchtree/diff.py index fc026bd..7b55353 100644 --- a/patchtree/diff.py +++ b/patchtree/diff.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: @dataclass -class DiffFile: +class File: content: str | None mode: int @@ -19,17 +19,15 @@ class DiffFile: class Diff: """ - The base Diff class just produces a regular diff from the (possibly absent) - original file to the file in the patch input tree. This effectively - overwrites whatever exists in the target sources with the file in the patch - input tree. + Produce a regular diff from the (possibly absent) original file to the file in the patch input tree. This + effectively overwrites whatever exists in the target sources with the file in the patch input tree. """ config: Config file: str - a: DiffFile - b: DiffFile + a: File + b: File def __init__(self, config: Config, file: str): self.config = config @@ -37,7 +35,7 @@ class Diff: def compare(self) -> str: """ - Generate patch text in "git-diff-files -p" format (see + Generate delta in "git-diff-files -p" format (see `<https://git-scm.com/docs/diff-format#generate_patch_text_with_p>`_) """ a = self.a @@ -46,8 +44,6 @@ class Diff: if a == b: return "" - assert (a.content or b.content) is not None - fromfile = f"a/{self.file}" tofile = f"b/{self.file}" @@ -65,35 +61,10 @@ class Diff: 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="", n=self.config.diff_context) - delta += "".join(f"{line}\n" for line in diff) + if a.content != b.content: + lines_a = a.lines() + lines_b = b.lines() + diff = unified_diff(lines_a, lines_b, fromfile, tofile, lineterm="", n=self.config.diff_context) + delta += "".join(f"{line}\n" for line in diff) return delta - - def diff(self) -> str: - return self.compare() - - -class IgnoreDiff(Diff): - """ - IgnoreDiff is ensures all the lines in the patch source are present in the - target. This ensures no duplicate ignore lines are added by applying the - patch. - """ - - def diff(self): - lines_a = self.a.lines() - lines_b = self.b.lines() - - add_lines = set(lines_b) - set(lines_a) - - self.b.content = "\n".join( - ( - *lines_a, - *add_lines, - ) - ) - - return self.compare() diff --git a/patchtree/patch.py b/patchtree/patch.py index 3c50bb9..32d3b7f 100644 --- a/patchtree/patch.py +++ b/patchtree/patch.py @@ -3,10 +3,10 @@ from typing import TYPE_CHECKING from pathlib import Path -from .diff import Diff, DiffFile +from .diff import Diff, File +from .process import Process if TYPE_CHECKING: - from .process import Process from .context import Context from .config import Config @@ -16,63 +16,47 @@ class Patch: patch: Path file: str - file_name: str = "" - file_type: str = "" - processors: list[str] = [] + processors: list[tuple[type[Process], Process.Args]] = [] def __init__(self, config: Config, patch: Path): self.patch = patch self.config = config - self.file_name = patch.name - - # find preprocessors - idx = self.file_name.find(config.process_delimiter) - if idx >= 0: - self.processors = self.file_name[idx:].split(config.process_delimiter) - self.processors = [template.strip() for template in self.processors] - self.processors = [template for template in self.processors if len(template) > 0] - self.processors.reverse() - self.file_name = self.file_name[:idx] - - # save the path to the target file - self.file = str(patch.parent.joinpath(self.file_name)) - - # find and split at file extension - idx = self.file_name.find(".") - if idx >= 0: - self.file_type = self.file_name[idx:] - self.file_name = self.file_name[:idx] - - def get_diff(self) -> type[Diff]: - return self.config.diff_strategies.get(self.file_type, Diff) - - def get_processors(self) -> list[type[Process]]: - processors = [] - for processor in self.processors: - if processor not in self.config.processors: - continue - processors.append(self.config.processors[processor]) - return processors + self.processors.clear() + self.file, *proc_strs = str(patch).split(config.process_delimiter) + for proc_str in proc_strs: + proc_name, *argv = proc_str.split(",") + args = Process.Args(name=proc_name, argv=argv) + proc_cls = config.processors.get(proc_name, None) + if proc_cls is None: + raise Exception(f"unknown processor: `{proc_cls}'") + for arg in argv: + key, value, *_ = (*arg.split("=", 1), None) + args.argd[key] = value + self.processors.insert( + 0, + ( + proc_cls, + args, + ), + ) def write(self, context: Context) -> None: - diff_class = self.get_diff() - processor_classes = self.get_processors() - - diff = diff_class(self.config, self.file) + diff = Diff(self.config, self.file) - diff.a = DiffFile( + diff.a = File( content=context.get_content(self.file), mode=context.get_mode(self.file), ) - diff.b = DiffFile( + diff.b = File( content=self.patch.read_text(), mode=self.patch.stat().st_mode, ) - for processor_class in processor_classes: - processor = processor_class(context) + + for cls, args in self.processors: + processor = cls(context, args) diff.b = processor.transform(diff.a, diff.b) - delta = diff.diff() + delta = diff.compare() context.output.write(delta) diff --git a/patchtree/process.py b/patchtree/process.py index c5cd339..3530307 100644 --- a/patchtree/process.py +++ b/patchtree/process.py @@ -1,13 +1,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Callable from tempfile import mkstemp from jinja2 import Environment from subprocess import Popen, run from pathlib import Path from shlex import split as shell_split +from dataclasses import dataclass, field -from .diff import DiffFile +from .diff import File if TYPE_CHECKING: from .context import Context @@ -21,10 +22,19 @@ class Process: context: Context """Patch file context.""" - def __init__(self, context: Context): + @dataclass + class Args: + name: str + argv: list[str] = field(default_factory=list) + argd: dict[str, str | None] = field(default_factory=dict) + + args: Args + + def __init__(self, context: Context, args: Args): + self.args = args self.context = context - def transform(self, a: DiffFile, b: DiffFile) -> DiffFile: + def transform(self, a: File, b: File) -> File: """ Transform the input file. @@ -45,6 +55,12 @@ class ProcessJinja2(Process): lstrip_blocks=True, ) + def __init__(self, *args, **kwargs): + super(ProcessJinja2, self).__init__(*args, **kwargs) + + if len(self.args.argv) > 0: + raise Exception("too many arguments") + def transform(self, a, b): template_vars = self.get_template_vars() assert b.content is not None @@ -60,6 +76,12 @@ class ProcessCoccinelle(Process): Coccinelle transformer. """ + def __init__(self, *args, **kwargs): + super(ProcessCoccinelle, self).__init__(*args, **kwargs) + + if len(self.args.argv) > 0: + raise Exception("too many arguments") + def transform(self, a, b): content_a = a.content or "" content_b = b.content or "" @@ -95,13 +117,13 @@ class ProcessCoccinelle(Process): return b -class ProcessTouch(Process): +class ProcessIdentity(Process): """ - Touch transformer. + Identity transformer. """ def transform(self, a, b): - return DiffFile(content=a.content, mode=b.mode) + return File(content=a.content, mode=b.mode) class ProcessExec(Process): @@ -109,6 +131,12 @@ class ProcessExec(Process): Executable transformer. """ + def __init__(self, *args, **kwargs): + super(ProcessExec, self).__init__(*args, **kwargs) + + if len(self.args.argv) > 0: + raise Exception("too many arguments") + def transform(self, a, b): assert b.content is not None @@ -127,3 +155,49 @@ class ProcessExec(Process): exec.unlink() return b + + +class ProcessMerge(Process): + """ + Merge transformer. + """ + + def merge_ignore(self, a: File, b: File) -> File: + lines_a = a.lines() + lines_b = b.lines() + + add_lines = set(lines_b) - set(lines_a) + + b.content = "\n".join( + ( + *lines_a, + *add_lines, + ) + ) + + return b + + strategies: dict[str, Callable[[ProcessMerge, File, File], File]] = { + "ignore": merge_ignore, + } + + strategy: Callable[[ProcessMerge, File, File], File] + + def __init__(self, *args, **kwargs): + super(ProcessMerge, self).__init__(*args, **kwargs) + + argv = self.args.argv + if len(argv) < 1: + raise Exception("not enough arguments") + + if len(argv) > 1: + raise Exception("too many arguments") + + strategy = argv[0] + if strategy not in self.strategies: + raise Exception(f"unknown merge strategy: `{strategy}'") + + self.strategy = self.strategies[strategy] + + def transform(self, a, b): + return self.strategy(self, a, b) |