From 9259d9c7276e24dff9ae6c472a5c88d5d0398a2d Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Fri, 11 Aug 2023 18:17:23 +0300 Subject: [PATCH] Local packages support improvements (#104) * handle git author correctly * make remote source required argument --- docs/ahriman.core.database.migrations.rst | 8 ++ docs/configuration.rst | 3 +- .../templates/build-status/table.jinja2 | 2 +- .../application/application_packages.py | 4 +- src/ahriman/core/build_tools/sources.py | 74 ++++++++---- .../core/configuration/configuration.py | 2 +- .../migrations/m001_package_source.py | 11 +- .../migrations/m005_make_opt_depends.py | 2 +- .../database/migrations/m007_check_depends.py | 2 +- .../database/migrations/m008_packagers.py | 2 +- .../database/migrations/m009_local_source.py | 27 +++++ .../database/operations/package_operations.py | 11 +- src/ahriman/core/gitremote/remote_push.py | 8 +- .../core/gitremote/remote_push_trigger.py | 5 +- src/ahriman/core/repository/repository.py | 5 +- src/ahriman/core/repository/update_handler.py | 17 ++- src/ahriman/core/spawn.py | 4 +- src/ahriman/core/util.py | 2 +- src/ahriman/models/aur_package.py | 3 +- src/ahriman/models/package.py | 59 +++++++--- src/ahriman/models/package_description.py | 3 +- src/ahriman/models/package_source.py | 8 +- src/ahriman/models/remote_source.py | 79 ++++++------- src/ahriman/models/user.py | 6 +- src/ahriman/web/schemas/remote_schema.py | 8 +- .../application/test_application_packages.py | 4 +- tests/ahriman/conftest.py | 17 ++- tests/ahriman/core/alpm/test_pacman.py | 11 +- .../ahriman/core/build_tools/test_sources.py | 107 +++++++++++++++--- .../migrations/test_m001_package_source.py | 15 --- .../migrations/test_m005_make_opt_depends.py | 2 +- .../migrations/test_m007_check_depends.py | 2 +- .../migrations/test_m008_packagers.py | 2 +- .../migrations/test_m009_local_source.py | 8 ++ .../operations/test_package_operations.py | 2 +- .../core/repository/test_repository.py | 5 + .../core/repository/test_update_handler.py | 17 ++- tests/ahriman/core/test_spawn.py | 6 +- tests/ahriman/models/conftest.py | 9 +- tests/ahriman/models/test_package.py | 5 +- tests/ahriman/models/test_package_source.py | 52 ++++++--- tests/ahriman/models/test_remote_source.py | 61 ++++------ tests/testresources/core/ahriman.ini | 3 +- 43 files changed, 451 insertions(+), 232 deletions(-) create mode 100644 src/ahriman/core/database/migrations/m009_local_source.py create mode 100644 tests/ahriman/core/database/migrations/test_m009_local_source.py diff --git a/docs/ahriman.core.database.migrations.rst b/docs/ahriman.core.database.migrations.rst index ec26ecb4..cf0cfaf0 100644 --- a/docs/ahriman.core.database.migrations.rst +++ b/docs/ahriman.core.database.migrations.rst @@ -76,6 +76,14 @@ ahriman.core.database.migrations.m008\_packagers module :no-undoc-members: :show-inheritance: +ahriman.core.database.migrations.m009\_local\_source module +----------------------------------------------------------- + +.. automodule:: ahriman.core.database.migrations.m009_local_source + :members: + :no-undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/docs/configuration.rst b/docs/configuration.rst index b49057b5..b354be36 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -179,7 +179,8 @@ Available options are: Remote push trigger ^^^^^^^^^^^^^^^^^^^ -* ``commit_author`` - git commit author, string, optional. In case if not set, the git will generate author for you. Note, however, that in this case it will disclosure your hostname. +* ``commit_email`` - git commit email, string, optional, default is ``ahriman@localhost``. +* ``commit_user`` - git commit user, string, optional, default is ``ahriman``. * ``push_url`` - url of the remote repository to which PKGBUILDs should be pushed after build process, string, required. * ``push_branch`` - branch of the remote repository to which PKGBUILDs should be pushed after build process, string, optional, default is ``master``. diff --git a/package/share/ahriman/templates/build-status/table.jinja2 b/package/share/ahriman/templates/build-status/table.jinja2 index 337b9343..64fab9f7 100644 --- a/package/share/ahriman/templates/build-status/table.jinja2 +++ b/package/share/ahriman/templates/build-status/table.jinja2 @@ -112,7 +112,7 @@ const payload = response.map(description => { const package_base = description.package.base; - const web_url = description.package.remote?.web_url; + const web_url = description.package.remote.web_url; return { id: package_base, base: web_url ? `${safe(package_base)}` : safe(package_base), diff --git a/src/ahriman/application/application/application_packages.py b/src/ahriman/application/application/application_packages.py index 6301031f..8c19aee7 100644 --- a/src/ahriman/application/application/application_packages.py +++ b/src/ahriman/application/application/application_packages.py @@ -95,7 +95,7 @@ class ApplicationPackages(ApplicationProperties): if (source_dir := Path(source)).is_dir(): package = Package.from_build(source_dir, self.architecture, username) cache_dir = self.repository.paths.cache_for(package.base) - shutil.copytree(source_dir, cache_dir) # copy package to store in caches + shutil.copytree(source_dir, cache_dir, dirs_exist_ok=True) # copy package to store in caches Sources.init(cache_dir) # we need to run init command in directory where we do have permissions elif (source_dir := self.repository.paths.cache_for(source)).is_dir(): package = Package.from_build(source_dir, self.architecture, username) @@ -145,7 +145,7 @@ class ApplicationPackages(ApplicationProperties): username(str | None, optional): optional override of username for build process (Default value = None) """ for name in names: - resolved_source = source.resolve(name) + resolved_source = source.resolve(name, self.repository.paths) fn = getattr(self, f"_add_{resolved_source.value}") fn(name, username) diff --git a/src/ahriman/core/build_tools/sources.py b/src/ahriman/core/build_tools/sources.py index 3971ab22..1587fb16 100644 --- a/src/ahriman/core/build_tools/sources.py +++ b/src/ahriman/core/build_tools/sources.py @@ -36,9 +36,11 @@ class Sources(LazyLogging): Attributes: DEFAULT_BRANCH(str): (class attribute) default branch to process git repositories. Must be used only for local stored repositories, use RemoteSource descriptor instead for real packages + DEFAULT_COMMIT_AUTHOR(tuple[str, str]): (class attribute) default commit author to be used if none set """ DEFAULT_BRANCH = "master" # default fallback branch + DEFAULT_COMMIT_AUTHOR = ("ahriman", "ahriman@localhost") _check_output = check_output @@ -61,13 +63,13 @@ class Sources(LazyLogging): return [PkgbuildPatch("arch", list(architectures))] @staticmethod - def fetch(sources_dir: Path, remote: RemoteSource | None) -> None: + def fetch(sources_dir: Path, remote: RemoteSource) -> None: """ either clone repository or update it to origin/``remote.branch`` Args: sources_dir(Path): local path to fetch - remote(RemoteSource | None): remote target (from where to fetch) + remote(RemoteSource): remote target (from where to fetch) """ instance = Sources() # local directory exists and there is .git directory @@ -77,11 +79,11 @@ class Sources(LazyLogging): instance.logger.info("skip update at %s because there are no branches configured", sources_dir) return - branch = remote.branch if remote is not None else instance.DEFAULT_BRANCH + branch = remote.branch or instance.DEFAULT_BRANCH if is_initialized_git: instance.logger.info("update HEAD to remote at %s using branch %s", sources_dir, branch) Sources._check_output("git", "fetch", "origin", branch, cwd=sources_dir, logger=instance.logger) - elif remote is not None: + elif remote.git_url is not None: instance.logger.info("clone remote %s to %s using branch %s", remote.git_url, sources_dir, branch) Sources._check_output("git", "clone", "--branch", branch, "--single-branch", remote.git_url, str(sources_dir), cwd=sources_dir.parent, logger=instance.logger) @@ -95,7 +97,7 @@ class Sources(LazyLogging): # move content if required # we are using full path to source directory in order to make append possible - pkgbuild_dir = remote.pkgbuild_dir if remote is not None else sources_dir.resolve() + pkgbuild_dir = remote.pkgbuild_dir or sources_dir.resolve() instance.move((sources_dir / pkgbuild_dir).resolve(), sources_dir) @staticmethod @@ -122,14 +124,16 @@ class Sources(LazyLogging): sources_dir(Path): local path to sources """ instance = Sources() - Sources._check_output("git", "init", "--initial-branch", instance.DEFAULT_BRANCH, - cwd=sources_dir, logger=instance.logger) + if not (sources_dir / ".git").is_dir(): + # skip initializing in case if it was already + Sources._check_output("git", "init", "--initial-branch", instance.DEFAULT_BRANCH, + cwd=sources_dir, logger=instance.logger) # extract local files... files = ["PKGBUILD", ".SRCINFO"] + [str(path) for path in Package.local_files(sources_dir)] instance.add(sources_dir, *files) # ...and commit them - instance.commit(sources_dir, author="ahriman ") + instance.commit(sources_dir) @staticmethod def load(sources_dir: Path, package: Package, patches: list[PkgbuildPatch], paths: RepositoryPaths) -> None: @@ -170,7 +174,8 @@ class Sources(LazyLogging): return f"{diff}\n" # otherwise, patch will be broken @staticmethod - def push(sources_dir: Path, remote: RemoteSource, *pattern: str, commit_author: str | None = None) -> None: + def push(sources_dir: Path, remote: RemoteSource, *pattern: str, + commit_author: tuple[str, str] | None = None) -> None: """ commit selected changes and push files to the remote repository @@ -178,13 +183,15 @@ class Sources(LazyLogging): sources_dir(Path): local path to git repository remote(RemoteSource): remote target, branch and url *pattern(str): glob patterns - commit_author(str | None, optional): commit author in form of git config (i.e. ``user ``) - (Default value = None) + commit_author(tuple[str, str] | None, optional): commit author if any (Default value = None) """ instance = Sources() instance.add(sources_dir, *pattern) - instance.commit(sources_dir, author=commit_author) - Sources._check_output("git", "push", remote.git_url, remote.branch, cwd=sources_dir, logger=instance.logger) + if not instance.commit(sources_dir, commit_author=commit_author): + return # no changes to push, just skip action + + git_url, branch = remote.git_source() + Sources._check_output("git", "push", git_url, branch, cwd=sources_dir, logger=instance.logger) def add(self, sources_dir: Path, *pattern: str, intent_to_add: bool = False) -> None: """ @@ -208,7 +215,8 @@ class Sources(LazyLogging): Sources._check_output("git", "add", *args, *[str(fn.relative_to(sources_dir)) for fn in found_files], cwd=sources_dir, logger=self.logger) - def commit(self, sources_dir: Path, message: str | None = None, author: str | None = None) -> None: + def commit(self, sources_dir: Path, message: str | None = None, + commit_author: tuple[str, str] | None = None) -> bool: """ commit changes @@ -216,14 +224,28 @@ class Sources(LazyLogging): sources_dir(Path): local path to git repository message(str | None, optional): optional commit message if any. If none set, message will be generated according to the current timestamp (Default value = None) - author(str | None, optional): optional commit author if any (Default value = None) + commit_author(tuple[str, str] | None, optional): optional commit author if any (Default value = None) + + Returns: + bool: True in case if changes have been committed and False otherwise """ + if not self.has_changes(sources_dir): + return False # nothing to commit + if message is None: message = f"Autogenerated commit at {utcnow()}" - args = ["--allow-empty", "--message", message] - if author is not None: - args.extend(["--author", author]) - Sources._check_output("git", "commit", *args, cwd=sources_dir, logger=self.logger) + args = ["--message", message] + environment: dict[str, str] = {} + + if commit_author is None: + commit_author = self.DEFAULT_COMMIT_AUTHOR + user, email = commit_author + environment["GIT_AUTHOR_NAME"] = environment["GIT_COMMITTER_NAME"] = user + environment["GIT_AUTHOR_EMAIL"] = environment["GIT_COMMITTER_EMAIL"] = email + + Sources._check_output("git", "commit", *args, cwd=sources_dir, logger=self.logger, environment=environment) + + return True def diff(self, sources_dir: Path) -> str: """ @@ -237,6 +259,20 @@ class Sources(LazyLogging): """ return Sources._check_output("git", "diff", cwd=sources_dir, logger=self.logger) + def has_changes(self, sources_dir: Path) -> bool: + """ + check if there are changes in current git tree + + Args: + sources_dir(Path): local path to git repository + + Returns: + bool: True if there are uncommitted changes and False otherwise + """ + # there is --exit-code argument to diff, however, there might be other process errors + changes = Sources._check_output("git", "diff", "--cached", "--name-only", cwd=sources_dir, logger=self.logger) + return bool(changes) + def move(self, pkgbuild_dir: Path, sources_dir: Path) -> None: """ move content from pkgbuild_dir to sources_dir diff --git a/src/ahriman/core/configuration/configuration.py b/src/ahriman/core/configuration/configuration.py index 30a269ba..ca316cbc 100644 --- a/src/ahriman/core/configuration/configuration.py +++ b/src/ahriman/core/configuration/configuration.py @@ -48,7 +48,7 @@ class Configuration(configparser.RawConfigParser): >>> from pathlib import Path >>> - >>> configuration = Configuration.from_path(Path("/etc/ahriman.ini"), "x86_64", quiet=False) + >>> configuration = Configuration.from_path(Path("/etc/ahriman.ini"), "x86_64") >>> repository_name = configuration.get("repository", "name") >>> makepkg_flags = configuration.getlist("build", "makepkg_flags") diff --git a/src/ahriman/core/database/migrations/m001_package_source.py b/src/ahriman/core/database/migrations/m001_package_source.py index 89caae0b..ece66537 100644 --- a/src/ahriman/core/database/migrations/m001_package_source.py +++ b/src/ahriman/core/database/migrations/m001_package_source.py @@ -70,6 +70,7 @@ def migrate_package_remotes(connection: Connection, paths: RepositoryPaths) -> N connection(Connection): database connection paths(RepositoryPaths): repository paths instance """ + from ahriman.core.alpm.remote import AUR from ahriman.core.database.operations import PackageOperations def insert_remote(base: str, remote: RemoteSource) -> None: @@ -92,7 +93,11 @@ def migrate_package_remotes(connection: Connection, paths: RepositoryPaths) -> N local_cache = paths.cache_for(package_base) if local_cache.exists() and not package.is_vcs: continue # skip packages which are not VCS and with local cache - remote_source = RemoteSource.from_source(PackageSource.AUR, package_base, "aur") - if remote_source is None: - continue # should never happen + remote_source = RemoteSource( + source=PackageSource.AUR, + git_url=AUR.remote_git_url(package_base, "aur"), + web_url=AUR.remote_web_url(package_base), + path=".", + branch="master", + ) insert_remote(package_base, remote_source) diff --git a/src/ahriman/core/database/migrations/m005_make_opt_depends.py b/src/ahriman/core/database/migrations/m005_make_opt_depends.py index e337d4b5..ed9f6084 100644 --- a/src/ahriman/core/database/migrations/m005_make_opt_depends.py +++ b/src/ahriman/core/database/migrations/m005_make_opt_depends.py @@ -66,7 +66,7 @@ def migrate_package_depends(connection: Connection, configuration: Configuration package_list = [] for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()): - base = Package.from_archive(full_path, pacman, remote=None) + base = Package.from_archive(full_path, pacman) for package, description in base.packages.items(): package_list.append({ "make_depends": description.make_depends, diff --git a/src/ahriman/core/database/migrations/m007_check_depends.py b/src/ahriman/core/database/migrations/m007_check_depends.py index 2bf57903..9769bd01 100644 --- a/src/ahriman/core/database/migrations/m007_check_depends.py +++ b/src/ahriman/core/database/migrations/m007_check_depends.py @@ -63,7 +63,7 @@ def migrate_package_check_depends(connection: Connection, configuration: Configu package_list = [] for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()): - base = Package.from_archive(full_path, pacman, remote=None) + base = Package.from_archive(full_path, pacman) for package, description in base.packages.items(): package_list.append({ "check_depends": description.check_depends, diff --git a/src/ahriman/core/database/migrations/m008_packagers.py b/src/ahriman/core/database/migrations/m008_packagers.py index b54196e4..df62c21f 100644 --- a/src/ahriman/core/database/migrations/m008_packagers.py +++ b/src/ahriman/core/database/migrations/m008_packagers.py @@ -69,7 +69,7 @@ def migrate_package_base_packager(connection: Connection, configuration: Configu package_list = [] for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()): - package = Package.from_archive(full_path, pacman, remote=None) + package = Package.from_archive(full_path, pacman) package_list.append({ "package_base": package.base, "packager": package.packager, diff --git a/src/ahriman/core/database/migrations/m009_local_source.py b/src/ahriman/core/database/migrations/m009_local_source.py new file mode 100644 index 00000000..b12a646c --- /dev/null +++ b/src/ahriman/core/database/migrations/m009_local_source.py @@ -0,0 +1,27 @@ +# +# Copyright (c) 2021-2023 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 = [ + """ + update package_bases set source = 'local' where source is null + """, +] diff --git a/src/ahriman/core/database/operations/package_operations.py b/src/ahriman/core/database/operations/package_operations.py index 8ddf5ea6..acf28703 100644 --- a/src/ahriman/core/database/operations/package_operations.py +++ b/src/ahriman/core/database/operations/package_operations.py @@ -86,11 +86,11 @@ class PackageOperations(Operations): { "package_base": package.base, "version": package.version, - "branch": package.remote.branch if package.remote is not None else None, - "git_url": package.remote.git_url if package.remote is not None else None, - "path": package.remote.path if package.remote is not None else None, - "web_url": package.remote.web_url if package.remote is not None else None, - "source": package.remote.source.value if package.remote is not None else None, + "branch": package.remote.branch, + "git_url": package.remote.git_url, + "path": package.remote.path, + "web_url": package.remote.web_url, + "source": package.remote.source.value, "packager": package.packager, } ) @@ -270,5 +270,4 @@ class PackageOperations(Operations): return { package_base: package.remote for package_base, package in packages.items() - if package.remote is not None } diff --git a/src/ahriman/core/gitremote/remote_push.py b/src/ahriman/core/gitremote/remote_push.py index 2e175f47..769c131e 100644 --- a/src/ahriman/core/gitremote/remote_push.py +++ b/src/ahriman/core/gitremote/remote_push.py @@ -39,7 +39,7 @@ class RemotePush(LazyLogging): sync PKGBUILDs to remote repository after actions Attributes: - commit_author(str | None): optional commit author in form of git config (i.e. ``user ``) + commit_author(tuple[str, str] | None): optional commit author in form of git config database(SQLite): database instance remote_source(RemoteSource): repository remote source (remote pull url and branch) """ @@ -54,7 +54,11 @@ class RemotePush(LazyLogging): section(str): settings section name """ self.database = database - self.commit_author = configuration.get(section, "commit_author", fallback=None) + + commit_email = configuration.get(section, "commit_email", fallback="ahriman@localhost") + commit_user = configuration.get(section, "commit_user", fallback="ahriman") + self.commit_author = (commit_user, commit_email) + self.remote_source = RemoteSource( git_url=configuration.get(section, "push_url"), web_url="", diff --git a/src/ahriman/core/gitremote/remote_push_trigger.py b/src/ahriman/core/gitremote/remote_push_trigger.py index 5475bda0..a2a9d47d 100644 --- a/src/ahriman/core/gitremote/remote_push_trigger.py +++ b/src/ahriman/core/gitremote/remote_push_trigger.py @@ -49,7 +49,10 @@ class RemotePushTrigger(Trigger): "gitremote": { "type": "dict", "schema": { - "commit_author": { + "commit_email": { + "type": "string", + }, + "commit_user": { "type": "string", }, "push_url": { diff --git a/src/ahriman/core/repository/repository.py b/src/ahriman/core/repository/repository.py index e22dcfea..dbedc455 100644 --- a/src/ahriman/core/repository/repository.py +++ b/src/ahriman/core/repository/repository.py @@ -117,8 +117,9 @@ class Repository(Executor, UpdateHandler): # we are iterating over bases, not single packages for full_path in packages: try: - local = Package.from_archive(full_path, self.pacman, None) - local.remote = sources.get(local.base) + local = Package.from_archive(full_path, self.pacman) + if (source := sources.get(local.base)) is not None: + local.remote = source current = result.setdefault(local.base, local) if current.version != local.version: diff --git a/src/ahriman/core/repository/update_handler.py b/src/ahriman/core/repository/update_handler.py index a3246c27..95a91703 100644 --- a/src/ahriman/core/repository/update_handler.py +++ b/src/ahriman/core/repository/update_handler.py @@ -24,6 +24,7 @@ from ahriman.core.exceptions import UnknownPackageError from ahriman.core.repository.cleaner import Cleaner from ahriman.models.package import Package from ahriman.models.package_source import PackageSource +from ahriman.models.remote_source import RemoteSource class UpdateHandler(Cleaner): @@ -55,12 +56,10 @@ class UpdateHandler(Cleaner): list[Package]: list of packages which are out-of-dated """ def load_remote(package: Package) -> Package: - source = package.remote.source if package.remote is not None else None - # try to load package from base and if none found try to load by separated packages for probe in [package.base] + sorted(package.packages.keys()): try: - if source == PackageSource.Repository: + if package.remote.source == PackageSource.Repository: return Package.from_official(probe, self.pacman, None) return Package.from_aur(probe, self.pacman, None) except UnknownPackageError: @@ -71,6 +70,8 @@ class UpdateHandler(Cleaner): for local in self.packages(): with self.in_package_context(local.base): + if not local.remote.is_remote: + continue # avoid checking local packages if local.base in self.ignore_list: continue if filter_packages and local.base not in filter_packages: @@ -107,7 +108,15 @@ class UpdateHandler(Cleaner): for cache_dir in self.paths.cache.iterdir(): with self.in_package_context(cache_dir.name): try: - Sources.fetch(cache_dir, remote=None) + source = RemoteSource( + source=PackageSource.Local, + git_url=cache_dir.absolute().as_uri(), + web_url="", + path=".", + branch="master", + ) + + Sources.fetch(cache_dir, source) remote = Package.from_build(cache_dir, self.architecture, None) local = packages.get(remote.base) diff --git a/src/ahriman/core/spawn.py b/src/ahriman/core/spawn.py index c29e6243..edf115a0 100644 --- a/src/ahriman/core/spawn.py +++ b/src/ahriman/core/spawn.py @@ -27,7 +27,6 @@ from multiprocessing import Process, Queue from threading import Lock, Thread from ahriman.core.log import LazyLogging -from ahriman.models.package_source import PackageSource class Spawn(Thread, LazyLogging): @@ -133,8 +132,7 @@ class Spawn(Thread, LazyLogging): username(str | None): optional override of username for build process now(bool): build packages now """ - # avoid abusing by building non-aur packages - kwargs = {"source": PackageSource.AUR.value, "username": username} + kwargs = {"username": username} if now: kwargs["now"] = "" self._spawn_process("package-add", *packages, **kwargs) diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py index 0fcce09a..50acb647 100644 --- a/src/ahriman/core/util.py +++ b/src/ahriman/core/util.py @@ -304,7 +304,7 @@ def parse_version(version: str) -> tuple[str | None, str, str]: def partition(source: list[T], predicate: Callable[[T], bool]) -> tuple[list[T], list[T]]: """ - partition list into two based on predicate, based on # https://docs.python.org/dev/library/itertools.html#itertools-recipes + partition list into two based on predicate, based on https://docs.python.org/dev/library/itertools.html#itertools-recipes Args: source(list[T]): source list to be partitioned diff --git a/src/ahriman/models/aur_package.py b/src/ahriman/models/aur_package.py index fb7cc82d..8b9e620e 100644 --- a/src/ahriman/models/aur_package.py +++ b/src/ahriman/models/aur_package.py @@ -66,13 +66,12 @@ class AURPackage: >>> package = AURPackage.from_repo(metadata) # load package from official repository RPC >>> # properties of the class are built based on ones from AUR RPC, thus additional method is required >>> - >>> >>> from ahriman.core.alpm.pacman import Pacman >>> from ahriman.core.configuration import Configuration >>> >>> configuration = Configuration() >>> pacman = Pacman("x86_64", configuration) - >>> metadata = pacman.get("pacman") + >>> metadata = pacman.package_get("pacman") >>> package = AURPackage.from_pacman(next(metadata)) # load package from pyalpm wrapper """ diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index b911d48d..b89d766b 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -51,7 +51,7 @@ class Package(LazyLogging): packager(str | None): package packager if available packages(dict[str, PackageDescription): map of package names to their properties. Filled only on load from archive - remote(RemoteSource | None): package remote source if applicable + remote(RemoteSource): package remote source if applicable version(str): package full version Examples: @@ -61,7 +61,7 @@ class Package(LazyLogging): it will contain every data available in the json body. Otherwise, if generate package from local archive:: - >>> package = Package.from_archive(local_path, pacman, remote=None) + >>> package = Package.from_archive(local_path, pacman) it will probably miss file descriptions (in case if there are multiple packages which belong to the base). @@ -76,7 +76,7 @@ class Package(LazyLogging): base: str version: str - remote: RemoteSource | None + remote: RemoteSource packages: dict[str, PackageDescription] packager: str | None = None @@ -192,22 +192,26 @@ class Package(LazyLogging): return sorted(packages) @classmethod - def from_archive(cls, path: Path, pacman: Pacman, remote: RemoteSource | None) -> Self: + def from_archive(cls, path: Path, pacman: Pacman) -> Self: """ construct package properties from package archive Args: path(Path): path to package archive pacman(Pacman): alpm wrapper instance - remote(RemoteSource): package remote source if applicable Returns: Self: package properties """ package = pacman.handle.load_pkg(str(path)) description = PackageDescription.from_package(package, path) - return cls(base=package.base, version=package.version, remote=remote, packages={package.name: description}, - packager=package.packager) + return cls( + base=package.base, + version=package.version, + remote=RemoteSource(source=PackageSource.Archive), + packages={package.name: description}, + packager=package.packager, + ) @classmethod def from_aur(cls, name: str, pacman: Pacman, packager: str | None = None) -> Self: @@ -223,7 +227,15 @@ class Package(LazyLogging): Self: package properties """ package = AUR.info(name, pacman=pacman) - remote = RemoteSource.from_source(PackageSource.AUR, package.package_base, package.repository) + + remote = RemoteSource( + source=PackageSource.AUR, + git_url=AUR.remote_git_url(package.package_base, package.repository), + web_url=AUR.remote_web_url(package.package_base), + path=".", + branch="master", + ) + return cls( base=package.package_base, version=package.version, @@ -265,14 +277,20 @@ class Package(LazyLogging): version = full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"]) remote = RemoteSource( + source=PackageSource.Local, git_url=path.absolute().as_uri(), - web_url="", + web_url=None, path=".", branch="master", - source=PackageSource.Local, ) - return cls(base=srcinfo["pkgbase"], version=version, remote=remote, packages=packages, packager=packager) + return cls( + base=srcinfo["pkgbase"], + version=version, + remote=remote, + packages=packages, + packager=packager, + ) @classmethod def from_json(cls, dump: dict[str, Any]) -> Self: @@ -291,8 +309,13 @@ class Package(LazyLogging): for key, value in packages_json.items() } remote = dump.get("remote") or {} - return cls(base=dump["base"], version=dump["version"], remote=RemoteSource.from_json(remote), packages=packages, - packager=dump.get("packager")) + return cls( + base=dump["base"], + version=dump["version"], + remote=RemoteSource.from_json(remote), + packages=packages, + packager=dump.get("packager"), + ) @classmethod def from_official(cls, name: str, pacman: Pacman, packager: str | None = None, *, use_syncdb: bool = True) -> Self: @@ -309,7 +332,15 @@ class Package(LazyLogging): Self: package properties """ package = OfficialSyncdb.info(name, pacman=pacman) if use_syncdb else Official.info(name, pacman=pacman) - remote = RemoteSource.from_source(PackageSource.Repository, package.package_base, package.repository) + + remote = RemoteSource( + source=PackageSource.Repository, + git_url=Official.remote_git_url(package.package_base, package.repository), + web_url=Official.remote_web_url(package.package_base), + path=".", + branch="main", + ) + return cls( base=package.package_base, version=package.version, diff --git a/src/ahriman/models/package_description.py b/src/ahriman/models/package_description.py index 6a41a260..7154e217 100644 --- a/src/ahriman/models/package_description.py +++ b/src/ahriman/models/package_description.py @@ -53,14 +53,13 @@ class PackageDescription: >>> description = PackageDescription.from_json(dump) >>> - >>> >>> from pathlib import Path >>> from ahriman.core.alpm.pacman import Pacman >>> from ahriman.core.configuration import Configuration >>> >>> configuration = Configuration() >>> pacman = Pacman("x86_64", configuration) - >>> pyalpm_description = next(package for package in pacman.get("pacman")) + >>> pyalpm_description = next(package for package in pacman.package_get("pacman")) >>> description = PackageDescription.from_package( >>> pyalpm_description, Path("/var/cache/pacman/pkg/pacman-6.0.1-4-x86_64.pkg.tar.zst")) """ diff --git a/src/ahriman/models/package_source.py b/src/ahriman/models/package_source.py index ee9b6565..2dfeb61c 100644 --- a/src/ahriman/models/package_source.py +++ b/src/ahriman/models/package_source.py @@ -24,6 +24,7 @@ from pathlib import Path from urllib.parse import urlparse from ahriman.core.util import package_like +from ahriman.models.repository_paths import RepositoryPaths class PackageSource(str, Enum): @@ -42,7 +43,7 @@ class PackageSource(str, Enum): Examples: In case if source is unknown the ``resolve()`` and the source descriptor is available method must be used:: - >>> real_source = PackageSource.Auto.resolve("ahriman") + >>> real_source = PackageSource.Auto.resolve("ahriman", configuration.repository_paths) the code above will ensure that the presudo-source ``PackageSource.Auto`` will not be processed later. """ @@ -55,12 +56,13 @@ class PackageSource(str, Enum): Remote = "remote" Repository = "repository" - def resolve(self, source: str) -> PackageSource: + def resolve(self, source: str, paths: RepositoryPaths) -> PackageSource: """ resolve auto into the correct type Args: source(str): source of the package + paths(RepositoryPaths): repository paths instance Returns: PackageSource: non-auto type of the package source @@ -74,7 +76,7 @@ class PackageSource(str, Enum): if maybe_url.scheme and maybe_url.scheme not in ("data", "file") and package_like(maybe_path): return PackageSource.Remote try: - if (maybe_path / "PKGBUILD").is_file(): + if (maybe_path / "PKGBUILD").is_file() or paths.cache_for(source).is_dir(): return PackageSource.Local if maybe_path.is_dir(): return PackageSource.Directory diff --git a/src/ahriman/models/remote_source.py b/src/ahriman/models/remote_source.py index 39552a44..5d7cfaa1 100644 --- a/src/ahriman/models/remote_source.py +++ b/src/ahriman/models/remote_source.py @@ -21,6 +21,7 @@ from dataclasses import dataclass, fields from pathlib import Path from typing import Any, Self +from ahriman.core.exceptions import InitializeError from ahriman.core.util import dataclass_view, filter_json from ahriman.models.package_source import PackageSource @@ -31,18 +32,18 @@ class RemoteSource: remote package source properties Attributes: - branch(str): branch of the git repository - git_url(str): url of the git repository - path(str): path to directory with PKGBUILD inside the git repository + branch(str | None): branch of the git repository + git_url(str | None): url of the git repository + path(str | None): path to directory with PKGBUILD inside the git repository source(PackageSource): package source pointer used by some parsers - web_url(str): url of the package in the web interface + web_url(str | None): url of the package in the web interface """ - git_url: str - web_url: str - path: str - branch: str source: PackageSource + git_url: str | None = None + web_url: str | None = None + path: str | None = None + branch: str | None = None def __post_init__(self) -> None: """ @@ -51,17 +52,27 @@ class RemoteSource: object.__setattr__(self, "source", PackageSource(self.source)) @property - def pkgbuild_dir(self) -> Path: + def is_remote(self) -> bool: + """ + check if source is remote + + Returns: + bool: True in case if package is well-known remote source (e.g. AUR) and False otherwise + """ + return self.source in (PackageSource.AUR, PackageSource.Repository) + + @property + def pkgbuild_dir(self) -> Path | None: """ get path to directory with package sources (PKGBUILD etc) Returns: - Path: path to directory with package sources based on settings + Path | None: path to directory with package sources based on settings if available """ - return Path(self.path) + return Path(self.path) if self.path is not None else None @classmethod - def from_json(cls, dump: dict[str, Any]) -> Self | None: + def from_json(cls, dump: dict[str, Any]) -> Self: """ construct remote source from the json dump (or database row) @@ -69,47 +80,25 @@ class RemoteSource: dump(dict[str, Any]): json dump body Returns: - Self | None: remote source + Self: remote source """ # filter to only known fields known_fields = [pair.name for pair in fields(cls)] - dump = filter_json(dump, known_fields) - if dump: - return cls(**dump) - return None + return cls(**filter_json(dump, known_fields)) - @classmethod - def from_source(cls, source: PackageSource, package_base: str, repository: str) -> Self | None: + def git_source(self) -> tuple[str, str]: """ - generate remote source from the package base - - Args: - source(PackageSource): source of the package - package_base(str): package base - repository(str): repository name + get git source if available Returns: - Self | None: generated remote source if any, None otherwise + tuple[str, str]: git url and branch + + Raises: + InitializeError: in case if git url and/or branch are not set """ - if source == PackageSource.AUR: - from ahriman.core.alpm.remote import AUR - return cls( - git_url=AUR.remote_git_url(package_base, repository), - web_url=AUR.remote_web_url(package_base), - path=".", - branch="master", - source=source, - ) - if source == PackageSource.Repository: - from ahriman.core.alpm.remote import Official - return cls( - git_url=Official.remote_git_url(package_base, repository), - web_url=Official.remote_web_url(package_base), - path=".", - branch="main", - source=source, - ) - return None + if self.git_url is None or self.branch is None: + raise InitializeError("Remote source is empty") + return self.git_url, self.branch def view(self) -> dict[str, Any]: """ diff --git a/src/ahriman/models/user.py b/src/ahriman/models/user.py index 494a5685..35e6c0db 100644 --- a/src/ahriman/models/user.py +++ b/src/ahriman/models/user.py @@ -41,7 +41,7 @@ class User: Simply create user from database data and perform required validation:: >>> password = User.generate_password(24) - >>> user = User(username="ahriman", password=password, access=UserAccess.Full, packager_id=None, key=None) + >>> user = User(username="ahriman", password=password, access=UserAccess.Full) Since the password supplied may be plain text, the ``hash_password`` method can be used to hash the password:: @@ -63,8 +63,8 @@ class User: username: str password: str access: UserAccess - packager_id: str | None - key: str | None + packager_id: str | None = None + key: str | None = None _HASHER = sha512_crypt diff --git a/src/ahriman/web/schemas/remote_schema.py b/src/ahriman/web/schemas/remote_schema.py index c8dfa822..9e2d6b84 100644 --- a/src/ahriman/web/schemas/remote_schema.py +++ b/src/ahriman/web/schemas/remote_schema.py @@ -27,22 +27,22 @@ class RemoteSchema(Schema): request and response package remote schema """ - branch = fields.String(required=True, metadata={ + branch = fields.String(metadata={ "description": "Repository branch", "example": "master", }) - git_url = fields.String(required=True, metadata={ + git_url = fields.String(metadata={ "description": "Package git url", "example": "https://aur.archlinux.org/ahriman.git", }) - path = fields.String(required=True, metadata={ + path = fields.String(metadata={ "description": "Path to package sources in git repository", "example": ".", }) source = fields.Enum(PackageSource, by_value=True, required=True, metadata={ "description": "Pacakge source", }) - web_url = fields.String(required=True, metadata={ + web_url = fields.String(metadata={ "description": "Package repository page", "example": "https://aur.archlinux.org/packages/ahriman", }) diff --git a/tests/ahriman/application/application/test_application_packages.py b/tests/ahriman/application/application/test_application_packages.py index 1e96a7f4..56cfb121 100644 --- a/tests/ahriman/application/application/test_application_packages.py +++ b/tests/ahriman/application/application/test_application_packages.py @@ -86,7 +86,9 @@ def test_add_local(application_packages: ApplicationPackages, package_ahriman: P application_packages._add_local(package_ahriman.base, "packager") is_dir_mock.assert_called_once_with() copytree_mock.assert_called_once_with( - Path(package_ahriman.base), application_packages.repository.paths.cache_for(package_ahriman.base)) + Path(package_ahriman.base), + application_packages.repository.paths.cache_for(package_ahriman.base), + dirs_exist_ok=True) init_mock.assert_called_once_with(application_packages.repository.paths.cache_for(package_ahriman.base)) build_queue_mock.assert_called_once_with(package_ahriman) diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py index 2a6c4d92..12b96d86 100644 --- a/tests/ahriman/conftest.py +++ b/tests/ahriman/conftest.py @@ -7,6 +7,7 @@ from typing import Any, TypeVar from unittest.mock import MagicMock from ahriman.core.alpm.pacman import Pacman +from ahriman.core.alpm.remote import AUR from ahriman.core.auth import Auth from ahriman.core.configuration import Configuration from ahriman.core.database import SQLite @@ -314,7 +315,13 @@ def package_python_schedule( return Package( base="python-schedule", version="1.0.0-2", - remote=RemoteSource.from_source(PackageSource.AUR, "python-schedule", "aur"), + remote=RemoteSource( + source=PackageSource.AUR, + git_url=AUR.remote_git_url("python-schedule", "aur"), + web_url=AUR.remote_web_url("python-schedule"), + path=".", + branch="master", + ), packages=packages) @@ -451,7 +458,13 @@ def remote_source() -> RemoteSource: Returns: RemoteSource: remote source test instance """ - return RemoteSource.from_source(PackageSource.AUR, "ahriman", "aur") + return RemoteSource( + source=PackageSource.AUR, + git_url=AUR.remote_git_url("ahriman", "aur"), + web_url=AUR.remote_web_url("ahriman"), + path=".", + branch="master", + ) @pytest.fixture diff --git a/tests/ahriman/core/alpm/test_pacman.py b/tests/ahriman/core/alpm/test_pacman.py index a22ac2ea..63644055 100644 --- a/tests/ahriman/core/alpm/test_pacman.py +++ b/tests/ahriman/core/alpm/test_pacman.py @@ -8,6 +8,7 @@ from unittest.mock import MagicMock from ahriman.core.alpm.pacman import Pacman from ahriman.core.configuration import Configuration +from ahriman.models.pacman_synchronization import PacmanSynchronization from ahriman.models.repository_paths import RepositoryPaths @@ -23,7 +24,7 @@ def test_init_with_local_cache(configuration: Configuration, mocker: MockerFixtu with TemporaryDirectory(ignore_cleanup_errors=True) as pacman_root: mocker.patch.object(RepositoryPaths, "pacman", Path(pacman_root)) # during the creation pyalpm.Handle will create also version file which we would like to remove later - pacman = Pacman("x86_64", configuration, refresh_database=1) + pacman = Pacman("x86_64", configuration, refresh_database=PacmanSynchronization.Enabled) assert pacman.handle sync_mock.assert_called_once_with(pytest.helpers.anyvar(int), force=False) @@ -40,7 +41,7 @@ def test_init_with_local_cache_forced(configuration: Configuration, mocker: Mock with TemporaryDirectory(ignore_cleanup_errors=True) as pacman_root: mocker.patch.object(RepositoryPaths, "pacman", Path(pacman_root)) # during the creation pyalpm.Handle will create also version file which we would like to remove later - pacman = Pacman("x86_64", configuration, refresh_database=2) + pacman = Pacman("x86_64", configuration, refresh_database=PacmanSynchronization.Force) assert pacman.handle sync_mock.assert_called_once_with(pytest.helpers.anyvar(int), force=True) @@ -54,7 +55,7 @@ def test_database_copy(pacman: Pacman, repository_paths: RepositoryPaths, mocker dst_path = Path("/var/lib/pacman/sync/core.db") mocker.patch("pathlib.Path.is_dir", return_value=True) # root database exists, local database does not - mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: True if p.is_relative_to(path) else False) + mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path)) copy_mock = mocker.patch("shutil.copy") chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown") @@ -71,7 +72,7 @@ def test_database_copy_skip(pacman: Pacman, repository_paths: RepositoryPaths, m path = Path("randomname") mocker.patch("pathlib.Path.is_dir", return_value=True) # root database exists, local database does not - mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: True if p.is_relative_to(path) else False) + mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path)) copy_mock = mocker.patch("shutil.copy") pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=False) @@ -86,7 +87,7 @@ def test_database_copy_no_directory(pacman: Pacman, repository_paths: Repository path = Path("randomname") mocker.patch("pathlib.Path.is_dir", return_value=False) # root database exists, local database does not - mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: True if p.is_relative_to(path) else False) + mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path)) copy_mock = mocker.patch("shutil.copy") pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=True) diff --git a/tests/ahriman/core/build_tools/test_sources.py b/tests/ahriman/core/build_tools/test_sources.py index 67adfbe6..137e3185 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.mock import call as MockCall from ahriman.core.build_tools.sources import Sources from ahriman.models.package import Package +from ahriman.models.package_source import PackageSource from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.remote_source import RemoteSource from ahriman.models.repository_paths import RepositoryPaths @@ -92,7 +93,7 @@ def test_fetch_new_without_remote(mocker: MockerFixture) -> None: move_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.move") local = Path("local") - Sources.fetch(local, None) + Sources.fetch(local, RemoteSource(source=PackageSource.Archive)) check_output_mock.assert_has_calls([ MockCall("git", "checkout", "--force", Sources.DEFAULT_BRANCH, cwd=local, logger=pytest.helpers.anyvar(int)), MockCall("git", "reset", "--hard", f"origin/{Sources.DEFAULT_BRANCH}", @@ -136,6 +137,7 @@ def test_init(mocker: MockerFixture) -> None: must create empty repository at the specified path """ mocker.patch("ahriman.models.package.Package.local_files", return_value=[Path("local")]) + mocker.patch("pathlib.Path.is_dir", return_value=False) add_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.add") check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") commit_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.commit") @@ -145,7 +147,21 @@ def test_init(mocker: MockerFixture) -> None: check_output_mock.assert_called_once_with("git", "init", "--initial-branch", Sources.DEFAULT_BRANCH, cwd=local, logger=pytest.helpers.anyvar(int)) add_mock.assert_called_once_with(local, "PKGBUILD", ".SRCINFO", "local") - commit_mock.assert_called_once_with(local, author="ahriman ") + commit_mock.assert_called_once_with(local) + + +def test_init_skip(mocker: MockerFixture) -> None: + """ + must skip git init if it was already + """ + mocker.patch("ahriman.models.package.Package.local_files", return_value=[Path("local")]) + mocker.patch("pathlib.Path.is_dir", return_value=True) + mocker.patch("ahriman.core.build_tools.sources.Sources.add") + mocker.patch("ahriman.core.build_tools.sources.Sources.commit") + check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + + Sources.init(Path("local")) + check_output_mock.assert_not_called() def test_load(package_ahriman: Package, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: @@ -216,19 +232,31 @@ def test_push(package_ahriman: Package, mocker: MockerFixture) -> None: must correctly push files to remote repository """ add_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.add") - commit_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.commit") + commit_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.commit", return_value=True) check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") - author = "commit author " + commit_author = ("commit author", "user@host") local = Path("local") - Sources.push(Path("local"), package_ahriman.remote, "glob", commit_author=author) + Sources.push(local, package_ahriman.remote, "glob", commit_author=commit_author) add_mock.assert_called_once_with(local, "glob") - commit_mock.assert_called_once_with(local, author=author) + commit_mock.assert_called_once_with(local, commit_author=commit_author) check_output_mock.assert_called_once_with( "git", "push", package_ahriman.remote.git_url, package_ahriman.remote.branch, cwd=local, logger=pytest.helpers.anyvar(int)) +def test_push_skipped(package_ahriman: Package, mocker: MockerFixture) -> None: + """ + must skip push if no changes were committed + """ + mocker.patch("ahriman.core.build_tools.sources.Sources.add") + mocker.patch("ahriman.core.build_tools.sources.Sources.commit", return_value=False) + check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + + Sources.push(Path("local"), package_ahriman.remote) + check_output_mock.assert_not_called() + + def test_add(sources: Sources, mocker: MockerFixture) -> None: """ must add files to git @@ -274,29 +302,54 @@ def test_commit(sources: Sources, mocker: MockerFixture) -> None: """ must commit changes """ + mocker.patch("ahriman.core.build_tools.sources.Sources.has_changes", return_value=True) check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") local = Path("local") message = "Commit message" - sources.commit(local, message=message) + user, email = sources.DEFAULT_COMMIT_AUTHOR + assert sources.commit(local, message=message) check_output_mock.assert_called_once_with( - "git", "commit", "--allow-empty", "--message", message, cwd=local, logger=pytest.helpers.anyvar(int) + "git", "commit", "--message", message, + cwd=local, logger=pytest.helpers.anyvar(int), environment={ + "GIT_AUTHOR_NAME": user, + "GIT_AUTHOR_EMAIL": email, + "GIT_COMMITTER_NAME": user, + "GIT_COMMITTER_EMAIL": email, + } ) +def test_commit_no_changes(sources: Sources, mocker: MockerFixture) -> None: + """ + must skip commit if there are no changes + """ + mocker.patch("ahriman.core.build_tools.sources.Sources.has_changes", return_value=False) + check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") + + assert not sources.commit(Path("local")) + check_output_mock.assert_not_called() + + def test_commit_author(sources: Sources, mocker: MockerFixture) -> None: """ must commit changes with commit author """ + mocker.patch("ahriman.core.build_tools.sources.Sources.has_changes", return_value=True) check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") local = Path("local") message = "Commit message" - author = "commit author " - sources.commit(Path("local"), message=message, author=author) + user, email = author = ("commit author", "user@host") + assert sources.commit(Path("local"), message=message, commit_author=author) check_output_mock.assert_called_once_with( - "git", "commit", "--allow-empty", "--message", message, "--author", author, - cwd=local, logger=pytest.helpers.anyvar(int) + "git", "commit", "--message", message, + cwd=local, logger=pytest.helpers.anyvar(int), environment={ + "GIT_AUTHOR_NAME": user, + "GIT_AUTHOR_EMAIL": email, + "GIT_COMMITTER_NAME": user, + "GIT_COMMITTER_EMAIL": email, + } ) @@ -304,13 +357,20 @@ def test_commit_autogenerated_message(sources: Sources, mocker: MockerFixture) - """ must commit changes with autogenerated commit message """ + mocker.patch("ahriman.core.build_tools.sources.Sources.has_changes", return_value=True) check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output") local = Path("local") - sources.commit(Path("local")) + assert sources.commit(Path("local")) + user, email = sources.DEFAULT_COMMIT_AUTHOR check_output_mock.assert_called_once_with( - "git", "commit", "--allow-empty", "--message", pytest.helpers.anyvar(str, strict=True), - cwd=local, logger=pytest.helpers.anyvar(int) + "git", "commit", "--message", pytest.helpers.anyvar(str, strict=True), + cwd=local, logger=pytest.helpers.anyvar(int), environment={ + "GIT_AUTHOR_NAME": user, + "GIT_AUTHOR_EMAIL": email, + "GIT_COMMITTER_NAME": user, + "GIT_COMMITTER_EMAIL": email, + } ) @@ -325,6 +385,23 @@ def test_diff(sources: Sources, mocker: MockerFixture) -> None: check_output_mock.assert_called_once_with("git", "diff", cwd=local, logger=pytest.helpers.anyvar(int)) +def test_has_changes(sources: Sources, mocker: MockerFixture) -> None: + """ + must correctly identify if there are changes + """ + local = Path("local") + + check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output", return_value="M a.txt") + assert sources.has_changes(local) + check_output_mock.assert_called_once_with("git", "diff", "--cached", "--name-only", + cwd=local, logger=pytest.helpers.anyvar(int)) + + check_output_mock = mocker.patch("ahriman.core.build_tools.sources.Sources._check_output", return_value="") + assert not sources.has_changes(local) + check_output_mock.assert_called_once_with("git", "diff", "--cached", "--name-only", + cwd=local, logger=pytest.helpers.anyvar(int)) + + def test_move(sources: Sources, mocker: MockerFixture) -> None: """ must move content between directories diff --git a/tests/ahriman/core/database/migrations/test_m001_package_source.py b/tests/ahriman/core/database/migrations/test_m001_package_source.py index e5e67329..61cca58d 100644 --- a/tests/ahriman/core/database/migrations/test_m001_package_source.py +++ b/tests/ahriman/core/database/migrations/test_m001_package_source.py @@ -66,18 +66,3 @@ def test_migrate_package_remotes_vcs(package_ahriman: Package, connection: Conne migrate_package_remotes(connection, repository_paths) connection.execute.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), pytest.helpers.anyvar(int)) - - -def test_migrate_package_remotes_no_remotes(package_ahriman: Package, connection: Connection, - repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: - """ - must skip processing in case if no remotes generated (should never happen) - """ - mocker.patch( - "ahriman.core.database.operations.PackageOperations._packages_get_select_package_bases", - return_value={package_ahriman.base: package_ahriman}) - mocker.patch("pathlib.Path.exists", return_value=False) - mocker.patch("ahriman.models.remote_source.RemoteSource.from_source", return_value=None) - - migrate_package_remotes(connection, repository_paths) - connection.execute.assert_not_called() diff --git a/tests/ahriman/core/database/migrations/test_m005_make_opt_depends.py b/tests/ahriman/core/database/migrations/test_m005_make_opt_depends.py index c392a540..83335a17 100644 --- a/tests/ahriman/core/database/migrations/test_m005_make_opt_depends.py +++ b/tests/ahriman/core/database/migrations/test_m005_make_opt_depends.py @@ -35,7 +35,7 @@ def test_migrate_package_depends(connection: Connection, configuration: Configur migrate_package_depends(connection, configuration) package_mock.assert_called_once_with( - package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int), remote=None) + package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int)) connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{ "make_depends": package_ahriman.packages[package_ahriman.base].make_depends, "opt_depends": package_ahriman.packages[package_ahriman.base].opt_depends, diff --git a/tests/ahriman/core/database/migrations/test_m007_check_depends.py b/tests/ahriman/core/database/migrations/test_m007_check_depends.py index 53908bee..2f537af6 100644 --- a/tests/ahriman/core/database/migrations/test_m007_check_depends.py +++ b/tests/ahriman/core/database/migrations/test_m007_check_depends.py @@ -35,7 +35,7 @@ def test_migrate_package_depends(connection: Connection, configuration: Configur migrate_package_check_depends(connection, configuration) package_mock.assert_called_once_with( - package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int), remote=None) + package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int)) connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{ "check_depends": package_ahriman.packages[package_ahriman.base].check_depends, "package": package_ahriman.base, diff --git a/tests/ahriman/core/database/migrations/test_m008_packagers.py b/tests/ahriman/core/database/migrations/test_m008_packagers.py index 71e626ed..f3861fcc 100644 --- a/tests/ahriman/core/database/migrations/test_m008_packagers.py +++ b/tests/ahriman/core/database/migrations/test_m008_packagers.py @@ -35,7 +35,7 @@ def test_migrate_package_base_packager(connection: Connection, configuration: Co migrate_package_base_packager(connection, configuration) package_mock.assert_called_once_with( - package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int), remote=None) + package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int)) connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{ "package_base": package_ahriman.base, "packager": package_ahriman.packager, diff --git a/tests/ahriman/core/database/migrations/test_m009_local_source.py b/tests/ahriman/core/database/migrations/test_m009_local_source.py new file mode 100644 index 00000000..4c63ef3e --- /dev/null +++ b/tests/ahriman/core/database/migrations/test_m009_local_source.py @@ -0,0 +1,8 @@ +from ahriman.core.database.migrations.m009_local_source import steps + + +def test_migration_packagers() -> None: + """ + migration must not be empty + """ + assert steps diff --git a/tests/ahriman/core/database/operations/test_package_operations.py b/tests/ahriman/core/database/operations/test_package_operations.py index 5e7309c3..7ad2cee7 100644 --- a/tests/ahriman/core/database/operations/test_package_operations.py +++ b/tests/ahriman/core/database/operations/test_package_operations.py @@ -193,7 +193,7 @@ def test_remote_update_update(database: SQLite, package_ahriman: Package) -> Non must perform package remote update for existing package """ database.remote_update(package_ahriman) - remote_source = RemoteSource.from_source(PackageSource.Repository, package_ahriman.base, "community") + remote_source = RemoteSource(source=PackageSource.Repository) package_ahriman.remote = remote_source database.remote_update(package_ahriman) diff --git a/tests/ahriman/core/repository/test_repository.py b/tests/ahriman/core/repository/test_repository.py index e45ff345..413d2aca 100644 --- a/tests/ahriman/core/repository/test_repository.py +++ b/tests/ahriman/core/repository/test_repository.py @@ -11,6 +11,8 @@ from ahriman.core.repository import Repository from ahriman.core.sign.gpg import GPG from ahriman.models.context_key import ContextKey from ahriman.models.package import Package +from ahriman.models.package_source import PackageSource +from ahriman.models.remote_source import RemoteSource def test_load(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> None: @@ -51,6 +53,9 @@ def test_load_archives(package_ahriman: Package, package_python_schedule: Packag for package, props in package_python_schedule.packages.items() ] + [package_ahriman] mocker.patch("ahriman.models.package.Package.from_archive", side_effect=single_packages) + mocker.patch("ahriman.core.database.SQLite.remotes_get", return_value={ + package_ahriman.base: package_ahriman.base + }) packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")]) assert len(packages) == 2 diff --git a/tests/ahriman/core/repository/test_update_handler.py b/tests/ahriman/core/repository/test_update_handler.py index 506d7a47..1d355b98 100644 --- a/tests/ahriman/core/repository/test_update_handler.py +++ b/tests/ahriman/core/repository/test_update_handler.py @@ -42,7 +42,7 @@ def test_updates_aur_official(update_handler: UpdateHandler, package_ahriman: Pa """ must provide updates based on repository data """ - package_ahriman.remote = RemoteSource.from_source(PackageSource.Repository, package_ahriman.base, "community") + package_ahriman.remote = RemoteSource(source=PackageSource.Repository) mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) mocker.patch("ahriman.models.package.Package.from_official", return_value=package_ahriman) @@ -65,6 +65,19 @@ def test_updates_aur_failed(update_handler: UpdateHandler, package_ahriman: Pack status_client_mock.assert_called_once_with(package_ahriman.base) +def test_updates_aur_local(update_handler: UpdateHandler, package_ahriman: Package, + mocker: MockerFixture) -> None: + """ + must skip packages with local sources + """ + package_ahriman.remote = RemoteSource(source=PackageSource.Local) + mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman]) + package_load_mock = mocker.patch("ahriman.models.package.Package.from_aur") + + assert not update_handler.updates_aur([], vcs=True) + package_load_mock.assert_not_called() + + def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Package, package_python_schedule: Package, mocker: MockerFixture) -> None: """ @@ -150,7 +163,7 @@ def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package, package_is_outdated_mock = mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True) assert update_handler.updates_local(vcs=True) == [package_ahriman] - fetch_mock.assert_called_once_with(Path(package_ahriman.base), remote=None) + fetch_mock.assert_called_once_with(Path(package_ahriman.base), pytest.helpers.anyvar(int)) package_load_mock.assert_called_once_with(Path(package_ahriman.base), "x86_64", None) status_client_mock.assert_called_once_with(package_ahriman.base) package_is_outdated_mock.assert_called_once_with( diff --git a/tests/ahriman/core/test_spawn.py b/tests/ahriman/core/test_spawn.py index 625bc10c..9e3f6ddc 100644 --- a/tests/ahriman/core/test_spawn.py +++ b/tests/ahriman/core/test_spawn.py @@ -75,7 +75,7 @@ def test_packages_add(spawner: Spawn, mocker: MockerFixture) -> None: """ spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") spawner.packages_add(["ahriman", "linux"], None, now=False) - spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", username=None) + spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", username=None) def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None: @@ -84,7 +84,7 @@ def test_packages_add_with_build(spawner: Spawn, mocker: MockerFixture) -> None: """ spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") spawner.packages_add(["ahriman", "linux"], None, now=True) - spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", username=None, now="") + spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", username=None, now="") def test_packages_add_with_username(spawner: Spawn, mocker: MockerFixture) -> None: @@ -93,7 +93,7 @@ def test_packages_add_with_username(spawner: Spawn, mocker: MockerFixture) -> No """ spawn_mock = mocker.patch("ahriman.core.spawn.Spawn._spawn_process") spawner.packages_add(["ahriman", "linux"], "username", now=False) - spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", source="aur", username="username") + spawn_mock.assert_called_once_with("package-add", "ahriman", "linux", username="username") def test_packages_rebuild(spawner: Spawn, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/models/conftest.py b/tests/ahriman/models/conftest.py index 0ea86ccf..4b62b4fc 100644 --- a/tests/ahriman/models/conftest.py +++ b/tests/ahriman/models/conftest.py @@ -4,6 +4,7 @@ import pytest from unittest.mock import MagicMock, PropertyMock from ahriman import __version__ +from ahriman.core.alpm.remote import AUR from ahriman.models.aur_package import AURPackage from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.counters import Counters @@ -70,7 +71,13 @@ def package_tpacpi_bat_git() -> Package: return Package( base="tpacpi-bat-git", version="3.1.r12.g4959b52-1", - remote=RemoteSource.from_source(PackageSource.AUR, "tpacpi-bat-git", "aur"), + remote=RemoteSource( + source=PackageSource.AUR, + git_url=AUR.remote_git_url("tpacpi-bat-git", "aur"), + web_url=AUR.remote_web_url("tpacpi-bat-git"), + path=".", + branch="master", + ), packages={"tpacpi-bat-git": PackageDescription()}) diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py index 452b25df..e6f0e0c8 100644 --- a/tests/ahriman/models/test_package.py +++ b/tests/ahriman/models/test_package.py @@ -158,7 +158,10 @@ def test_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker """ mocker.patch("ahriman.models.package_description.PackageDescription.from_package", return_value=package_ahriman.packages[package_ahriman.base]) - assert Package.from_archive(Path("path"), pyalpm_handle, package_ahriman.remote) == package_ahriman + generated = Package.from_archive(Path("path"), pyalpm_handle) + generated.remote = package_ahriman.remote + + assert generated == package_ahriman def test_from_aur(package_ahriman: Package, aur_package_ahriman: AURPackage, pacman: Pacman, diff --git a/tests/ahriman/models/test_package_source.py b/tests/ahriman/models/test_package_source.py index 22c55651..27c18b53 100644 --- a/tests/ahriman/models/test_package_source.py +++ b/tests/ahriman/models/test_package_source.py @@ -4,6 +4,7 @@ from pathlib import Path from ahriman.models.package_description import PackageDescription from ahriman.models.package_source import PackageSource +from ahriman.models.repository_paths import RepositoryPaths def _is_file_mock(is_any_file: bool, is_pkgbuild: bool) -> Callable[[Path], bool]: @@ -21,71 +22,86 @@ def _is_file_mock(is_any_file: bool, is_pkgbuild: bool) -> Callable[[Path], bool return side_effect -def test_resolve_non_auto() -> None: +def test_resolve_non_auto(repository_paths: RepositoryPaths) -> None: """ must resolve non auto type to itself """ for source in filter(lambda src: src != PackageSource.Auto, PackageSource): - assert source.resolve("") == source + assert source.resolve("", repository_paths) == source -def test_resolve_archive(package_description_ahriman: PackageDescription, mocker: MockerFixture) -> None: +def test_resolve_archive(package_description_ahriman: PackageDescription, repository_paths: RepositoryPaths, + mocker: MockerFixture) -> None: """ must resolve auto type into the archive """ mocker.patch("pathlib.Path.is_dir", return_value=False) mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=_is_file_mock(True, False)) - assert PackageSource.Auto.resolve(package_description_ahriman.filename) == PackageSource.Archive + assert PackageSource.Auto.resolve(package_description_ahriman.filename, repository_paths) == PackageSource.Archive -def test_resolve_aur(mocker: MockerFixture) -> None: +def test_resolve_aur(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: """ must resolve auto type into the AUR package """ mocker.patch("pathlib.Path.is_dir", return_value=False) mocker.patch("pathlib.Path.is_file", return_value=False) - assert PackageSource.Auto.resolve("package") == PackageSource.AUR + assert PackageSource.Auto.resolve("package", repository_paths) == PackageSource.AUR -def test_resolve_aur_not_package_like(mocker: MockerFixture) -> None: +def test_resolve_aur_not_package_like(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: """ must resolve auto type into the AUR package if it is file, but does not look like a package archive """ mocker.patch("pathlib.Path.is_dir", return_value=False) mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=_is_file_mock(True, False)) - assert PackageSource.Auto.resolve("package") == PackageSource.AUR + assert PackageSource.Auto.resolve("package", repository_paths) == PackageSource.AUR -def test_resolve_aur_no_access(mocker: MockerFixture) -> None: +def test_resolve_aur_no_access(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: """ must resolve auto type into the AUR package in case if we cannot read in suggested path """ mocker.patch("pathlib.Path.is_dir", side_effect=PermissionError()) - assert PackageSource.Auto.resolve("package") == PackageSource.AUR + assert PackageSource.Auto.resolve("package", repository_paths) == PackageSource.AUR -def test_resolve_directory(mocker: MockerFixture) -> None: +def test_resolve_directory(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: """ must resolve auto type into the directory """ - mocker.patch("pathlib.Path.is_dir", return_value=True) + mocker.patch("pathlib.Path.is_dir", autospec=True, side_effect=lambda p: p == Path("path")) mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=_is_file_mock(False, False)) - assert PackageSource.Auto.resolve("path") == PackageSource.Directory + assert PackageSource.Auto.resolve("path", repository_paths) == PackageSource.Directory -def test_resolve_local(mocker: MockerFixture) -> None: +def test_resolve_local(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: """ must resolve auto type into the local sources """ - mocker.patch("pathlib.Path.is_dir", return_value=True) + mocker.patch("pathlib.Path.is_dir", return_value=False) mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=_is_file_mock(True, True)) - assert PackageSource.Auto.resolve("path") == PackageSource.Local + assert PackageSource.Auto.resolve("path", repository_paths) == PackageSource.Local -def test_resolve_remote(package_description_ahriman: PackageDescription, mocker: MockerFixture) -> None: +def test_resolve_local_cache(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None: + """ + must resolve auto type into the local sources + """ + cache_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.cache_for", return_value=Path("cache")) + mocker.patch("pathlib.Path.is_dir", autospec=True, side_effect=lambda p: p == Path("cache")) + mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=_is_file_mock(False, False)) + + assert PackageSource.Auto.resolve("path", repository_paths) == PackageSource.Local + cache_mock.assert_called_once_with("path") + + +def test_resolve_remote(package_description_ahriman: PackageDescription, repository_paths: RepositoryPaths, + mocker: MockerFixture) -> None: """ must resolve auto type into the remote sources """ mocker.patch("pathlib.Path.is_dir", return_value=False) mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=_is_file_mock(False, False)) - assert PackageSource.Auto.resolve(f"https://host/{package_description_ahriman.filename}") == PackageSource.Remote + url = f"https://host/{package_description_ahriman.filename}" + assert PackageSource.Auto.resolve(url, repository_paths) == PackageSource.Remote diff --git a/tests/ahriman/models/test_remote_source.py b/tests/ahriman/models/test_remote_source.py index 9d9969fd..b1126b6a 100644 --- a/tests/ahriman/models/test_remote_source.py +++ b/tests/ahriman/models/test_remote_source.py @@ -1,7 +1,8 @@ -from pathlib import Path -from pytest_mock import MockerFixture +import pytest -from ahriman.models.package import Package +from pathlib import Path + +from ahriman.core.exceptions import InitializeError from ahriman.models.package_source import PackageSource from ahriman.models.remote_source import RemoteSource @@ -20,6 +21,14 @@ def test_post_init(remote_source: RemoteSource) -> None: assert remote == remote_source +def test_is_remote() -> None: + """ + must correctly define if source is remote or not + """ + for source in PackageSource: + assert RemoteSource(source=source).is_remote or source not in (PackageSource.AUR, PackageSource.Repository) + + def test_pkgbuild_dir(remote_source: RemoteSource) -> None: """ must return path as is in `path` property @@ -35,48 +44,16 @@ def test_from_json(remote_source: RemoteSource) -> None: assert RemoteSource.from_json(remote_source.view()) == remote_source -def test_from_json_empty() -> None: +def test_git_source(remote_source: RemoteSource) -> None: """ - must return None in case of empty dictionary, which is required by the database wrapper + must correctly return git source """ - assert RemoteSource.from_json({}) is None + assert remote_source.git_source() == (remote_source.git_url, remote_source.branch) -def test_from_source_aur(package_ahriman: Package, mocker: MockerFixture) -> None: +def test_git_source_empty() -> None: """ - must construct remote from AUR source + must raise exception if path is none """ - remote_git_url_mock = mocker.patch("ahriman.core.alpm.remote.AUR.remote_git_url") - remote_web_url_mock = mocker.patch("ahriman.core.alpm.remote.AUR.remote_web_url") - - remote = RemoteSource.from_source(PackageSource.AUR, package_ahriman.base, "aur") - remote_git_url_mock.assert_called_once_with(package_ahriman.base, "aur") - remote_web_url_mock.assert_called_once_with(package_ahriman.base) - assert remote.pkgbuild_dir == Path(".") - assert remote.branch == "master" - assert remote.source == PackageSource.AUR - - -def test_from_source_official(package_ahriman: Package, mocker: MockerFixture) -> None: - """ - must construct remote from official repository source - """ - remote_git_url_mock = mocker.patch("ahriman.core.alpm.remote.Official.remote_git_url") - remote_web_url_mock = mocker.patch("ahriman.core.alpm.remote.Official.remote_web_url") - - remote = RemoteSource.from_source(PackageSource.Repository, package_ahriman.base, "community") - remote_git_url_mock.assert_called_once_with(package_ahriman.base, "community") - remote_web_url_mock.assert_called_once_with(package_ahriman.base) - assert remote.pkgbuild_dir == Path(".") - assert remote.branch == "main" - assert remote.source == PackageSource.Repository - - -def test_from_source_other() -> None: - """ - must return None in case if source is not one of AUR or Repository - """ - assert RemoteSource.from_source(PackageSource.Archive, "package", "repository") is None - assert RemoteSource.from_source(PackageSource.Directory, "package", "repository") is None - assert RemoteSource.from_source(PackageSource.Local, "package", "repository") is None - assert RemoteSource.from_source(PackageSource.Remote, "package", "repository") is None + with pytest.raises(InitializeError): + RemoteSource(source=PackageSource.Remote).git_source() diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini index 57b632fa..b614fe30 100644 --- a/tests/testresources/core/ahriman.ini +++ b/tests/testresources/core/ahriman.ini @@ -48,7 +48,8 @@ target = gitremote target = gitremote [gitremote] -commit_author = "user " +commit_user = user +commit_email = user@host push_url = https://github.com/arcan1s/repository.git pull_url = https://github.com/arcan1s/repository.git