feat: add package copy subcommand

This commit is contained in:
Evgenii Alekseev 2024-09-27 17:23:04 +03:00
parent 7bc4810377
commit 1e7d4daf18
10 changed files with 300 additions and 12 deletions

View File

@ -36,6 +36,14 @@ ahriman.application.handlers.clean module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.copy module
----------------------------------------
.. automodule:: ahriman.application.handlers.copy
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.daemon module
------------------------------------------

View File

@ -148,13 +148,11 @@ Before using this command you will need to create local directory and put ``PKGB
How to copy package from another repository
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
As simple as add package from archive. Considering case when you would like to copy package ``package`` with version ``ver-rel`` from repository ``source-repository`` to ``target-respository`` (same architecture), the command will be following:
It is possible to copy package and its metadata between local repositories, optionally removing the source archive, e.g.:
.. code-block:: shell
sudo -u ahriman ahriman -r target-repository package-add /var/lib/ahriman/repository/source-repository/x86_64/package-ver-rel-x86_64.pkg.tar.zst
In addition, you can remove source package as usual later.
sudo -u ahriman ahriman -r target-repository package-copy source-repository ahriman
This feature in particular useful if for managing multiple repositories like ``[testing]`` and ``[extra]``.

View File

@ -110,6 +110,7 @@ Start web service (requires additional configuration):
_set_package_add_parser(subparsers)
_set_package_changes_parser(subparsers)
_set_package_changes_remove_parser(subparsers)
_set_package_copy_parser(subparsers)
_set_package_remove_parser(subparsers)
_set_package_status_parser(subparsers)
_set_package_status_remove_parser(subparsers)
@ -334,6 +335,27 @@ def _set_package_changes_remove_parser(root: SubParserAction) -> argparse.Argume
return parser
def _set_package_copy_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package copy subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-copy", aliases=["copy"], help="copy package from another repository",
description="copy package and its metadata from another repository",
formatter_class=_HelpFormatter)
parser.add_argument("source", help="source repository name")
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("--remove", help="remove package from the source repository after", action="store_true")
parser.set_defaults(handler=handlers.Copy)
return parser
def _set_package_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package removal subcommand

View File

@ -141,19 +141,19 @@ class ApplicationPackages(ApplicationProperties):
self.database.build_queue_insert(package)
self.reporter.set_unknown(package)
def add(self, names: Iterable[str], source: PackageSource, username: str | None = None) -> None:
def add(self, packages: Iterable[str], source: PackageSource, username: str | None = None) -> None:
"""
add packages for the next build
Args:
names(Iterable[str]): list of package bases to add
packages(Iterable[str]): list of package bases to add
source(PackageSource): package source to add
username(str | None, optional): optional override of username for build process (Default value = None)
"""
for name in names:
resolved_source = source.resolve(name, self.repository.paths)
for package in packages:
resolved_source = source.resolve(package, self.repository.paths)
fn = getattr(self, f"_add_{resolved_source.value}")
fn(name, username)
fn(package, username)
def on_result(self, result: Result) -> None:
"""
@ -167,16 +167,16 @@ class ApplicationPackages(ApplicationProperties):
"""
raise NotImplementedError
def remove(self, names: Iterable[str]) -> Result:
def remove(self, packages: Iterable[str]) -> Result:
"""
remove packages from repository
Args:
names(Iterable[str]): list of packages (either base or name) to remove
packages(Iterable[str]): list of packages (either base or name) to remove
Returns:
Result: removal result
"""
result = self.repository.process_remove(names)
result = self.repository.process_remove(packages)
self.on_result(result)
return result

View File

@ -191,6 +191,12 @@ class ApplicationRepository(ApplicationProperties):
"""
updates = {}
# always add already built packages, because they will be always added
updates.update({
package.base: package
for package in self.repository.load_archives(self.repository.packages_built())
})
if aur:
updates.update({package.base: package for package in self.repository.updates_aur(filter_packages, vcs=vcs)})
if local:

View File

@ -21,6 +21,7 @@ from ahriman.application.handlers.add import Add
from ahriman.application.handlers.backup import Backup
from ahriman.application.handlers.change import Change
from ahriman.application.handlers.clean import Clean
from ahriman.application.handlers.copy import Copy
from ahriman.application.handlers.daemon import Daemon
from ahriman.application.handlers.dump import Dump
from ahriman.application.handlers.handler import Handler

View File

@ -0,0 +1,95 @@
#
# Copyright (c) 2021-2024 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 <http://www.gnu.org/licenses/>.
#
import argparse
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.repository_id import RepositoryId
class Copy(Handler):
"""
copy packages handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=report)
application.on_start()
configuration_path, _ = configuration.check_loaded()
source_repository_id = RepositoryId(repository_id.architecture, args.source)
source_configuration = Configuration.from_path(configuration_path, source_repository_id)
source_application = Application(source_repository_id, source_configuration, report=report)
packages = source_application.repository.packages(args.package)
Copy.check_status(args.exit_code, packages)
for package in packages:
Copy.copy_package(package, application, source_application)
# run update
application.update([])
if args.remove:
source_application.remove(args.package)
@staticmethod
def copy_package(package: Package, application: Application, source_application: Application) -> None:
"""
copy package ``package`` from source repository to target repository
Args:
package(Package): package to copy
application(Application): application instance of the target repository
source_application(Application): application instance of the source repository
"""
# copy files
source_paths = [
str(source_application.repository.paths.repository / source.filename)
for source in package.packages.values()
if source.filename is not None
]
application.add(source_paths, PackageSource.Archive)
# copy metadata
application.reporter.package_changes_update(
package.base, source_application.reporter.package_changes_get(package.base)
)
application.reporter.package_dependencies_update(
package.base, source_application.reporter.package_dependencies_get(package.base)
)
application.reporter.package_update(package, BuildStatusEnum.Pending)

View File

@ -1,5 +1,6 @@
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import call as MockCall
@ -213,6 +214,9 @@ def test_updates_all(application_repository: ApplicationRepository, package_ahri
"""
must get updates for all
"""
path = Path("local")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.packages_built", return_value=[path])
updates_built_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives")
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur",
return_value=[package_ahriman])
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
@ -220,6 +224,7 @@ def test_updates_all(application_repository: ApplicationRepository, package_ahri
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=True, local=True, manual=True, vcs=True, check_files=True)
updates_built_mock.assert_called_once_with([path])
updates_aur_mock.assert_called_once_with([], vcs=True)
updates_local_mock.assert_called_once_with(vcs=True)
updates_manual_mock.assert_called_once_with()
@ -230,12 +235,16 @@ def test_updates_disabled(application_repository: ApplicationRepository, mocker:
"""
must get updates without anything
"""
path = Path("local")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.packages_built", return_value=[path])
updates_built_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives")
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=False, local=False, manual=False, vcs=True, check_files=False)
updates_built_mock.assert_called_once_with([path])
updates_aur_mock.assert_not_called()
updates_local_mock.assert_not_called()
updates_manual_mock.assert_not_called()
@ -246,12 +255,16 @@ def test_updates_no_aur(application_repository: ApplicationRepository, mocker: M
"""
must get updates without aur
"""
path = Path("local")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.packages_built", return_value=[path])
updates_built_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives")
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=False, local=True, manual=True, vcs=True, check_files=True)
updates_built_mock.assert_called_once_with([path])
updates_aur_mock.assert_not_called()
updates_local_mock.assert_called_once_with(vcs=True)
updates_manual_mock.assert_called_once_with()
@ -262,12 +275,16 @@ def test_updates_no_local(application_repository: ApplicationRepository, mocker:
"""
must get updates without local packages
"""
path = Path("local")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.packages_built", return_value=[path])
updates_built_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives")
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=True, local=False, manual=True, vcs=True, check_files=True)
updates_built_mock.assert_called_once_with([path])
updates_aur_mock.assert_called_once_with([], vcs=True)
updates_local_mock.assert_not_called()
updates_manual_mock.assert_called_once_with()
@ -278,12 +295,16 @@ def test_updates_no_manual(application_repository: ApplicationRepository, mocker
"""
must get updates without manual
"""
path = Path("local")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.packages_built", return_value=[path])
updates_built_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives")
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=True, local=True, manual=False, vcs=True, check_files=True)
updates_built_mock.assert_called_once_with([path])
updates_aur_mock.assert_called_once_with([], vcs=True)
updates_local_mock.assert_called_once_with(vcs=True)
updates_manual_mock.assert_not_called()
@ -294,12 +315,16 @@ def test_updates_no_vcs(application_repository: ApplicationRepository, mocker: M
"""
must get updates without VCS
"""
path = Path("local")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.packages_built", return_value=[path])
updates_built_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives")
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=True, local=True, manual=True, vcs=False, check_files=True)
updates_built_mock.assert_called_once_with([path])
updates_aur_mock.assert_called_once_with([], vcs=False)
updates_local_mock.assert_called_once_with(vcs=False)
updates_manual_mock.assert_called_once_with()
@ -310,12 +335,16 @@ def test_updates_no_check_files(application_repository: ApplicationRepository, m
"""
must get updates without checking broken links
"""
path = Path("local")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.packages_built", return_value=[path])
updates_built_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives")
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates([], aur=True, local=True, manual=True, vcs=True, check_files=False)
updates_built_mock.assert_called_once_with([path])
updates_aur_mock.assert_called_once_with([], vcs=True)
updates_local_mock.assert_called_once_with(vcs=True)
updates_manual_mock.assert_called_once_with()
@ -326,12 +355,16 @@ def test_updates_with_filter(application_repository: ApplicationRepository, mock
"""
must get updates with filter
"""
path = Path("local")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.packages_built", return_value=[path])
updates_built_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives")
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
application_repository.updates(["filter"], aur=True, local=True, manual=True, vcs=True, check_files=True)
updates_built_mock.assert_called_once_with([path])
updates_aur_mock.assert_called_once_with(["filter"], vcs=True)
updates_local_mock.assert_called_once_with(vcs=True)
updates_manual_mock.assert_called_once_with()

View File

@ -0,0 +1,105 @@
import argparse
import pytest
from pytest_mock import MockerFixture
from ahriman.application.application import Application
from ahriman.application.handlers import Copy
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.source = "source"
args.package = ["ahriman"]
args.exit_code = False
args.remove = False
return args
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.core.repository.Repository.packages", return_value=[package_ahriman])
application_mock = mocker.patch("ahriman.application.handlers.Copy.copy_package")
update_mock = mocker.patch("ahriman.application.application.Application.update")
remove_mock = mocker.patch("ahriman.application.application.Application.remove")
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
_, repository_id = configuration.check_loaded()
Copy.run(args, repository_id, configuration, report=False)
application_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(int), pytest.helpers.anyvar(int))
update_mock.assert_called_once_with([])
remove_mock.assert_not_called()
on_start_mock.assert_called_once_with()
def test_run_remove(args: argparse.Namespace, configuration: Configuration, repository: Repository,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run command and remove packages afterwards
"""
args = _default_args(args)
args.remove = True
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.core.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.application.handlers.Copy.copy_package")
mocker.patch("ahriman.application.application.Application.update")
remove_mock = mocker.patch("ahriman.application.application.Application.remove")
_, repository_id = configuration.check_loaded()
Copy.run(args, repository_id, configuration, report=False)
remove_mock.assert_called_once_with(args.package)
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must raise ExitCode exception on empty result
"""
args = _default_args(args)
args.exit_code = True
mocker.patch("ahriman.core.repository.Repository.packages", return_value=[])
mocker.patch("ahriman.application.application.Application.update")
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_status")
_, repository_id = configuration.check_loaded()
Copy.run(args, repository_id, configuration, report=False)
check_mock.assert_called_once_with(True, [])
def test_copy_package(package_ahriman: Package, application: Application, mocker: MockerFixture) -> None:
"""
must copy package between repositories and its metadata
"""
add_mock = mocker.patch("ahriman.application.application.Application.add")
changes_get_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get")
changes_update_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
deps_get_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_dependencies_get")
deps_update_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_dependencies_update")
package_update_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_update")
path = application.repository.paths.repository / package_ahriman.packages[package_ahriman.base].filename
Copy.copy_package(package_ahriman, application, application)
add_mock.assert_called_once_with([str(path)], PackageSource.Archive)
changes_get_mock.assert_called_once_with(package_ahriman.base)
changes_update_mock.assert_called_once_with(package_ahriman.base, changes_get_mock.return_value)
deps_get_mock.assert_called_once_with(package_ahriman.base)
deps_update_mock.assert_called_once_with(package_ahriman.base, deps_get_mock.return_value)
package_update_mock.assert_called_once_with(package_ahriman, BuildStatusEnum.Pending)

View File

@ -309,6 +309,26 @@ def test_subparsers_package_changes_remove_package_changes(parser: argparse.Argu
assert dir(args) == dir(reference_args)
def test_subparsers_package_copy_option_architecture(parser: argparse.ArgumentParser) -> None:
"""
package-copy command must correctly parse architecture list
"""
args = parser.parse_args(["package-copy", "source", "ahriman"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "package-copy", "source", "ahriman"])
assert args.architecture == "x86_64"
def test_subparsers_package_copy_option_repository(parser: argparse.ArgumentParser) -> None:
"""
package-copy command must correctly parse repository list
"""
args = parser.parse_args(["package-copy", "source", "ahriman"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "package-copy", "source", "ahriman"])
assert args.repository == "repo"
def test_subparsers_package_remove_option_architecture(parser: argparse.ArgumentParser) -> None:
"""
package-remove command must correctly parse architecture list