Add the gifn-apply test tool
Add a tool that checks out the repositories mentioned in
a git-if-needed patch series, runs gif-it-needed, and performs
some checks on its operation. This tool will eventually be used in
a Zuul test job for this repository.
Change-Id: Id02fb7c21f5ab34d9639bf845fcc3961d929b13b
diff --git a/tools/git-if-needed/python/gifn_apply/quilt.py b/tools/git-if-needed/python/gifn_apply/quilt.py
new file mode 100644
index 0000000..3de9049
--- /dev/null
+++ b/tools/git-if-needed/python/gifn_apply/quilt.py
@@ -0,0 +1,88 @@
+# SPDX-FileCopyrightText: StorPool <support@storpool.com>
+# SPDX-License-Identifier: BSD-2-Clause
+"""Parse a quilt patches file, then parse the patches themselves."""
+
+from __future__ import annotations
+
+import dataclasses
+import pathlib
+import re
+
+from . import defs
+
+
+_REPO_PARTS = 2
+
+_RE_CHANGE_ID = re.compile(r"^ \s* Change-Id \s* : \s* (?P<value> I [0-9a-f]+ ) \s* $", re.X)
+
+_RE_DIFF_START = re.compile(r"^ --- [ ]", re.X)
+
+
+class QuiltError(defs.GApplyError):
+ """An error that occurred while parsing the quilt patch structure."""
+
+
+@dataclasses.dataclass(frozen=True)
+class Patch:
+ """A single patch read from the quilt series file."""
+
+ change_id: str
+ filename: str
+ path: pathlib.Path
+ relpath: pathlib.Path
+ repo: defs.Repo
+
+
+def _extract_change_id(patchfile: pathlib.Path) -> str:
+ """Extract the value of the patch's Change-Id trailer."""
+ change_id = None
+ for line in patchfile.read_text(encoding="UTF-8").splitlines():
+ if _RE_DIFF_START.match(line):
+ if change_id is None:
+ raise defs.GApplyError(f"No Change-Id line found in {patchfile}")
+
+ return change_id
+
+ change = _RE_CHANGE_ID.match(line)
+ if change is not None:
+ change_id = change.group("value")
+
+ raise defs.GApplyError(f"No diff start line ('--- ...') found in {patchfile}")
+
+
+def parse_series(cfg: defs.Config) -> tuple[list[Patch], list[defs.Repo]]:
+ """Parse a series file, return a list of patches and a list of repository names."""
+ repos = set()
+
+ def parse_line(sline: str) -> Patch:
+ """Parse a single relative patch filename read from the series file."""
+ fields = sline.split()
+ if len(fields) != 1:
+ raise NotImplementedError(f"quilt patch options not supported yet: {sline!r}")
+ filename = fields[0]
+
+ relpath = pathlib.Path(filename)
+ if (
+ relpath.is_absolute()
+ or len(relpath.parts) <= _REPO_PARTS
+ or any(part.startswith(".") for part in relpath.parts)
+ ):
+ raise QuiltError(f"Invalid patch filename {filename!r} in {cfg.series}")
+
+ repo = defs.Repo(origin=relpath.parts[0], repo="/".join(relpath.parts[1:_REPO_PARTS]))
+ repos.add(repo)
+
+ patchfile = cfg.patches / relpath
+ if not patchfile.is_file():
+ raise QuiltError(f"Need a regular patch file at {patchfile}")
+
+ change_id = _extract_change_id(patchfile)
+ return Patch(
+ change_id=change_id, filename=filename, path=patchfile, relpath=relpath, repo=repo
+ )
+
+ if not cfg.series.is_file():
+ raise QuiltError(f"Need a regular series file at {cfg.series}")
+
+ res = [parse_line(line) for line in cfg.series.read_text(encoding="UTF-8").splitlines()]
+ return res, sorted(repos)