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/defs.py b/tools/git-if-needed/python/gifn_apply/defs.py
new file mode 100644
index 0000000..32b13e8
--- /dev/null
+++ b/tools/git-if-needed/python/gifn_apply/defs.py
@@ -0,0 +1,52 @@
+# SPDX-FileCopyrightText: StorPool <support@storpool.com>
+# SPDX-License-Identifier: BSD-2-Clause
+"""Common definitions for the gifn-apply routines."""
+
+from __future__ import annotations
+
+import dataclasses
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    import logging
+    import pathlib
+    import urllib.parse as uparse
+
+
+VERSION = "0.1.0"
+
+
+class GApplyError(Exception):
+    """The base class for errors that occurred during the gifn-apply operation."""
+
+
+@dataclasses.dataclass(frozen=True, order=True)
+class Repo:
+    """A repository split into the origin fragment and the name/path within."""
+
+    origin: str
+    repo: str
+
+    @property
+    def path(self) -> str:
+        """Combine the origin and the repo path."""
+        return f"{self.origin}/{self.repo}"
+
+
+@dataclasses.dataclass(frozen=True)
+class RepoURL:
+    """A parsed URL for a repo base."""
+
+    url: uparse.ParseResult
+
+
+@dataclasses.dataclass(frozen=True)
+class Config:
+    """Runtime configuration for the gifn-apply tool."""
+
+    log: logging.Logger
+    program: pathlib.Path
+    patches: pathlib.Path
+    series: pathlib.Path
+    repo_urls: dict[str, RepoURL]