diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index 480279ef..d9e69348 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -285,7 +285,7 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser: formatter_class=_formatter) parser.add_argument("package", help="filter check by package base", nargs="*") parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true") - parser.set_defaults(handler=handlers.Update, dry_run=True, no_aur=False, no_manual=True) + parser.set_defaults(handler=handlers.Update, dry_run=True, no_aur=False, no_local=False, no_manual=True) return parser @@ -461,6 +461,7 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser: parser.add_argument("package", help="filter check by package base", nargs="*") parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true") parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true") + parser.add_argument("--no-local", help="do not check local packages for updates", action="store_true") parser.add_argument("--no-manual", help="do not include manual updates", action="store_true") parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true") parser.set_defaults(handler=handlers.Update) diff --git a/src/ahriman/application/application/repository.py b/src/ahriman/application/application/repository.py index 1c023173..7607ce51 100644 --- a/src/ahriman/application/application/repository.py +++ b/src/ahriman/application/application/repository.py @@ -163,27 +163,31 @@ class Repository(Properties): packages = self.repository.process_build(level) process_update(packages) - def updates(self, filter_packages: Iterable[str], no_aur: bool, no_manual: bool, no_vcs: bool, + def updates(self, filter_packages: Iterable[str], no_aur: bool, no_local: bool, no_manual: bool, no_vcs: bool, log_fn: Callable[[str], None]) -> List[Package]: """ get list of packages to run update process :param filter_packages: do not check every package just specified in the list :param no_aur: do not check for aur updates + :param no_local: do not check local packages for updates :param no_manual: do not check for manual updates :param no_vcs: do not check VCS packages :param log_fn: logger function to log updates :return: list of out-of-dated packages """ - updates = [] + updates = {} if not no_aur: - updates.extend(self.repository.updates_aur(filter_packages, no_vcs)) + updates.update({package.base: package for package in self.repository.updates_aur(filter_packages, no_vcs)}) + if not no_local: + updates.update({package.base: package for package in self.repository.updates_local()}) if not no_manual: - updates.extend(self.repository.updates_manual()) + updates.update({package.base: package for package in self.repository.updates_manual()}) local_versions = {package.base: package.version for package in self.repository.packages()} - for package in updates: + updated_packages = [package for _, package in sorted(updates.items())] + for package in updated_packages: UpdatePrinter(package, local_versions.get(package.base)).print( verbose=True, log_fn=log_fn, separator=" -> ") - return updates + return updated_packages diff --git a/src/ahriman/application/handlers/add.py b/src/ahriman/application/handlers/add.py index 2cc85024..75f75a12 100644 --- a/src/ahriman/application/handlers/add.py +++ b/src/ahriman/application/handlers/add.py @@ -46,5 +46,5 @@ class Add(Handler): if not args.now: return - packages = application.updates(args.package, True, False, True, application.logger.info) + packages = application.updates(args.package, True, True, False, True, application.logger.info) application.update(packages) diff --git a/src/ahriman/application/handlers/update.py b/src/ahriman/application/handlers/update.py index 972bce0f..7d6fca90 100644 --- a/src/ahriman/application/handlers/update.py +++ b/src/ahriman/application/handlers/update.py @@ -42,7 +42,7 @@ class Update(Handler): :param no_report: force disable reporting """ application = Application(architecture, configuration, no_report) - packages = application.updates(args.package, args.no_aur, args.no_manual, args.no_vcs, + packages = application.updates(args.package, args.no_aur, args.no_local, args.no_manual, args.no_vcs, Update.log_fn(application, args.dry_run)) if args.dry_run: return diff --git a/src/ahriman/core/build_tools/sources.py b/src/ahriman/core/build_tools/sources.py index eb7abade..20a97d9c 100644 --- a/src/ahriman/core/build_tools/sources.py +++ b/src/ahriman/core/build_tools/sources.py @@ -20,7 +20,7 @@ import logging from pathlib import Path -from typing import List +from typing import List, Optional from ahriman.core.util import check_output @@ -64,7 +64,7 @@ class Sources: patch_path.write_text(patch) @staticmethod - def fetch(sources_dir: Path, remote: str) -> None: + def fetch(sources_dir: Path, remote: Optional[str]) -> None: """ either clone repository or update it to origin/`branch` :param sources_dir: local path to fetch @@ -81,6 +81,8 @@ class Sources: Sources.logger.info("update HEAD to remote at %s", sources_dir) Sources._check_output("git", "fetch", "origin", Sources._branch, exception=None, cwd=sources_dir, logger=Sources.logger) + elif remote is None: + Sources.logger.warning("%s is not initialized, but no remote provided", sources_dir) else: Sources.logger.info("clone remote %s to %s", remote, sources_dir) Sources._check_output("git", "clone", remote, str(sources_dir), exception=None, logger=Sources.logger) diff --git a/src/ahriman/core/repository/update_handler.py b/src/ahriman/core/repository/update_handler.py index 21555694..822dcf7f 100644 --- a/src/ahriman/core/repository/update_handler.py +++ b/src/ahriman/core/repository/update_handler.py @@ -19,6 +19,7 @@ # from typing import Iterable, List +from ahriman.core.build_tools.sources import Sources from ahriman.core.repository.cleaner import Cleaner from ahriman.models.package import Package from ahriman.models.package_source import PackageSource @@ -65,6 +66,31 @@ class UpdateHandler(Cleaner): return result + def updates_local(self) -> List[Package]: + """ + check local packages for updates + :return: list of local packages which are out-of-dated + """ + result: List[Package] = [] + packages = {local.base: local for local in self.packages()} + + for dirname in self.paths.cache.iterdir(): + try: + Sources.fetch(dirname, remote=None) + remote = Package.load(str(dirname), PackageSource.Local, self.pacman, self.aur_url) + + local = packages.get(remote.base) + if local is None: + self.reporter.set_unknown(remote) + result.append(remote) + elif local.is_outdated(remote, self.paths): + self.reporter.set_pending(local.base) + result.append(remote) + except Exception: + self.logger.exception("could not procees package at %s", dirname) + + return result + def updates_manual(self) -> List[Package]: """ check for packages for which manual update has been requested diff --git a/tests/ahriman/application/application/test_application_repository.py b/tests/ahriman/application/application/test_application_repository.py index 9ac51b41..462f85fc 100644 --- a/tests/ahriman/application/application/test_application_repository.py +++ b/tests/ahriman/application/application/test_application_repository.py @@ -205,10 +205,12 @@ def test_updates_all(application_repository: Repository, package_ahriman: Packag mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[]) 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") updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual") - application_repository.updates([], no_aur=False, no_manual=False, no_vcs=False, log_fn=print) + application_repository.updates([], no_aur=False, no_local=False, no_manual=False, no_vcs=False, log_fn=print) updates_aur_mock.assert_called_once_with([], False) + updates_local_mock.assert_called_once() updates_manual_mock.assert_called_once() @@ -218,10 +220,12 @@ def test_updates_disabled(application_repository: Repository, mocker: MockerFixt """ mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[]) 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") - application_repository.updates([], no_aur=True, no_manual=True, no_vcs=False, log_fn=print) + application_repository.updates([], no_aur=True, no_local=True, no_manual=True, no_vcs=False, log_fn=print) updates_aur_mock.assert_not_called() + updates_local_mock.assert_not_called() updates_manual_mock.assert_not_called() @@ -231,10 +235,27 @@ def test_updates_no_aur(application_repository: Repository, mocker: MockerFixtur """ mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[]) 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") - application_repository.updates([], no_aur=True, no_manual=False, no_vcs=False, log_fn=print) + application_repository.updates([], no_aur=True, no_local=False, no_manual=False, no_vcs=False, log_fn=print) updates_aur_mock.assert_not_called() + updates_local_mock.assert_called_once() + updates_manual_mock.assert_called_once() + + +def test_updates_no_local(application_repository: Repository, mocker: MockerFixture) -> None: + """ + must get updates without local packages + """ + mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[]) + 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") + + application_repository.updates([], no_aur=False, no_local=True, no_manual=False, no_vcs=False, log_fn=print) + updates_aur_mock.assert_called_once_with([], False) + updates_local_mock.assert_not_called() updates_manual_mock.assert_called_once() @@ -244,10 +265,12 @@ def test_updates_no_manual(application_repository: Repository, mocker: MockerFix """ mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[]) 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") - application_repository.updates([], no_aur=False, no_manual=True, no_vcs=False, log_fn=print) + application_repository.updates([], no_aur=False, no_local=False, no_manual=True, no_vcs=False, log_fn=print) updates_aur_mock.assert_called_once_with([], False) + updates_local_mock.assert_called_once() updates_manual_mock.assert_not_called() @@ -257,21 +280,26 @@ def test_updates_no_vcs(application_repository: Repository, mocker: MockerFixtur """ mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[]) 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") - application_repository.updates([], no_aur=False, no_manual=False, no_vcs=True, log_fn=print) + application_repository.updates([], no_aur=False, no_local=False, no_manual=False, no_vcs=True, log_fn=print) updates_aur_mock.assert_called_once_with([], True) + updates_local_mock.assert_called_once() updates_manual_mock.assert_called_once() def test_updates_with_filter(application_repository: Repository, mocker: MockerFixture) -> None: """ - must get updates without VCS + must get updates with filter """ mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[]) 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") - application_repository.updates(["filter"], no_aur=False, no_manual=False, no_vcs=False, log_fn=print) + application_repository.updates(["filter"], no_aur=False, no_local=False, no_manual=False, no_vcs=False, + log_fn=print) updates_aur_mock.assert_called_once_with(["filter"], False) + updates_local_mock.assert_called_once() updates_manual_mock.assert_called_once() diff --git a/tests/ahriman/application/handlers/test_handler_update.py b/tests/ahriman/application/handlers/test_handler_update.py index 4e8a9b6d..5aa560c0 100644 --- a/tests/ahriman/application/handlers/test_handler_update.py +++ b/tests/ahriman/application/handlers/test_handler_update.py @@ -16,6 +16,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace: args.package = [] args.dry_run = False args.no_aur = False + args.no_local = False args.no_manual = False args.no_vcs = False return args diff --git a/tests/ahriman/core/build_tools/test_sources.py b/tests/ahriman/core/build_tools/test_sources.py index 4892b353..29433797 100644 --- a/tests/ahriman/core/build_tools/test_sources.py +++ b/tests/ahriman/core/build_tools/test_sources.py @@ -86,6 +86,23 @@ def test_fetch_new(mocker: MockerFixture) -> None: ]) +def test_fetch_new_without_remote(mocker: MockerFixture) -> None: + """ + must fetch nothing in case if no remote set + """ + mocker.patch("pathlib.Path.is_dir", return_value=False) + check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + + local = Path("local") + Sources.fetch(local, None) + check_output_mock.assert_has_calls([ + mock.call("git", "checkout", "--force", Sources._branch, + exception=None, cwd=local, logger=pytest.helpers.anyvar(int)), + mock.call("git", "reset", "--hard", f"origin/{Sources._branch}", + exception=None, cwd=local, logger=pytest.helpers.anyvar(int)) + ]) + + def test_has_remotes(mocker: MockerFixture) -> None: """ must ask for remotes diff --git a/tests/ahriman/core/repository/test_update_handler.py b/tests/ahriman/core/repository/test_update_handler.py index 07d28d53..59267de3 100644 --- a/tests/ahriman/core/repository/test_update_handler.py +++ b/tests/ahriman/core/repository/test_update_handler.py @@ -81,6 +81,50 @@ def test_updates_aur_ignore_vcs(update_handler: UpdateHandler, package_ahriman: package_is_outdated_mock.assert_not_called() +def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must check for updates for locally stored packages + """ + mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) + mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base]) + mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) + fetch_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") + package_load_mock = mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending") + + assert update_handler.updates_local() == [package_ahriman] + fetch_mock.assert_called_once_with(package_ahriman.base, remote=None) + package_load_mock.assert_called_once() + status_client_mock.assert_called_once() + + +def test_updates_local_unknown(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must return unknown package as out-dated + """ + mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[]) + mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base]) + mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) + mocker.patch("ahriman.core.build_tools.sources.Sources.fetch") + mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman) + status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_unknown") + + assert update_handler.updates_local() == [package_ahriman] + status_client_mock.assert_called_once() + + +def test_updates_local_with_failures(update_handler: UpdateHandler, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must process local through the packages with failure + """ + mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages") + mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base]) + mocker.patch("ahriman.core.build_tools.sources.Sources.fetch", side_effect=Exception()) + + assert not update_handler.updates_local() + + def test_updates_manual_clear(update_handler: UpdateHandler, mocker: MockerFixture) -> None: """ requesting manual updates must clear packages directory @@ -125,7 +169,7 @@ def test_updates_manual_status_unknown(update_handler: UpdateHandler, package_ah def test_updates_manual_with_failures(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None: """ - must process through the packages with failure + must process manual through the packages with failure """ mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base]) mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[])