aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.editorconfig5
-rw-r--r--.pre-commit-config.yaml1
-rw-r--r--doc/api/index.rst17
-rw-r--r--doc/conf.py34
-rw-r--r--doc/dev/diff.rst5
-rw-r--r--doc/dev/index.rst72
-rw-r--r--doc/dev/processor.rst11
-rw-r--r--doc/index.rst11
-rw-r--r--doc/user/index.rst38
-rw-r--r--makefile5
-rw-r--r--patchtree/config.py36
-rw-r--r--patchtree/diff.py19
-rw-r--r--patchtree/patch.py4
-rw-r--r--patchtree/process.py30
-rw-r--r--pyproject.toml3
-rw-r--r--readme.md1
-rw-r--r--readme.rst18
17 files changed, 288 insertions, 22 deletions
diff --git a/.editorconfig b/.editorconfig
index 22da3f7..c4d6fb1 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -3,8 +3,11 @@ root = true
[*]
end_of_line = lf
insert_final_newline = true
-max_line_length = 80
+max_line_length = 110
[*.py]
indent_size = 4
indent_style = space
+
+[*.rst]
+max_line_length = unset
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b21ca20..700c8aa 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,6 +4,7 @@ repos:
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
+ exclude_types: [markdown]
- id: mixed-line-ending
args: [--fix=lf]
- repo: https://github.com/codespell-project/codespell
diff --git a/doc/api/index.rst b/doc/api/index.rst
new file mode 100644
index 0000000..b4678f8
--- /dev/null
+++ b/doc/api/index.rst
@@ -0,0 +1,17 @@
+API reference
+=============
+
+.. automodule:: patchtree.config
+ :members:
+
+.. automodule:: patchtree.process
+ :members:
+
+.. automodule:: patchtree.diff
+ :members:
+
+.. automodule:: patchtree.patch
+ :members:
+
+.. automodule:: patchtree.context
+ :members:
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 0000000..07823d6
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,34 @@
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+from os import environ
+from pathlib import Path
+from tomllib import loads as toml_loads
+import sys
+
+repo_root = Path(__file__).parent.parent
+
+sys.path.insert(0, str(repo_root.resolve()))
+import patchtree
+
+project = "patchtree"
+release = "???"
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx_automodapi.automodapi",
+]
+templates_path = []
+exclude_patterns = []
+html_theme = "alabaster"
+html_static_path = []
+autodoc_mock_imports = [project]
+
+try:
+ toml = repo_root.joinpath("pyproject.toml").read_text()
+ pyproject = toml_loads(toml)
+ release = pyproject["project"]["version"]
+except:
+ pass
+
+if "RENESAS_INTERNAL" in environ:
+ extensions.append("lpc_cs_sphinx_renesas_theme")
+ html_theme = "lpc_cs_sphinx_renesas_theme"
diff --git a/doc/dev/diff.rst b/doc/dev/diff.rst
new file mode 100644
index 0000000..6875ed6
--- /dev/null
+++ b/doc/dev/diff.rst
@@ -0,0 +1,5 @@
+Default
+*******
+
+Include
+*******
diff --git a/doc/dev/index.rst b/doc/dev/index.rst
new file mode 100644
index 0000000..6932fa9
--- /dev/null
+++ b/doc/dev/index.rst
@@ -0,0 +1,72 @@
+Developer docs
+==============
+
+Installation
+************
+
+.. note::
+
+ Patchtree is developed and tested on \*NIX-like operating systems.
+ Windows compatibility is not tested or guaranteed.
+ The output .patch files can of course be used anywhere where ``git apply`` works.
+
+Make sure you have the following dependencies installed:
+
+* Python 3 and pip
+* Coccinelle (optional)
+
+Installation can be done by cloning this repository and running
+
+.. code::
+
+ $ pip install .
+
+This will install any missing Python dependencies automatically as well.
+
+Generating clean patches
+************************
+
+In order to generate a clean .patch file, patchtree needs
+
+* the original source tree (either as a folder or .zip file)
+* a list of patch files placed in the same structure as the original source tree
+
+Writing patch sources
+*********************
+
+
+
+.. _config:
+
+Configuration
+*************
+
+.. currentmodule:: patchtree.config
+
+The configuration file is a Python file sourced from ``ptconfig.py`` relative to the current working directory when executing the ``patchtree`` command.
+This file can contain arbitrary Python code.
+Certain global variables influence the behavior of patchtree when defined.
+These variables are the member variables of the :class:`Config` class.
+
+Processors
+**********
+
+.. currentmodule:: patchtree.process
+
+The processors included with patchtree are listed below.
+Custom processors can be created by inheriting from the base :py:class:`Process` class and registering through the `configuration file <config_>`_.
+
+.. toctree::
+ :maxdepth: 1
+
+ processor.rst
+
+Diff engines
+************
+
+The diff engines included with patchtree are listed below.
+
+.. toctree::
+ :maxdepth: 1
+
+ diff.rst
diff --git a/doc/dev/processor.rst b/doc/dev/processor.rst
new file mode 100644
index 0000000..f577220
--- /dev/null
+++ b/doc/dev/processor.rst
@@ -0,0 +1,11 @@
+Touch
+*****
+
+Coccinelle
+**********
+
+Jinja template
+**************
+
+Executable
+**********
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000..8390160
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,11 @@
+.. include:: ../readme.rst
+
+Usage
+*****
+
+.. toctree::
+ :maxdepth: 2
+
+ user/index.rst
+ dev/index.rst
+ api/index.rst
diff --git a/doc/user/index.rst b/doc/user/index.rst
new file mode 100644
index 0000000..2d311c6
--- /dev/null
+++ b/doc/user/index.rst
@@ -0,0 +1,38 @@
+User docs
+=========
+
+.. note::
+
+ By convention, the patch file should be placed in the root of the target directory under the filename ``.patchtree.diff``.
+ This allows you to easily revert and/or upgrade the patch later.
+
+.. important::
+
+ If you keep the target directory under version control, make sure the repository root is the same as the root of the patch's target directory.
+ Patches produced by patchtree contain *extended header lines* which are be interpreted by ``git apply``.
+ Because these header lines must include the path to each modified file relative to the repository root, any files which don't exist at the expected location will be skipped silently by ``git``.
+
+Applying a patch
+****************
+
+To apply patches output by patchtree, download the ``.patch`` file and place it in the directory where it should apply the changes under the name ``.patchtree.diff``.
+
+To apply the patch, run the following command in the target directory::
+
+ $ git apply .patchtree.diff
+
+Reverting a patch
+*****************
+
+To revert the changes of a patch, run the following command in the target directory::
+
+ $ git apply --reverse .patchtree.diff
+
+Upgrading a patch
+*****************
+
+Upgrading a patch consists of
+
+#. reverting the current (old) patch
+#. downloading and replacing the ``.patchtree.diff`` file with the new patch
+#. reapplying the patch file
diff --git a/makefile b/makefile
new file mode 100644
index 0000000..30468df
--- /dev/null
+++ b/makefile
@@ -0,0 +1,5 @@
+.PHONY: FORCE
+
+# sphinx-apidoc -o doc/api patchtree
+docs: FORCE
+ sphinx-build -b html doc build/html
diff --git a/patchtree/config.py b/patchtree/config.py
index 196ecb1..21cef8b 100644
--- a/patchtree/config.py
+++ b/patchtree/config.py
@@ -23,6 +23,10 @@ DEFAULT_DIFFS: dict[str, type[Diff]] = {
class Header:
+ """
+ Patch output header.
+ """
+
config: Config
context: Context
@@ -64,19 +68,39 @@ class Header:
@dataclass
class Config:
+ """
+ Configuration dataclass.
+
+ This class contains all configuration options read from the :ref: `configuration file <config>`.
+ """
+
context: type[Context] = Context
+ """Context class type."""
+
patch: type[Patch] = Patch
+ """Patch class type."""
+
argument_parser: type[ArgumentParser] = ArgumentParser
+ """ArgumentParser class type."""
+
process_delimiter: str = "#"
- processors: dict[str, type[Process]] = field(
- default_factory=lambda: DEFAULT_PROCESSORS
- )
- diff_strategies: dict[str, type[Diff]] = field(
- default_factory=lambda: DEFAULT_DIFFS
- )
+ """String used to delimit processors in patch source filenames."""
+
+ 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."""
+
diff_context: int = 3
+ """Lines of context to include in the default diffs."""
+
output_shebang: bool = False
+ """Whether to output a shebang line with the ``git patch`` command to apply
+ the patch."""
def __post_init__(self):
self.processors = {**DEFAULT_PROCESSORS, **self.processors}
diff --git a/patchtree/diff.py b/patchtree/diff.py
index 3982296..fc026bd 100644
--- a/patchtree/diff.py
+++ b/patchtree/diff.py
@@ -20,8 +20,9 @@ class DiffFile:
class Diff:
"""
The base Diff class just produces a regular diff from the (possibly absent)
- SDK10 file. This effectively adds a new file or replaces the SDK10 source file
- with the file in the patch directory.
+ 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
@@ -36,8 +37,8 @@ class Diff:
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)
+ 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
@@ -66,9 +67,7 @@ class Diff:
lines_a = a.lines()
lines_b = b.lines()
- diff = unified_diff(
- lines_a, lines_b, fromfile, tofile, lineterm="", n=self.config.diff_context
- )
+ 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
@@ -79,9 +78,9 @@ class Diff:
class IgnoreDiff(Diff):
"""
- IgnoreDiff is slightly different and is used to ensure all the lines in the
- patch source ignore file are present in the SDK version. This ensures no
- duplicate ignore lines exist after patching.
+ 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):
diff --git a/patchtree/patch.py b/patchtree/patch.py
index d2d0ae8..3c50bb9 100644
--- a/patchtree/patch.py
+++ b/patchtree/patch.py
@@ -31,9 +31,7 @@ class Patch:
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 = [template for template in self.processors if len(template) > 0]
self.processors.reverse()
self.file_name = self.file_name[:idx]
diff --git a/patchtree/process.py b/patchtree/process.py
index e4d0162..eb20b17 100644
--- a/patchtree/process.py
+++ b/patchtree/process.py
@@ -14,16 +14,32 @@ if TYPE_CHECKING:
class Process:
+ """
+ Processor base interface.
+ """
+
context: Context
+ """Patch file context."""
def __init__(self, context: Context):
self.context = context
def transform(self, a: DiffFile, b: DiffFile) -> DiffFile:
- return b
+ """
+ Transform the input file.
+
+ :param a: content of file to patch
+ :param b: content of patch input (in patch tree)
+ :returns: processed file
+ """
+ raise NotImplementedError()
class ProcessJinja2(Process):
+ """
+ Jinja2 preprocessor.
+ """
+
environment: Environment = Environment(
trim_blocks=True,
lstrip_blocks=True,
@@ -40,6 +56,10 @@ class ProcessJinja2(Process):
class ProcessCoccinelle(Process):
+ """
+ Coccinelle transformer.
+ """
+
def transform(self, a, b):
content_a = a.content or ""
content_b = b.content or ""
@@ -76,11 +96,19 @@ class ProcessCoccinelle(Process):
class ProcessTouch(Process):
+ """
+ Touch transformer.
+ """
+
def transform(self, a, b):
return DiffFile(content=a.content, mode=b.mode)
class ProcessExec(Process):
+ """
+ Executable transformer.
+ """
+
def transform(self, a, b):
assert b.content is not None
diff --git a/pyproject.toml b/pyproject.toml
index 9097d79..2835ae7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,3 +22,6 @@ Homepage = "https://bitbucket.global.renesas.com/users/loek.le-blansch.pv_renesa
[project.scripts]
patchtree = "patchtree.cli:main"
+
+[tool.black]
+line-length = 110
diff --git a/readme.md b/readme.md
deleted file mode 100644
index 1820122..0000000
--- a/readme.md
+++ /dev/null
@@ -1 +0,0 @@
-# patchtree
diff --git a/readme.rst b/readme.rst
new file mode 100644
index 0000000..98fe56b
--- /dev/null
+++ b/readme.rst
@@ -0,0 +1,18 @@
+::
+
+ ┏━━━━━━━━━━━━━━━━━━┓
+ ┃ original sources ┠─┐
+ ┗━┯━━━━━━━━━━━━━━━━┛ │
+ └──────────────────┘
+ ┏━━━━━━━━━━━━━┓ ╔═════════════════╗ ┏━━━━━━━━━━━━━━━┓
+ ┃ new sources ┠─┐ ║ ║ ┃ clean patches ┠─┐
+ ┗━┯━━━━━━━━━━━┛ │ ━━━━🭬 ║ PATCHTREE ║ ━━━━🭬 ┗━┯━━━━━━━━━━━━━┛ ├─┐
+ └─────────────┘ ║ ║ └─┬─────────────┘ │
+ ┏━━━━━━━━━━━━━━━━━━┓ ╚═════════════════╝ └───────────────┘
+ ┃ semantic patches ┠─┐
+ ┗━┯━━━━━━━━━━━━━━━━┛ │
+ └──────────────────┘
+
+Patchtree is a tool for generating clean patches for external source trees.
+It allows both patch sources to be maintained separately from the sources they apply to, and templating or scripting of the patches themselves in order to be adjust to variations in the external source tree.
+This makes it a useful for automating the process of backporting bugfixes or adding functionality to existing software releases.