diff --git a/docs/ahriman.1 b/docs/ahriman.1
index ba13df92..7b489aab 100644
--- a/docs/ahriman.1
+++ b/docs/ahriman.1
@@ -3,7 +3,7 @@
ahriman
.SH SYNOPSIS
.B ahriman
-[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ...
+[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ...
.SH DESCRIPTION
ArcH linux ReposItory MANager
@@ -71,7 +71,7 @@ remove package status
update package status
.TP
\fBahriman\fR \fI\,patch-add\/\fR
-add patch set
+add patch for PKGBUILD function
.TP
\fBahriman\fR \fI\,patch-list\/\fR
list patch sets
@@ -79,6 +79,9 @@ list patch sets
\fBahriman\fR \fI\,patch-remove\/\fR
remove patch set
.TP
+\fBahriman\fR \fI\,patch-set-add\/\fR
+add patch set
+.TP
\fBahriman\fR \fI\,repo-backup\/\fR
backup repository data
.TP
@@ -284,21 +287,25 @@ set status for specified packages. If no packages supplied, service status will
new status
.SH COMMAND \fI\,'ahriman patch-add'\/\fR
-usage: ahriman patch-add [-h] [-t TRACK] package
+usage: ahriman patch-add [-h] [-p PATCH] package variable
-create or update source patches
+create or update patched PKGBUILD function or variable
.TP
\fBpackage\fR
-path to directory with changed files for patch addition/update
+package base
+
+.TP
+\fBvariable\fR
+PKGBUILD variable or function name. If variable is a function, it must end with ()
.SH OPTIONS \fI\,'ahriman patch-add'\/\fR
.TP
-\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR
-files which has to be tracked
+\fB\-p\fR \fI\,PATCH\/\fR, \fB\-\-patch\fR \fI\,PATCH\/\fR
+path to file which contains function or variable value. If not set, the value will be read from stdin
.SH COMMAND \fI\,'ahriman patch-list'\/\fR
-usage: ahriman patch-list [-h] [-e] [package]
+usage: ahriman patch-list [-h] [-e] [-v VARIABLE] [package]
list available patches for the package
@@ -311,8 +318,12 @@ package base
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
+.TP
+\fB\-v\fR \fI\,VARIABLE\/\fR, \fB\-\-variable\fR \fI\,VARIABLE\/\fR
+if set, show only patches for specified PKGBUILD variables
+
.SH COMMAND \fI\,'ahriman patch-remove'\/\fR
-usage: ahriman patch-remove [-h] package
+usage: ahriman patch-remove [-h] [-v VARIABLE] package
remove patches for the package
@@ -320,6 +331,26 @@ remove patches for the package
\fBpackage\fR
package base
+.SH OPTIONS \fI\,'ahriman patch-remove'\/\fR
+.TP
+\fB\-v\fR \fI\,VARIABLE\/\fR, \fB\-\-variable\fR \fI\,VARIABLE\/\fR
+should be used for single\-function patches in case if you wold like to remove only specified PKGBUILD variables. In case
+if not set, it will remove all patches related to the package
+
+.SH COMMAND \fI\,'ahriman patch-set-add'\/\fR
+usage: ahriman patch-set-add [-h] [-t TRACK] package
+
+create or update source patches
+
+.TP
+\fBpackage\fR
+path to directory with changed files for patch addition/update
+
+.SH OPTIONS \fI\,'ahriman patch-set-add'\/\fR
+.TP
+\fB\-t\fR \fI\,TRACK\/\fR, \fB\-\-track\fR \fI\,TRACK\/\fR
+files which has to be tracked
+
.SH COMMAND \fI\,'ahriman repo-backup'\/\fR
usage: ahriman repo-backup [-h] path
diff --git a/docs/ahriman.core.database.migrations.rst b/docs/ahriman.core.database.migrations.rst
index 08dc66fb..9fd425af 100644
--- a/docs/ahriman.core.database.migrations.rst
+++ b/docs/ahriman.core.database.migrations.rst
@@ -28,6 +28,14 @@ ahriman.core.database.migrations.m002\_user\_access module
:no-undoc-members:
:show-inheritance:
+ahriman.core.database.migrations.m003\_patch\_variables module
+--------------------------------------------------------------
+
+.. automodule:: ahriman.core.database.migrations.m003_patch_variables
+ :members:
+ :no-undoc-members:
+ :show-inheritance:
+
Module contents
---------------
diff --git a/docs/ahriman.core.formatters.rst b/docs/ahriman.core.formatters.rst
index c3ecda6d..d3b2a849 100644
--- a/docs/ahriman.core.formatters.rst
+++ b/docs/ahriman.core.formatters.rst
@@ -36,6 +36,14 @@ ahriman.core.formatters.package\_printer module
:no-undoc-members:
:show-inheritance:
+ahriman.core.formatters.patch\_printer module
+---------------------------------------------
+
+.. automodule:: ahriman.core.formatters.patch_printer
+ :members:
+ :no-undoc-members:
+ :show-inheritance:
+
ahriman.core.formatters.printer module
--------------------------------------
diff --git a/docs/faq.rst b/docs/faq.rst
index 49503eff..99af90d8 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -162,11 +162,13 @@ Unlike ``RemotePullTrigger`` trigger, the ``RemotePushTrigger`` more likely will
But I just wanted to change PKGBUILD from AUR a bit!
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Well it is supported also.
+Well it is supported also. The recommended way is to patch specific function, e.g. by running ``sudo -u ahriman ahriman patch-add ahriman version``. This command will prompt for new value of the PKGBUILD variable ``version``. You can also write it to file and read from it ``sudo -u ahriman ahriman patch-add ahriman version version.patch``.
+
+Alternatively you can create full-diff patches, which are calculated by using ``git diff`` from current PKGBUILD master branch:
#. Clone sources from AUR.
#. Make changes you would like to (e.g. edit ``PKGBUILD``, add external patches).
-#. Run ``sudo -u ahriman ahriman patch-add /path/to/local/directory/with/PKGBUILD``.
+#. Run ``sudo -u ahriman ahriman patch-set-add /path/to/local/directory/with/PKGBUILD``.
The last command will calculate diff from current tree to the ``HEAD`` and will store it locally. Patches will be applied on any package actions (e.g. it can be used for dependency management).
diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py
index 03d79372..7861c971 100644
--- a/src/ahriman/application/ahriman.py
+++ b/src/ahriman/application/ahriman.py
@@ -93,6 +93,7 @@ def _parser() -> argparse.ArgumentParser:
_set_patch_add_parser(subparsers)
_set_patch_list_parser(subparsers)
_set_patch_remove_parser(subparsers)
+ _set_patch_set_add_parser(subparsers)
_set_repo_backup_parser(subparsers)
_set_repo_check_parser(subparsers)
_set_repo_clean_parser(subparsers)
@@ -320,7 +321,7 @@ def _set_package_status_update_parser(root: SubParserAction) -> argparse.Argumen
def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
- add parser for new patch subcommand
+ add parser for new single-function patch subcommand
Args:
root(SubParserAction): subparsers for the commands
@@ -328,16 +329,18 @@ def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
Returns:
argparse.ArgumentParser: created argument parser
"""
- parser = root.add_parser("patch-add", help="add patch set", description="create or update source patches",
- epilog="In order to add a patch set for the package you will need to clone "
- "the AUR package manually, add required changes (e.g. external patches, "
- "edit PKGBUILD) and run command, e.g. ``ahriman patch path/to/directory``. "
- "By default it tracks *.patch and *.diff files, but this behavior can be changed "
- "by using --track option",
+ parser = root.add_parser("patch-add", help="add patch for PKGBUILD function",
+ description="create or update patched PKGBUILD function or variable",
+ epilog="Unlike ``patch-set-add``, this function allows to patch only one PKGBUILD f"
+ "unction, e.g. typing ``ahriman patch-add ahriman version`` it will change the "
+ "``version`` inside PKGBUILD, typing ``ahriman patch-add ahriman build()`` "
+ "it will change ``build()`` function inside PKGBUILD",
formatter_class=_formatter)
- parser.add_argument("package", help="path to directory with changed files for patch addition/update")
- parser.add_argument("-t", "--track", help="files which has to be tracked", action="append",
- default=["*.diff", "*.patch"])
+ parser.add_argument("package", help="package base")
+ parser.add_argument("variable", help="PKGBUILD variable or function name. If variable is a function, "
+ "it must end with ()")
+ parser.add_argument("patch", help="path to file which contains function or variable value. If not set, "
+ "the value will be read from stdin", type=Path, nargs="?")
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True)
return parser
@@ -356,6 +359,8 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="list available patches for the package", formatter_class=_formatter)
parser.add_argument("package", help="package base", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
+ parser.add_argument("-v", "--variable", help="if set, show only patches for specified PKGBUILD variables",
+ action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, no_report=True)
return parser
@@ -373,10 +378,39 @@ def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("patch-remove", help="remove patch set", description="remove patches for the package",
formatter_class=_formatter)
parser.add_argument("package", help="package base")
+ parser.add_argument("-v", "--variable", help="should be used for single-function patches in case if you wold like "
+ "to remove only specified PKGBUILD variables. In case if not set, "
+ "it will remove all patches related to the package",
+ action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, no_report=True)
return parser
+def _set_patch_set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
+ """
+ add parser for new full-diff patch subcommand
+
+ Args:
+ root(SubParserAction): subparsers for the commands
+
+ Returns:
+ argparse.ArgumentParser: created argument parser
+ """
+ parser = root.add_parser("patch-set-add", help="add patch set", description="create or update source patches",
+ epilog="In order to add a patch set for the package you will need to clone "
+ "the AUR package manually, add required changes (e.g. external patches, "
+ "edit PKGBUILD) and run command, e.g. ``ahriman patch-set-add path/to/directory``. "
+ "By default it tracks *.patch and *.diff files, but this behavior can be changed "
+ "by using --track option",
+ formatter_class=_formatter)
+ parser.add_argument("package", help="path to directory with changed files for patch addition/update", type=Path)
+ parser.add_argument("-t", "--track", help="files which has to be tracked", action="append",
+ default=["*.diff", "*.patch"])
+ parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, no_report=True,
+ variable=None)
+ return parser
+
+
def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository backup subcommand
diff --git a/src/ahriman/application/handlers/patch.py b/src/ahriman/application/handlers/patch.py
index cd9cc432..8d1f34d1 100644
--- a/src/ahriman/application/handlers/patch.py
+++ b/src/ahriman/application/handlers/patch.py
@@ -18,17 +18,19 @@
# along with this program. If not, see .
#
import argparse
+import sys
from pathlib import Path
-from typing import List, Optional, Type
+from typing import List, Optional, Tuple, Type
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
-from ahriman.core.formatters import StringPrinter
+from ahriman.core.formatters import PatchPrinter
from ahriman.models.action import Action
from ahriman.models.package import Package
+from ahriman.models.pkgbuild_patch import PkgbuildPatch
class Patch(Handler):
@@ -52,51 +54,93 @@ class Patch(Handler):
application = Application(architecture, configuration, no_report, unsafe)
application.on_start()
- if args.action == Action.List:
- Patch.patch_set_list(application, args.package, args.exit_code)
+ if args.action == Action.Update and args.variable is not None:
+ patch = Patch.patch_create_from_function(args.variable, args.patch)
+ Patch.patch_set_create(application, args.package, patch)
+ elif args.action == Action.Update and args.variable is None:
+ package_base, patch = Patch.patch_create_from_diff(args.package, args.track)
+ Patch.patch_set_create(application, package_base, patch)
+ elif args.action == Action.List:
+ Patch.patch_set_list(application, args.package, args.variable, args.exit_code)
elif args.action == Action.Remove:
- Patch.patch_set_remove(application, args.package)
- elif args.action == Action.Update:
- Patch.patch_set_create(application, Path(args.package), args.track)
+ Patch.patch_set_remove(application, args.package, args.variable)
@staticmethod
- def patch_set_create(application: Application, sources_dir: Path, track: List[str]) -> None:
+ def patch_create_from_diff(sources_dir: Path, track: List[str]) -> Tuple[str, PkgbuildPatch]:
+ """
+ create PKGBUILD plain diff patches from sources directory
+
+ Args:
+ sources_dir(Path): path to directory with the package sources
+ track(List[str]): track files which match the glob before creating the patch
+
+ Returns:
+ Tuple[str, PkgbuildPatch]: package base and created PKGBUILD patch based on the diff from master HEAD
+ to current files
+ """
+ package = Package.from_build(sources_dir)
+ patch = Sources.patch_create(sources_dir, *track)
+ return package.base, PkgbuildPatch(None, patch)
+
+ @staticmethod
+ def patch_create_from_function(variable: str, patch_path: Optional[Path]) -> PkgbuildPatch:
+ """
+ create single-function patch set for the package base
+
+ Args:
+ variable(str): function or variable name inside PKGBUILD
+ patch_path(Path): optional path to patch content. If not set, it will be read from stdin
+
+ Returns:
+ PkgbuildPatch: created patch for the PKGBUILD function
+ """
+ if patch_path is None:
+ print("Post new function or variable value below. Press Ctrl-D to finish:", file=sys.stderr)
+ patch = "".join(list(sys.stdin))
+ else:
+ patch = patch_path.read_text(encoding="utf8")
+ patch = patch.strip() # remove spaces around the patch
+ return PkgbuildPatch(variable, patch)
+
+ @staticmethod
+ def patch_set_create(application: Application, package_base: str, patch: PkgbuildPatch) -> None:
"""
create patch set for the package base
Args:
application(Application): application instance
- sources_dir(Path): path to directory with the package sources
- track(List[str]): track files which match the glob before creating the patch
+ package_base(str): package base
+ patch(PkgbuildPatch): patch descriptor
"""
- package = Package.from_build(sources_dir)
- patch = Sources.patch_create(sources_dir, *track)
- application.database.patches_insert(package.base, patch)
+ application.database.patches_insert(package_base, patch)
@staticmethod
- def patch_set_list(application: Application, package_base: Optional[str], exit_code: bool) -> None:
+ def patch_set_list(application: Application, package_base: Optional[str], variables: List[str],
+ exit_code: bool) -> None:
"""
list patches available for the package base
Args:
application(Application): application instance
package_base(Optional[str]): package base
+ variables(List[str]): extract patches only for specified PKGBUILD variables
exit_code(bool): exit with error on empty search result
+ :
"""
- patches = application.database.patches_list(package_base)
+ patches = application.database.patches_list(package_base, variables)
Patch.check_if_empty(exit_code, not patches)
for base, patch in patches.items():
- content = base if package_base is None else patch
- StringPrinter(content).print(verbose=True)
+ PatchPrinter(base, patch).print(verbose=True, separator=" = ")
@staticmethod
- def patch_set_remove(application: Application, package_base: str) -> None:
+ def patch_set_remove(application: Application, package_base: str, variables: List[str]) -> None:
"""
remove patch set for the package base
Args:
application(Application): application instance
package_base(str): package base
+ variables(List[str]): remove patches only for specified PKGBUILD variables
"""
- application.database.patches_remove(package_base)
+ application.database.patches_remove(package_base, variables)
diff --git a/src/ahriman/core/build_tools/sources.py b/src/ahriman/core/build_tools/sources.py
index 77df3b71..8c597d64 100644
--- a/src/ahriman/core/build_tools/sources.py
+++ b/src/ahriman/core/build_tools/sources.py
@@ -45,24 +45,22 @@ class Sources(LazyLogging):
_check_output = check_output
@staticmethod
- def extend_architectures(sources_dir: Path, architecture: str) -> None:
+ def extend_architectures(sources_dir: Path, architecture: str) -> List[PkgbuildPatch]:
"""
extend existing PKGBUILD with repository architecture
Args:
sources_dir(Path): local path to directory with source files
architecture(str): repository architecture
- """
- pkgbuild_path = sources_dir / "PKGBUILD"
- if not pkgbuild_path.is_file():
- return
+ Returns:
+ List[PkgbuildPatch]: generated patch for PKGBUILD architectures if required
+ """
architectures = Package.supported_architectures(sources_dir)
if "any" in architectures: # makepkg does not like when there is any other arch except for any
- return
+ return []
architectures.add(architecture)
- patch = PkgbuildPatch("arch", list(architectures))
- patch.write(pkgbuild_path)
+ return [PkgbuildPatch("arch", list(architectures))]
@staticmethod
def fetch(sources_dir: Path, remote: Optional[RemoteSource]) -> None:
@@ -134,14 +132,14 @@ class Sources(LazyLogging):
exception=None, cwd=sources_dir, logger=instance.logger)
@staticmethod
- def load(sources_dir: Path, package: Package, patch: Optional[str], paths: RepositoryPaths) -> None:
+ def load(sources_dir: Path, package: Package, patches: List[PkgbuildPatch], paths: RepositoryPaths) -> None:
"""
fetch sources from remote and apply patches
Args:
sources_dir(Path): local path to fetch
package(Package): package definitions
- patch(Optional[str]): optional patch to be applied
+ patches(List[PkgbuildPatch]): optional patch to be applied
paths(RepositoryPaths): repository paths instance
"""
instance = Sources()
@@ -150,9 +148,9 @@ class Sources(LazyLogging):
shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True)
instance.fetch(sources_dir, package.remote)
- if patch is not None:
+ patches.extend(instance.extend_architectures(sources_dir, paths.architecture))
+ for patch in patches:
instance.patch_apply(sources_dir, patch)
- instance.extend_architectures(sources_dir, paths.architecture)
@staticmethod
def patch_create(sources_dir: Path, *pattern: str) -> str:
@@ -248,15 +246,18 @@ class Sources(LazyLogging):
dst = sources_dir / src.relative_to(pkgbuild_dir)
shutil.move(src, dst)
- def patch_apply(self, sources_dir: Path, patch: str) -> None:
+ def patch_apply(self, sources_dir: Path, patch: PkgbuildPatch) -> None:
"""
apply patches if any
Args:
sources_dir(Path): local path to directory with git sources
- patch(str): patch to be applied
+ patch(PkgbuildPatch): patch to be applied
"""
# create patch
- self.logger.info("apply patch from database")
- Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
- exception=None, cwd=sources_dir, input_data=patch, logger=self.logger)
+ self.logger.info("apply patch %s from database at %s", patch.key, sources_dir)
+ if patch.is_plain_diff:
+ Sources._check_output("git", "apply", "--ignore-space-change", "--ignore-whitespace",
+ exception=None, cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
+ else:
+ patch.write(sources_dir / "PKGBUILD")
diff --git a/src/ahriman/core/database/migrations/m003_patch_variables.py b/src/ahriman/core/database/migrations/m003_patch_variables.py
new file mode 100644
index 00000000..b454bd85
--- /dev/null
+++ b/src/ahriman/core/database/migrations/m003_patch_variables.py
@@ -0,0 +1,43 @@
+#
+# Copyright (c) 2021-2022 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+__all__ = ["steps"]
+
+
+steps = [
+ """
+ alter table patches rename to patches_
+ """,
+ """
+ create table patches (
+ package_base text not null,
+ variable text,
+ patch blob not null
+ )
+ """,
+ """
+ create unique index patches_package_base_variable on patches (package_base, coalesce(variable, ''))
+ """,
+ """
+ insert into patches (package_base, patch) select package_base, patch from patches_
+ """,
+ """
+ drop table patches_
+ """,
+]
diff --git a/src/ahriman/core/database/operations/patch_operations.py b/src/ahriman/core/database/operations/patch_operations.py
index 9dcebfe4..97b483d5 100644
--- a/src/ahriman/core/database/operations/patch_operations.py
+++ b/src/ahriman/core/database/operations/patch_operations.py
@@ -17,10 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
+from collections import defaultdict
+
from sqlite3 import Connection
-from typing import Dict, Optional
+from typing import Dict, List, Optional, Tuple
from ahriman.core.database.operations import Operations
+from ahriman.models.pkgbuild_patch import PkgbuildPatch
class PatchOperations(Operations):
@@ -28,7 +31,7 @@ class PatchOperations(Operations):
operations for patches
"""
- def patches_get(self, package_base: str) -> Optional[str]:
+ def patches_get(self, package_base: str) -> List[PkgbuildPatch]:
"""
retrieve patches for the package
@@ -36,62 +39,77 @@ class PatchOperations(Operations):
package_base(str): package base to search for patches
Returns:
- Optional[str]: plain text patch for the package
+ List[PkgbuildPatch]: plain text patch for the package
"""
- return self.patches_list(package_base).get(package_base)
+ return self.patches_list(package_base, []).get(package_base, [])
- def patches_insert(self, package_base: str, patch: str) -> None:
+ def patches_insert(self, package_base: str, patch: PkgbuildPatch) -> None:
"""
insert or update patch in database
Args:
package_base(str): package base to insert
- patch(str): patch content
+ patch(PkgbuildPatch): patch content
"""
def run(connection: Connection) -> None:
connection.execute(
"""
insert into patches
- (package_base, patch)
+ (package_base, variable, patch)
values
- (:package_base, :patch)
- on conflict (package_base) do update set
+ (:package_base, :variable, :patch)
+ on conflict (package_base, coalesce(variable, '')) do update set
patch = :patch
""",
- {"package_base": package_base, "patch": patch})
+ {"package_base": package_base, "variable": patch.key, "patch": patch.value})
return self.with_connection(run, commit=True)
- def patches_list(self, package_base: Optional[str]) -> Dict[str, str]:
+ def patches_list(self, package_base: Optional[str], variables: List[str]) -> Dict[str, List[PkgbuildPatch]]:
"""
extract all patches
Args:
package_base(Optional[str]): optional filter by package base
+ variables(List[str]): extract patches only for specified PKGBUILD variables
Returns:
- Dict[str, str]: map of package base to patch content
+ Dict[str, List[PkgbuildPatch]]: map of package base to patch content
"""
- def run(connection: Connection) -> Dict[str, str]:
- return {
- cursor["package_base"]: cursor["patch"]
+ def run(connection: Connection) -> List[Tuple[str, PkgbuildPatch]]:
+ return [
+ (cursor["package_base"], PkgbuildPatch(cursor["variable"], cursor["patch"]))
for cursor in connection.execute(
"""select * from patches where :package_base is null or package_base = :package_base""",
{"package_base": package_base})
- }
+ ]
- return self.with_connection(run)
+ # we could use itertools & operator but why?
+ patches: Dict[str, List[PkgbuildPatch]] = defaultdict(list)
+ for package, patch in self.with_connection(run):
+ if variables and patch.key not in variables:
+ continue
+ patches[package].append(patch)
+ return dict(patches)
- def patches_remove(self, package_base: str) -> None:
+ def patches_remove(self, package_base: str, variables: List[str]) -> None:
"""
remove patch set
Args:
package_base(str): package base to clear patches
+ variables(List[str]): remove patches only for specified PKGBUILD variables
"""
+ def run_many(connection: Connection) -> None:
+ connection.executemany(
+ """delete from patches where package_base = :package_base and variable = :variable""",
+ [{"package_base": package_base, "variable": variable} for variable in variables])
+
def run(connection: Connection) -> None:
connection.execute(
"""delete from patches where package_base = :package_base""",
{"package_base": package_base})
+ if variables:
+ return self.with_connection(run_many, commit=True)
return self.with_connection(run, commit=True)
diff --git a/src/ahriman/core/formatters/__init__.py b/src/ahriman/core/formatters/__init__.py
index 931e99d5..c6cffb1c 100644
--- a/src/ahriman/core/formatters/__init__.py
+++ b/src/ahriman/core/formatters/__init__.py
@@ -24,6 +24,7 @@ from ahriman.core.formatters.aur_printer import AurPrinter
from ahriman.core.formatters.build_printer import BuildPrinter
from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
from ahriman.core.formatters.package_printer import PackagePrinter
+from ahriman.core.formatters.patch_printer import PatchPrinter
from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.formatters.user_printer import UserPrinter
diff --git a/src/ahriman/core/formatters/patch_printer.py b/src/ahriman/core/formatters/patch_printer.py
new file mode 100644
index 00000000..ab9568d6
--- /dev/null
+++ b/src/ahriman/core/formatters/patch_printer.py
@@ -0,0 +1,56 @@
+#
+# Copyright (c) 2021-2022 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from typing import List
+
+from ahriman.core.formatters import StringPrinter
+from ahriman.models.pkgbuild_patch import PkgbuildPatch
+from ahriman.models.property import Property
+
+
+class PatchPrinter(StringPrinter):
+ """
+ print content of the PKGBUILD patch
+
+ Attributes:
+ patches(List[PkgbuildPatch]): PKGBUILD patch object
+ """
+
+ def __init__(self, package_base: str, patches: List[PkgbuildPatch]) -> None:
+ """
+ default constructor
+
+ Args:
+ package_base(str): package base
+ patches(List[PkgbuildPatch]): PKGBUILD patch object
+ """
+ StringPrinter.__init__(self, package_base)
+ self.patches = patches
+
+ def properties(self) -> List[Property]:
+ """
+ convert content into printable data
+
+ Returns:
+ List[Property]: list of content properties
+ """
+ return [
+ Property(patch.key or "Full source diff", patch.value, is_required=True)
+ for patch in self.patches
+ ]
diff --git a/src/ahriman/core/repository/executor.py b/src/ahriman/core/repository/executor.py
index 46f847ea..48ed817f 100644
--- a/src/ahriman/core/repository/executor.py
+++ b/src/ahriman/core/repository/executor.py
@@ -109,7 +109,7 @@ class Executor(Cleaner):
try:
self.paths.tree_clear(package_base) # remove all internal files
self.database.build_queue_clear(package_base)
- self.database.patches_remove(package_base)
+ self.database.patches_remove(package_base, [])
self.reporter.remove(package_base) # we only update status page in case of base removal
except Exception:
self.logger.exception("could not remove base %s", package_base)
diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py
index 96183717..447a8e5c 100644
--- a/src/ahriman/models/package.py
+++ b/src/ahriman/models/package.py
@@ -305,7 +305,7 @@ class Package(LazyLogging):
from ahriman.core.build_tools.sources import Sources
- Sources.load(paths.cache_for(self.base), self, None, paths)
+ Sources.load(paths.cache_for(self.base), self, [], paths)
try:
# update pkgver first
diff --git a/src/ahriman/models/pkgbuild_patch.py b/src/ahriman/models/pkgbuild_patch.py
index 64c7a5dc..ca6a4b7e 100644
--- a/src/ahriman/models/pkgbuild_patch.py
+++ b/src/ahriman/models/pkgbuild_patch.py
@@ -21,7 +21,7 @@ import shlex
from dataclasses import dataclass, field
from pathlib import Path
-from typing import List, Union
+from typing import List, Optional, Union
@dataclass(frozen=True)
@@ -30,16 +30,23 @@ class PkgbuildPatch:
wrapper for patching PKBGUILDs
Attributes:
- key(str): name of the property in PKGBUILD, e.g. version, url etc
+ key(Optional[str]): name of the property in PKGBUILD, e.g. version, url etc. If not set, patch will be
+ considered as full PKGBUILD diffs
value(Union[str, List[str]]): value of the stored PKGBUILD property. It must be either string or list of string
values
unsafe(bool): if set, value will be not quoted, might break PKGBUILD
"""
- key: str
+ key: Optional[str]
value: Union[str, List[str]]
unsafe: bool = field(default=False, kw_only=True)
+ def __post_init__(self) -> None:
+ """
+ remove empty key
+ """
+ object.__setattr__(self, "key", self.key or None)
+
@property
def is_function(self) -> bool:
"""
@@ -48,7 +55,17 @@ class PkgbuildPatch:
Returns:
bool: True in case if key ends with parentheses and False otherwise
"""
- return self.key.endswith("()")
+ return self.key is not None and self.key.endswith("()")
+
+ @property
+ def is_plain_diff(self) -> bool:
+ """
+ check if patch is full diff one or just single-variable patch
+
+ Returns:
+ bool: True in case key set and False otherwise
+ """
+ return self.key is None
def quote(self, value: str) -> str:
"""
@@ -74,6 +91,8 @@ class PkgbuildPatch:
if isinstance(self.value, list): # list like
value = " ".join(map(self.quote, self.value))
return f"""{self.key}=({value})"""
+ if self.is_plain_diff: # no additional logic for plain diffs
+ return self.value
# we suppose that function values are only supported in string-like values
if self.is_function:
return f"{self.key} {self.value}" # no quoting enabled here
diff --git a/tests/ahriman/application/handlers/test_handler_patch.py b/tests/ahriman/application/handlers/test_handler_patch.py
index 3960aeed..71d8aae3 100644
--- a/tests/ahriman/application/handlers/test_handler_patch.py
+++ b/tests/ahriman/application/handlers/test_handler_patch.py
@@ -1,5 +1,6 @@
import argparse
import pytest
+import sys
from pathlib import Path
from pytest_mock import MockerFixture
@@ -9,6 +10,7 @@ from ahriman.application.handlers import Patch
from ahriman.core.configuration import Configuration
from ahriman.models.action import Action
from ahriman.models.package import Package
+from ahriman.models.pkgbuild_patch import PkgbuildPatch
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@@ -25,6 +27,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.exit_code = False
args.remove = False
args.track = ["*.diff", "*.patch"]
+ args.variable = None
return args
@@ -35,12 +38,31 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
args = _default_args(args)
args.action = Action.Update
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
+ patch_mock = mocker.patch("ahriman.application.handlers.Patch.patch_create_from_diff",
+ return_value=(args.package, PkgbuildPatch(None, "patch")))
application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_create")
- on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
Patch.run(args, "x86_64", configuration, True, False)
- application_mock.assert_called_once_with(pytest.helpers.anyvar(int), Path(args.package), args.track)
- on_start_mock.assert_called_once_with()
+ patch_mock.assert_called_once_with(args.package, args.track)
+ application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, PkgbuildPatch(None, "patch"))
+
+
+def test_run_function(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command with patch function flag
+ """
+ args = _default_args(args)
+ args.action = Action.Update
+ args.patch = "patch"
+ args.variable = "version"
+ patch = PkgbuildPatch(args.variable, args.patch)
+ mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
+ patch_mock = mocker.patch("ahriman.application.handlers.Patch.patch_create_from_function", return_value=patch)
+ application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_create")
+
+ Patch.run(args, "x86_64", configuration, True, False)
+ patch_mock.assert_called_once_with(args.variable, args.patch)
+ application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, patch)
def test_run_list(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
@@ -49,11 +71,12 @@ def test_run_list(args: argparse.Namespace, configuration: Configuration, mocker
"""
args = _default_args(args)
args.action = Action.List
+ args.variable = ["version"]
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_list")
Patch.run(args, "x86_64", configuration, True, False)
- application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, False)
+ application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, ["version"], False)
def test_run_remove(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
@@ -62,24 +85,71 @@ def test_run_remove(args: argparse.Namespace, configuration: Configuration, mock
"""
args = _default_args(args)
args.action = Action.Remove
+ args.variable = ["version"]
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.application.handlers.Patch.patch_set_remove")
Patch.run(args, "x86_64", configuration, True, False)
- application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package)
+ application_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.package, ["version"])
+
+
+def test_patch_create_from_diff(package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must create patch from directory tree diff
+ """
+ patch = PkgbuildPatch(None, "patch")
+ path = Path("local")
+ mocker.patch("pathlib.Path.mkdir")
+ package_mock = mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
+ sources_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value=patch.value)
+
+ assert Patch.patch_create_from_diff(path, ["*.diff"]) == (package_ahriman.base, patch)
+ package_mock.assert_called_once_with(path)
+ sources_mock.assert_called_once_with(path, "*.diff")
+
+
+def test_patch_create_from_function(mocker: MockerFixture) -> None:
+ """
+ must create function patch from file
+ """
+ path = Path("local")
+ patch = PkgbuildPatch("version", "patch")
+ read_mock = mocker.patch("pathlib.Path.read_text", return_value=patch.value)
+
+ assert Patch.patch_create_from_function(patch.key, path) == patch
+ read_mock.assert_called_once_with(encoding="utf8")
+
+
+def test_patch_create_from_function_stdin(mocker: MockerFixture) -> None:
+ """
+ must create function patch from stdin
+ """
+ patch = PkgbuildPatch("version", "This is a patch")
+ mocker.patch.object(sys, "stdin", patch.value.splitlines())
+ assert Patch.patch_create_from_function(patch.key, None) == patch
+
+
+def test_patch_create_from_function_strip(mocker: MockerFixture) -> None:
+ """
+ must remove spaces at the beginning and at the end of the line
+ """
+ patch = PkgbuildPatch("version", "This is a patch")
+ mocker.patch.object(sys, "stdin", ["\n"] + patch.value.splitlines() + ["\n"])
+ assert Patch.patch_create_from_function(patch.key, None) == patch
def test_patch_set_list(application: Application, mocker: MockerFixture) -> None:
"""
must list available patches for the command
"""
- get_mock = mocker.patch("ahriman.core.database.SQLite.patches_list", return_value={"ahriman": "patch"})
+ get_mock = mocker.patch("ahriman.core.database.SQLite.patches_list",
+ return_value={"ahriman": PkgbuildPatch(None, "patch")})
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
- Patch.patch_set_list(application, "ahriman", False)
- get_mock.assert_called_once_with("ahriman")
- print_mock.assert_called_once_with(verbose=True)
+ Patch.patch_set_list(application, "ahriman", ["version"], False)
+ get_mock.assert_called_once_with("ahriman", ["version"])
+ print_mock.assert_called_once_with(verbose=True, separator=" = ")
check_mock.assert_called_once_with(False, False)
@@ -90,7 +160,7 @@ def test_patch_set_list_empty_exception(application: Application, mocker: Mocker
mocker.patch("ahriman.core.database.SQLite.patches_list", return_value={})
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")
- Patch.patch_set_list(application, "ahriman", True)
+ Patch.patch_set_list(application, "ahriman", [], True)
check_mock.assert_called_once_with(True, True)
@@ -98,13 +168,9 @@ def test_patch_set_create(application: Application, package_ahriman: Package, mo
"""
must create patch set for the package
"""
- mocker.patch("pathlib.Path.mkdir")
- mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
- mocker.patch("ahriman.core.build_tools.sources.Sources.patch_create", return_value="patch")
create_mock = mocker.patch("ahriman.core.database.SQLite.patches_insert")
-
- Patch.patch_set_create(application, Path("path"), ["*.patch"])
- create_mock.assert_called_once_with(package_ahriman.base, "patch")
+ Patch.patch_set_create(application, package_ahriman.base, PkgbuildPatch("version", package_ahriman.version))
+ create_mock.assert_called_once_with(package_ahriman.base, PkgbuildPatch("version", package_ahriman.version))
def test_patch_set_remove(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
@@ -112,5 +178,5 @@ def test_patch_set_remove(application: Application, package_ahriman: Package, mo
must remove patch set for the package
"""
remove_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove")
- Patch.patch_set_remove(application, package_ahriman.base)
- remove_mock.assert_called_once_with(package_ahriman.base)
+ Patch.patch_set_remove(application, package_ahriman.base, ["version"])
+ remove_mock.assert_called_once_with(package_ahriman.base, ["version"])
diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py
index 8d04c14e..e2d607bd 100644
--- a/tests/ahriman/application/test_ahriman.py
+++ b/tests/ahriman/application/test_ahriman.py
@@ -197,7 +197,7 @@ def test_subparsers_patch_add(parser: argparse.ArgumentParser) -> None:
"""
patch-add command must imply action, architecture list, lock and no-report
"""
- args = parser.parse_args(["patch-add", "ahriman"])
+ args = parser.parse_args(["patch-add", "ahriman", "version"])
assert args.action == Action.Update
assert args.architecture == [""]
assert args.lock is None
@@ -208,18 +208,10 @@ def test_subparsers_patch_add_architecture(parser: argparse.ArgumentParser) -> N
"""
patch-add command must correctly parse architecture list
"""
- args = parser.parse_args(["-a", "x86_64", "patch-add", "ahriman"])
+ args = parser.parse_args(["-a", "x86_64", "patch-add", "ahriman", "version"])
assert args.architecture == [""]
-def test_subparsers_patch_add_track(parser: argparse.ArgumentParser) -> None:
- """
- patch-add command must correctly parse track files patterns
- """
- args = parser.parse_args(["patch-add", "-t", "*.py", "ahriman"])
- assert args.track == ["*.diff", "*.patch", "*.py"]
-
-
def test_subparsers_patch_list(parser: argparse.ArgumentParser) -> None:
"""
patch-list command must imply action, architecture list, lock and no-report
@@ -258,6 +250,42 @@ def test_subparsers_patch_remove_architecture(parser: argparse.ArgumentParser) -
assert args.architecture == [""]
+def test_subparsers_patch_set_add(parser: argparse.ArgumentParser) -> None:
+ """
+ patch-set-add command must imply action, architecture list, lock, no-report and variable
+ """
+ args = parser.parse_args(["patch-set-add", "ahriman"])
+ assert args.action == Action.Update
+ assert args.architecture == [""]
+ assert args.lock is None
+ assert args.no_report
+ assert args.variable is None
+
+
+def test_subparsers_patch_set_add_architecture(parser: argparse.ArgumentParser) -> None:
+ """
+ patch-set-add command must correctly parse architecture list
+ """
+ args = parser.parse_args(["-a", "x86_64", "patch-set-add", "ahriman"])
+ assert args.architecture == [""]
+
+
+def test_subparsers_patch_set_add_option_package(parser: argparse.ArgumentParser) -> None:
+ """
+ patch-set-add command must convert package option to path instance
+ """
+ args = parser.parse_args(["patch-set-add", "ahriman"])
+ assert isinstance(args.package, Path)
+
+
+def test_subparsers_patch_set_add_option_track(parser: argparse.ArgumentParser) -> None:
+ """
+ patch-set-add command must correctly parse track files patterns
+ """
+ args = parser.parse_args(["patch-set-add", "-t", "*.py", "ahriman"])
+ assert args.track == ["*.diff", "*.patch", "*.py"]
+
+
def test_subparsers_repo_backup(parser: argparse.ArgumentParser) -> None:
"""
repo-backup command must imply architecture list, lock, no-report and unsafe
diff --git a/tests/ahriman/core/build_tools/test_sources.py b/tests/ahriman/core/build_tools/test_sources.py
index 8499405f..a78b82f6 100644
--- a/tests/ahriman/core/build_tools/test_sources.py
+++ b/tests/ahriman/core/build_tools/test_sources.py
@@ -6,6 +6,7 @@ from unittest import mock
from ahriman.core.build_tools.sources import Sources
from ahriman.models.package import Package
+from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_paths import RepositoryPaths
@@ -16,11 +17,9 @@ def test_extend_architectures(mocker: MockerFixture) -> None:
"""
mocker.patch("pathlib.Path.is_file", return_value=True)
archs_mock = mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"x86_64"})
- write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
- Sources.extend_architectures(Path("local"), "i686")
+ assert Sources.extend_architectures(Path("local"), "i686") == [PkgbuildPatch("arch", list({"x86_64", "i686"}))]
archs_mock.assert_called_once_with(Path("local"))
- write_mock.assert_called_once_with(Path("local") / "PKGBUILD")
def test_extend_architectures_any(mocker: MockerFixture) -> None:
@@ -29,21 +28,7 @@ def test_extend_architectures_any(mocker: MockerFixture) -> None:
"""
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"any"})
- write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
-
- Sources.extend_architectures(Path("local"), "i686")
- write_mock.assert_not_called()
-
-
-def test_extend_architectures_skip(mocker: MockerFixture) -> None:
- """
- must skip extending list of the architectures in case if no PKGBUILD file found
- """
- mocker.patch("pathlib.Path.is_file", return_value=False)
- write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
-
- Sources.extend_architectures(Path("local"), "i686")
- write_mock.assert_not_called()
+ assert Sources.extend_architectures(Path("local"), "i686") == []
def test_fetch_empty(remote_source: RemoteSource, mocker: MockerFixture) -> None:
@@ -167,15 +152,16 @@ def test_load(package_ahriman: Package, repository_paths: RepositoryPaths, mocke
"""
must load packages sources correctly
"""
- mocker.patch("pathlib.Path.is_dir", return_value=False)
+ patch = PkgbuildPatch(None, "patch")
+ path = Path("local")
fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply")
- architectures_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures")
+ architectures_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures", return_value=[])
- Sources.load(Path("local"), package_ahriman, "patch", repository_paths)
- fetch_mock.assert_called_once_with(Path("local"), package_ahriman.remote)
- patch_mock.assert_called_once_with(Path("local"), "patch")
- architectures_mock.assert_called_once_with(Path("local"), repository_paths.architecture)
+ Sources.load(path, package_ahriman, [patch], repository_paths)
+ fetch_mock.assert_called_once_with(path, package_ahriman.remote)
+ patch_mock.assert_called_once_with(path, patch)
+ architectures_mock.assert_called_once_with(path, repository_paths.architecture)
def test_load_no_patch(package_ahriman: Package, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
@@ -184,9 +170,10 @@ def test_load_no_patch(package_ahriman: Package, repository_paths: RepositoryPat
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
+ mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures", return_value=[])
patch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.patch_apply")
- Sources.load(Path("local"), package_ahriman, None, repository_paths)
+ Sources.load(Path("local"), package_ahriman, [], repository_paths)
patch_mock.assert_not_called()
@@ -197,8 +184,9 @@ def test_load_with_cache(package_ahriman: Package, repository_paths: RepositoryP
mocker.patch("pathlib.Path.is_dir", return_value=True)
copytree_mock = mocker.patch("shutil.copytree")
mocker.patch("ahriman.core.build_tools.sources.Sources.fetch")
+ mocker.patch("ahriman.core.build_tools.sources.Sources.extend_architectures", return_value=[])
- Sources.load(Path("local"), package_ahriman, None, repository_paths)
+ Sources.load(Path("local"), package_ahriman, [], repository_paths)
copytree_mock.assert_called_once() # we do not check full command here, sorry
@@ -331,11 +319,24 @@ def test_patch_apply(sources: Sources, mocker: MockerFixture) -> None:
"""
must apply patches if any
"""
+ patch = PkgbuildPatch(None, "patch")
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output")
local = Path("local")
- sources.patch_apply(local, "patches")
+ sources.patch_apply(local, patch)
check_output_mock.assert_called_once_with(
"git", "apply", "--ignore-space-change", "--ignore-whitespace",
- exception=None, cwd=local, input_data="patches", logger=pytest.helpers.anyvar(int)
+ exception=None, cwd=local, input_data=patch.value, logger=pytest.helpers.anyvar(int)
)
+
+
+def test_patch_apply_function(sources: Sources, mocker: MockerFixture) -> None:
+ """
+ must apply single-function patches
+ """
+ patch = PkgbuildPatch("version", "42")
+ local = Path("local")
+ write_mock = mocker.patch("ahriman.models.pkgbuild_patch.PkgbuildPatch.write")
+
+ sources.patch_apply(local, patch)
+ write_mock.assert_called_once_with(local / "PKGBUILD")
diff --git a/tests/ahriman/core/build_tools/test_task.py b/tests/ahriman/core/build_tools/test_task.py
index 11652bb7..44b5669b 100644
--- a/tests/ahriman/core/build_tools/test_task.py
+++ b/tests/ahriman/core/build_tools/test_task.py
@@ -20,4 +20,4 @@ def test_init(task_ahriman: Task, database: SQLite, mocker: MockerFixture) -> No
"""
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load")
task_ahriman.init(Path("ahriman"), database)
- load_mock.assert_called_once_with(Path("ahriman"), task_ahriman.package, None, task_ahriman.paths)
+ load_mock.assert_called_once_with(Path("ahriman"), task_ahriman.package, [], task_ahriman.paths)
diff --git a/tests/ahriman/core/database/migrations/test_m003_patch_variables.py b/tests/ahriman/core/database/migrations/test_m003_patch_variables.py
new file mode 100644
index 00000000..f03b786c
--- /dev/null
+++ b/tests/ahriman/core/database/migrations/test_m003_patch_variables.py
@@ -0,0 +1,8 @@
+from ahriman.core.database.migrations.m003_patch_variables import steps
+
+
+def test_migration_package_source() -> None:
+ """
+ migration must not be empty
+ """
+ assert steps
diff --git a/tests/ahriman/core/database/operations/test_patch_operations.py b/tests/ahriman/core/database/operations/test_patch_operations.py
index adc14e3e..4c13e862 100644
--- a/tests/ahriman/core/database/operations/test_patch_operations.py
+++ b/tests/ahriman/core/database/operations/test_patch_operations.py
@@ -1,55 +1,96 @@
from ahriman.core.database import SQLite
from ahriman.models.package import Package
+from ahriman.models.pkgbuild_patch import PkgbuildPatch
def test_patches_get_insert(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must insert patch to database
"""
- database.patches_insert(package_ahriman.base, "patch_1")
- database.patches_insert(package_python_schedule.base, "patch_2")
- assert database.patches_get(package_ahriman.base) == "patch_1"
- assert not database.build_queue_get()
+ database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch_1"))
+ database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch_3"))
+ database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch_2"))
+ assert database.patches_get(package_ahriman.base) == [
+ PkgbuildPatch(None, "patch_1"), PkgbuildPatch("key", "patch_3")
+ ]
def test_patches_list(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must list all patches
"""
- database.patches_insert(package_ahriman.base, "patch1")
- database.patches_insert(package_python_schedule.base, "patch2")
- assert database.patches_list(None) == {package_ahriman.base: "patch1", package_python_schedule.base: "patch2"}
+ database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
+ database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3"))
+ database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
+ assert database.patches_list(None, []) == {
+ package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch3")],
+ package_python_schedule.base: [PkgbuildPatch(None, "patch2")],
+ }
def test_patches_list_filter(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must list all patches filtered by package name (same as get)
"""
- database.patches_insert(package_ahriman.base, "patch1")
- database.patches_insert(package_python_schedule.base, "patch2")
+ database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
+ database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
- assert database.patches_list(package_ahriman.base) == {package_ahriman.base: "patch1"}
- assert database.patches_list(package_python_schedule.base) == {package_python_schedule.base: "patch2"}
+ assert database.patches_list(package_ahriman.base, []) == {package_ahriman.base: [PkgbuildPatch(None, "patch1")]}
+ assert database.patches_list(package_python_schedule.base, []) == {
+ package_python_schedule.base: [PkgbuildPatch(None, "patch2")],
+ }
+
+
+def test_patches_list_filter_by_variable(database: SQLite, package_ahriman: Package,
+ package_python_schedule: Package) -> None:
+ """
+ must list all patches filtered by package name (same as get)
+ """
+ database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
+ database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch2"))
+ database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch3"))
+
+ assert database.patches_list(None, []) == {
+ package_ahriman.base: [PkgbuildPatch(None, "patch1"), PkgbuildPatch("key", "patch2")],
+ package_python_schedule.base: [PkgbuildPatch(None, "patch3")],
+ }
+ assert database.patches_list(None, ["key"]) == {
+ package_ahriman.base: [PkgbuildPatch("key", "patch2")],
+ }
def test_patches_insert_remove(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must remove patch from database
"""
- database.patches_insert(package_ahriman.base, "patch_1")
- database.patches_insert(package_python_schedule.base, "patch_2")
- database.patches_remove(package_ahriman.base)
+ database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
+ database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
+ database.patches_remove(package_ahriman.base, [])
- assert database.patches_get(package_ahriman.base) is None
- database.patches_insert(package_python_schedule.base, "patch_2")
+ assert database.patches_get(package_ahriman.base) == []
+ assert database.patches_get(package_python_schedule.base) == [PkgbuildPatch(None, "patch2")]
+
+
+def test_patches_insert_remove_by_variable(database: SQLite, package_ahriman: Package,
+ package_python_schedule: Package) -> None:
+ """
+ must remove patch from database by variable
+ """
+ database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
+ database.patches_insert(package_ahriman.base, PkgbuildPatch("key", "patch3"))
+ database.patches_insert(package_python_schedule.base, PkgbuildPatch(None, "patch2"))
+ database.patches_remove(package_ahriman.base, ["key"])
+
+ assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")]
+ assert database.patches_get(package_python_schedule.base) == [PkgbuildPatch(None, "patch2")]
def test_patches_insert_insert(database: SQLite, package_ahriman: Package) -> None:
"""
must update patch in database
"""
- database.patches_insert(package_ahriman.base, "patch_1")
- assert database.patches_get(package_ahriman.base) == "patch_1"
+ database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch1"))
+ assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch1")]
- database.patches_insert(package_ahriman.base, "patch_2")
- assert database.patches_get(package_ahriman.base) == "patch_2"
+ database.patches_insert(package_ahriman.base, PkgbuildPatch(None, "patch2"))
+ assert database.patches_get(package_ahriman.base) == [PkgbuildPatch(None, "patch2")]
diff --git a/tests/ahriman/core/formatters/conftest.py b/tests/ahriman/core/formatters/conftest.py
index 7246d195..a274db52 100644
--- a/tests/ahriman/core/formatters/conftest.py
+++ b/tests/ahriman/core/formatters/conftest.py
@@ -1,10 +1,11 @@
import pytest
-from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, StatusPrinter, StringPrinter, \
- UpdatePrinter, UserPrinter, VersionPrinter
+from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, PatchPrinter, StatusPrinter, \
+ StringPrinter, UpdatePrinter, UserPrinter, VersionPrinter
from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
+from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.user import User
@@ -47,6 +48,20 @@ def package_ahriman_printer(package_ahriman: Package) -> PackagePrinter:
return PackagePrinter(package_ahriman, BuildStatus())
+@pytest.fixture
+def patch_printer(package_ahriman: Package) -> PatchPrinter:
+ """
+ fixture for patch printer
+
+ Args:
+ package_ahriman(Package): package fixture
+
+ Returns:
+ PatchPrinter: patch printer test instance
+ """
+ return PatchPrinter(package_ahriman.base, [PkgbuildPatch("key", "value")])
+
+
@pytest.fixture
def status_printer() -> StatusPrinter:
"""
diff --git a/tests/ahriman/core/formatters/test_patch_printer.py b/tests/ahriman/core/formatters/test_patch_printer.py
new file mode 100644
index 00000000..059de51f
--- /dev/null
+++ b/tests/ahriman/core/formatters/test_patch_printer.py
@@ -0,0 +1,22 @@
+from ahriman.core.formatters import PatchPrinter
+
+
+def test_properties(patch_printer: PatchPrinter) -> None:
+ """
+ must return non empty properties list
+ """
+ assert patch_printer.properties()
+
+
+def test_properties_required(patch_printer: PatchPrinter) -> None:
+ """
+ must return all properties as required
+ """
+ assert all(prop.is_required for prop in patch_printer.properties())
+
+
+def test_title(patch_printer: PatchPrinter) -> None:
+ """
+ must return non empty title
+ """
+ assert patch_printer.title() == "ahriman"
diff --git a/tests/ahriman/core/repository/test_executor.py b/tests/ahriman/core/repository/test_executor.py
index beb46415..9cd55ab4 100644
--- a/tests/ahriman/core/repository/test_executor.py
+++ b/tests/ahriman/core/repository/test_executor.py
@@ -72,7 +72,7 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke
# must update status and remove package files
tree_clear_mock.assert_called_once_with(package_ahriman.base)
build_queue_mock.assert_called_once_with(package_ahriman.base)
- patches_mock.assert_called_once_with(package_ahriman.base)
+ patches_mock.assert_called_once_with(package_ahriman.base, [])
status_client_mock.assert_called_once_with(package_ahriman.base)
diff --git a/tests/ahriman/core/test_tree.py b/tests/ahriman/core/test_tree.py
index 402cab1a..ef5a7e86 100644
--- a/tests/ahriman/core/test_tree.py
+++ b/tests/ahriman/core/test_tree.py
@@ -49,8 +49,7 @@ def test_leaf_load(package_ahriman: Package, repository_paths: RepositoryPaths,
leaf = Leaf.load(package_ahriman, repository_paths, database)
assert leaf.package == package_ahriman
assert leaf.dependencies == {"ahriman-dependency"}
- load_mock.assert_called_once_with(
- pytest.helpers.anyvar(int), package_ahriman, None, repository_paths)
+ load_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman, [], repository_paths)
dependencies_mock.assert_called_once_with(pytest.helpers.anyvar(int))
diff --git a/tests/ahriman/models/test_pkgbuild_patch.py b/tests/ahriman/models/test_pkgbuild_patch.py
index 9063ee06..0d40134d 100644
--- a/tests/ahriman/models/test_pkgbuild_patch.py
+++ b/tests/ahriman/models/test_pkgbuild_patch.py
@@ -5,6 +5,15 @@ from unittest.mock import MagicMock, call
from ahriman.models.pkgbuild_patch import PkgbuildPatch
+def test_post_init() -> None:
+ """
+ must remove empty keys
+ """
+ assert PkgbuildPatch("", "value").key is None
+ assert PkgbuildPatch(None, "value").key is None
+ assert PkgbuildPatch("key", "value").key == "key"
+
+
def test_is_function() -> None:
"""
must correctly define key as function
@@ -13,6 +22,14 @@ def test_is_function() -> None:
assert PkgbuildPatch("key()", "value").is_function
+def test_is_plain_diff() -> None:
+ """
+ must correctly define key as function
+ """
+ assert not PkgbuildPatch("key", "value").is_plain_diff
+ assert PkgbuildPatch(None, "value").is_plain_diff
+
+
def test_quote() -> None:
"""
must quote strings if unsafe flag is not set
@@ -32,6 +49,13 @@ def test_serialize() -> None:
assert PkgbuildPatch("key", "4'2", unsafe=True).serialize() == "key=4'2"
+def test_serialize_plain_diff() -> None:
+ """
+ must correctly serialize function values
+ """
+ assert PkgbuildPatch(None, "{ value }").serialize() == "{ value }"
+
+
def test_serialize_function() -> None:
"""
must correctly serialize function values