mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-07 02:55:49 +00:00
feat: changes screen implementation (#117)
Add support of changes generation. Changes will be generated (unless explicitly asked not to) automatically during check process (i.e. `repo-update --dry-run` and aliases) and uploaded to the remote server. Changes can be reviewed either by web interface or by special subcommands. Changes will be automatically cleared during next successful build
This commit is contained in:
@ -101,6 +101,8 @@ def _parser() -> argparse.ArgumentParser:
|
||||
_set_help_updates_parser(subparsers)
|
||||
_set_help_version_parser(subparsers)
|
||||
_set_package_add_parser(subparsers)
|
||||
_set_package_changes_parser(subparsers)
|
||||
_set_package_changes_remove_parser(subparsers)
|
||||
_set_package_remove_parser(subparsers)
|
||||
_set_package_status_parser(subparsers)
|
||||
_set_package_status_remove_parser(subparsers)
|
||||
@ -281,6 +283,44 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
return parser
|
||||
|
||||
|
||||
def _set_package_changes_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for package changes subcommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("package-changes", help="get package changes",
|
||||
description="retrieve package changes stored in database",
|
||||
epilog="This feature requests package status from the web interface if it is available.",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("package", help="package base")
|
||||
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
|
||||
parser.set_defaults(handler=handlers.Change, action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_package_changes_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for package change remove subcommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("package-changes-remove", help="remove package changes",
|
||||
description="remove the package changes stored remotely",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("package", help="package base")
|
||||
parser.set_defaults(handler=handlers.Change, action=Action.Remove, lock=None, quiet=True, report=False, unsafe=True)
|
||||
return parser
|
||||
|
||||
|
||||
def _set_package_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for package removal subcommand
|
||||
@ -493,6 +533,9 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
description="check for packages updates. Same as repo-update --dry-run --no-manual",
|
||||
formatter_class=_formatter)
|
||||
parser.add_argument("package", help="filter check by package base", nargs="*")
|
||||
parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
|
||||
"Only applicable in dry run mode",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
|
||||
parser.add_argument("--vcs", help="fetch actual version of VCS packages",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
@ -558,8 +601,12 @@ def _set_repo_daemon_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
parser.add_argument("-i", "--interval", help="interval between runs in seconds", type=int, default=60 * 60 * 12)
|
||||
parser.add_argument("--aur", help="enable or disable checking for AUR updates",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
|
||||
"Only applicable in dry run mode",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--dependencies", help="process missing package dependencies",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")
|
||||
parser.add_argument("--local", help="enable or disable checking of local packages for updates",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--manual", help="include or exclude manual updates",
|
||||
@ -569,7 +616,7 @@ def _set_repo_daemon_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
|
||||
"-yy to force refresh even if up to date",
|
||||
action="count", default=False)
|
||||
parser.set_defaults(handler=handlers.Daemon, dry_run=False, exit_code=False, package=[])
|
||||
parser.set_defaults(handler=handlers.Daemon, exit_code=False, package=[])
|
||||
return parser
|
||||
|
||||
|
||||
@ -769,6 +816,9 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
parser.add_argument("package", help="filter check by package base", nargs="*")
|
||||
parser.add_argument("--aur", help="enable or disable checking for AUR updates",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
|
||||
"Only applicable in dry run mode",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--dependencies", help="process missing package dependencies",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")
|
||||
|
@ -150,7 +150,7 @@ class Application(ApplicationPackages, ApplicationRepository):
|
||||
with_dependencies[package.base] = package
|
||||
|
||||
# register package in local database
|
||||
self.database.remote_update(package)
|
||||
self.database.package_base_update(package)
|
||||
self.repository.reporter.set_unknown(package)
|
||||
|
||||
return list(with_dependencies.values())
|
||||
|
@ -65,7 +65,7 @@ class ApplicationPackages(ApplicationProperties):
|
||||
"""
|
||||
package = Package.from_aur(source, username)
|
||||
self.database.build_queue_insert(package)
|
||||
self.database.remote_update(package)
|
||||
self.database.package_base_update(package)
|
||||
|
||||
def _add_directory(self, source: str, *_: Any) -> None:
|
||||
"""
|
||||
@ -139,7 +139,7 @@ class ApplicationPackages(ApplicationProperties):
|
||||
"""
|
||||
package = Package.from_official(source, self.repository.pacman, username)
|
||||
self.database.build_queue_insert(package)
|
||||
self.database.remote_update(package)
|
||||
self.database.package_base_update(package)
|
||||
|
||||
def add(self, names: Iterable[str], source: PackageSource, username: str | None = None) -> None:
|
||||
"""
|
||||
|
@ -33,6 +33,23 @@ class ApplicationRepository(ApplicationProperties):
|
||||
repository control class
|
||||
"""
|
||||
|
||||
def changes(self, packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
generate and update package changes
|
||||
|
||||
Args:
|
||||
packages(Iterable[Package]): list of packages to retrieve changes
|
||||
"""
|
||||
last_commit_hashes = self.database.hashes_get()
|
||||
|
||||
for package in packages:
|
||||
last_commit_sha = last_commit_hashes.get(package.base)
|
||||
if last_commit_sha is None:
|
||||
continue # skip check in case if we can't calculate diff
|
||||
|
||||
changes = self.repository.package_changes(package, last_commit_sha)
|
||||
self.repository.reporter.package_changes_set(package.base, changes)
|
||||
|
||||
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
|
||||
"""
|
||||
run all clean methods. Warning: some functions might not be available under non-root
|
||||
|
@ -21,6 +21,7 @@ from ahriman.application.handlers.handler import Handler
|
||||
|
||||
from ahriman.application.handlers.add import Add
|
||||
from ahriman.application.handlers.backup import Backup
|
||||
from ahriman.application.handlers.change import Change
|
||||
from ahriman.application.handlers.clean import Clean
|
||||
from ahriman.application.handlers.daemon import Daemon
|
||||
from ahriman.application.handlers.dump import Dump
|
||||
|
59
src/ahriman/application/handlers/change.py
Normal file
59
src/ahriman/application/handlers/change.py
Normal file
@ -0,0 +1,59 @@
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.formatters import ChangesPrinter
|
||||
from ahriman.models.action import Action
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class Change(Handler):
|
||||
"""
|
||||
package changes handler
|
||||
"""
|
||||
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||
|
||||
@classmethod
|
||||
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||
report: bool) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line args
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
application = Application(repository_id, configuration, report=True)
|
||||
client = application.repository.reporter
|
||||
|
||||
match args.action:
|
||||
case Action.List:
|
||||
changes = client.package_changes_get(args.package)
|
||||
ChangesPrinter(changes)(verbose=True, separator="")
|
||||
Change.check_if_empty(args.exit_code, changes.is_empty)
|
||||
case Action.Remove:
|
||||
client.package_changes_set(args.package, Changes())
|
@ -47,9 +47,13 @@ class Update(Handler):
|
||||
"""
|
||||
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||
application.on_start()
|
||||
|
||||
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs)
|
||||
Update.check_if_empty(args.exit_code, not packages)
|
||||
if args.dry_run:
|
||||
if args.dry_run: # some check specific actions
|
||||
if args.changes: # generate changes if requested
|
||||
application.changes(packages)
|
||||
|
||||
Update.check_if_empty(args.exit_code, not packages) # status code check
|
||||
return
|
||||
|
||||
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
||||
|
@ -21,6 +21,7 @@ import shutil
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.exceptions import CalledProcessError
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.util import check_output, utcnow, walk
|
||||
from ahriman.models.package import Package
|
||||
@ -42,6 +43,25 @@ class Sources(LazyLogging):
|
||||
DEFAULT_BRANCH = "master" # default fallback branch
|
||||
DEFAULT_COMMIT_AUTHOR = ("ahriman", "ahriman@localhost")
|
||||
|
||||
@staticmethod
|
||||
def changes(source_dir: Path, last_commit_sha: str | None) -> str | None:
|
||||
"""
|
||||
extract changes from the last known commit if available
|
||||
|
||||
Args:
|
||||
source_dir(Path): local path to directory with source files
|
||||
last_commit_sha(str | None): last known commit hash
|
||||
|
||||
Returns:
|
||||
str | None: changes from the last commit if available or ``None`` otherwise
|
||||
"""
|
||||
if last_commit_sha is None:
|
||||
return None # no previous reference found
|
||||
|
||||
instance = Sources()
|
||||
instance.fetch_until(source_dir, commit_sha=last_commit_sha)
|
||||
return instance.diff(source_dir, last_commit_sha)
|
||||
|
||||
@staticmethod
|
||||
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
|
||||
"""
|
||||
@ -61,13 +81,16 @@ class Sources(LazyLogging):
|
||||
return [PkgbuildPatch("arch", list(architectures))]
|
||||
|
||||
@staticmethod
|
||||
def fetch(sources_dir: Path, remote: RemoteSource) -> None:
|
||||
def fetch(sources_dir: Path, remote: RemoteSource) -> str | None:
|
||||
"""
|
||||
either clone repository or update it to origin/``remote.branch``
|
||||
|
||||
Args:
|
||||
sources_dir(Path): local path to fetch
|
||||
remote(RemoteSource): remote target (from where to fetch)
|
||||
|
||||
Returns:
|
||||
str | None: current commit sha if available
|
||||
"""
|
||||
instance = Sources()
|
||||
# local directory exists and there is .git directory
|
||||
@ -75,13 +98,12 @@ class Sources(LazyLogging):
|
||||
if is_initialized_git and not instance.has_remotes(sources_dir):
|
||||
# there is git repository, but no remote configured so far
|
||||
instance.logger.info("skip update at %s because there are no branches configured", sources_dir)
|
||||
return
|
||||
return instance.head(sources_dir)
|
||||
|
||||
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)
|
||||
check_output("git", "fetch", "--quiet", "--depth", "1", "origin", branch,
|
||||
cwd=sources_dir, logger=instance.logger)
|
||||
instance.fetch_until(sources_dir, branch=branch)
|
||||
elif remote.git_url is not None:
|
||||
instance.logger.info("clone remote %s to %s using branch %s", remote.git_url, sources_dir, branch)
|
||||
check_output("git", "clone", "--quiet", "--depth", "1", "--branch", branch, "--single-branch",
|
||||
@ -100,6 +122,8 @@ class Sources(LazyLogging):
|
||||
pkgbuild_dir = remote.pkgbuild_dir or sources_dir.resolve()
|
||||
instance.move((sources_dir / pkgbuild_dir).resolve(), sources_dir)
|
||||
|
||||
return instance.head(sources_dir)
|
||||
|
||||
@staticmethod
|
||||
def has_remotes(sources_dir: Path) -> bool:
|
||||
"""
|
||||
@ -136,7 +160,7 @@ class Sources(LazyLogging):
|
||||
instance.commit(sources_dir)
|
||||
|
||||
@staticmethod
|
||||
def load(sources_dir: Path, package: Package, patches: list[PkgbuildPatch], paths: RepositoryPaths) -> None:
|
||||
def load(sources_dir: Path, package: Package, patches: list[PkgbuildPatch], paths: RepositoryPaths) -> str | None:
|
||||
"""
|
||||
fetch sources from remote and apply patches
|
||||
|
||||
@ -145,17 +169,22 @@ class Sources(LazyLogging):
|
||||
package(Package): package definitions
|
||||
patches(list[PkgbuildPatch]): optional patch to be applied
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
|
||||
Returns:
|
||||
str | None: current commit sha if available
|
||||
"""
|
||||
instance = Sources()
|
||||
if (cache_dir := paths.cache_for(package.base)).is_dir() and cache_dir != sources_dir:
|
||||
# no need to clone whole repository, just copy from cache first
|
||||
shutil.copytree(cache_dir, sources_dir, dirs_exist_ok=True)
|
||||
instance.fetch(sources_dir, package.remote)
|
||||
last_commit_sha = instance.fetch(sources_dir, package.remote)
|
||||
|
||||
patches.extend(instance.extend_architectures(sources_dir, paths.repository_id.architecture))
|
||||
for patch in patches:
|
||||
instance.patch_apply(sources_dir, patch)
|
||||
|
||||
return last_commit_sha
|
||||
|
||||
@staticmethod
|
||||
def patch_create(sources_dir: Path, *pattern: str) -> str:
|
||||
"""
|
||||
@ -247,17 +276,47 @@ class Sources(LazyLogging):
|
||||
|
||||
return True
|
||||
|
||||
def diff(self, sources_dir: Path) -> str:
|
||||
def diff(self, sources_dir: Path, sha: str | None = None) -> str:
|
||||
"""
|
||||
generate diff from the current version and write it to the output file
|
||||
|
||||
Args:
|
||||
sources_dir(Path): local path to git repository
|
||||
sha(str | None, optional): optional commit sha to calculate diff (Default value = None)
|
||||
|
||||
Returns:
|
||||
str: patch as plain string
|
||||
"""
|
||||
return check_output("git", "diff", cwd=sources_dir, logger=self.logger)
|
||||
args = []
|
||||
if sha is not None:
|
||||
args.append(sha)
|
||||
return check_output("git", "diff", *args, cwd=sources_dir, logger=self.logger)
|
||||
|
||||
def fetch_until(self, sources_dir: Path, *, branch: str | None = None, commit_sha: str | None = None) -> None:
|
||||
"""
|
||||
fetch repository until commit sha
|
||||
|
||||
Args:
|
||||
sources_dir(Path): local path to git repository
|
||||
branch(str | None, optional): use specified branch (Default value = None)
|
||||
commit_sha(str | None, optional): commit hash to fetch. If none set, only one will be fetched
|
||||
(Default value = None)
|
||||
"""
|
||||
commit_sha = commit_sha or "HEAD" # if none set we just fetch the last commit
|
||||
|
||||
commits_count = 1
|
||||
while commit_sha is not None:
|
||||
command = ["git", "fetch", "--quiet", "--depth", str(commits_count)]
|
||||
if branch is not None:
|
||||
command += ["origin", branch]
|
||||
check_output(*command, cwd=sources_dir, logger=self.logger) # fetch one more level
|
||||
|
||||
try:
|
||||
# check if there is an object in current git directory
|
||||
check_output("git", "cat-file", "-e", commit_sha, cwd=sources_dir, logger=self.logger)
|
||||
commit_sha = None # reset search
|
||||
except CalledProcessError:
|
||||
commits_count += 1 # increase depth
|
||||
|
||||
def has_changes(self, sources_dir: Path) -> bool:
|
||||
"""
|
||||
@ -273,6 +332,20 @@ class Sources(LazyLogging):
|
||||
changes = check_output("git", "diff", "--cached", "--name-only", cwd=sources_dir, logger=self.logger)
|
||||
return bool(changes)
|
||||
|
||||
def head(self, sources_dir: Path, ref_name: str = "HEAD") -> str:
|
||||
"""
|
||||
extract HEAD reference for the current git repository
|
||||
|
||||
Args:
|
||||
sources_dir(Path): local path to git repository
|
||||
ref_name(str, optional): reference name (Default value = "HEAD")
|
||||
|
||||
Returns:
|
||||
str: HEAD commit hash
|
||||
"""
|
||||
# we might want to parse git files instead though
|
||||
return check_output("git", "rev-parse", ref_name, cwd=sources_dir)
|
||||
|
||||
def move(self, pkgbuild_dir: Path, sources_dir: Path) -> None:
|
||||
"""
|
||||
move content from pkgbuild_dir to sources_dir
|
||||
|
@ -109,7 +109,7 @@ class Task(LazyLogging):
|
||||
).splitlines()
|
||||
return [Path(package) for package in packages]
|
||||
|
||||
def init(self, sources_dir: Path, database: SQLite, local_version: str | None) -> None:
|
||||
def init(self, sources_dir: Path, database: SQLite, local_version: str | None) -> str | None:
|
||||
"""
|
||||
fetch package from git
|
||||
|
||||
@ -118,10 +118,13 @@ class Task(LazyLogging):
|
||||
database(SQLite): database instance
|
||||
local_version(str | None): local version of the package. If set and equal to current version, it will
|
||||
automatically bump pkgrel
|
||||
|
||||
Returns:
|
||||
str | None: current commit sha if available
|
||||
"""
|
||||
Sources.load(sources_dir, self.package, database.patches_get(self.package.base), self.paths)
|
||||
last_commit_sha = Sources.load(sources_dir, self.package, database.patches_get(self.package.base), self.paths)
|
||||
if local_version is None:
|
||||
return # there is no local package or pkgrel increment is disabled
|
||||
return last_commit_sha # there is no local package or pkgrel increment is disabled
|
||||
|
||||
# load fresh package
|
||||
loaded_package = Package.from_build(sources_dir, self.architecture, None)
|
||||
@ -129,3 +132,5 @@ class Task(LazyLogging):
|
||||
self.logger.info("package %s is the same as in repo, bumping pkgrel to %s", self.package.base, pkgrel)
|
||||
patch = PkgbuildPatch("pkgrel", pkgrel)
|
||||
patch.write(sources_dir / "PKGBUILD")
|
||||
|
||||
return last_commit_sha
|
||||
|
33
src/ahriman/core/database/migrations/m012_last_commit_sha.py
Normal file
33
src/ahriman/core/database/migrations/m012_last_commit_sha.py
Normal file
@ -0,0 +1,33 @@
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
__all__ = ["steps"]
|
||||
|
||||
|
||||
steps = [
|
||||
"""
|
||||
create table package_changes (
|
||||
package_base text not null,
|
||||
repository text not null,
|
||||
last_commit_sha text not null,
|
||||
changes text,
|
||||
unique (package_base, repository)
|
||||
)
|
||||
""",
|
||||
]
|
@ -21,6 +21,7 @@ from ahriman.core.database.operations.operations import Operations
|
||||
|
||||
from ahriman.core.database.operations.auth_operations import AuthOperations
|
||||
from ahriman.core.database.operations.build_operations import BuildOperations
|
||||
from ahriman.core.database.operations.changes_operations import ChangesOperations
|
||||
from ahriman.core.database.operations.logs_operations import LogsOperations
|
||||
from ahriman.core.database.operations.package_operations import PackageOperations
|
||||
from ahriman.core.database.operations.patch_operations import PatchOperations
|
||||
|
143
src/ahriman/core/database/operations/changes_operations.py
Normal file
143
src/ahriman/core/database/operations/changes_operations.py
Normal file
@ -0,0 +1,143 @@
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from sqlite3 import Connection
|
||||
|
||||
from ahriman.core.database.operations import Operations
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class ChangesOperations(Operations):
|
||||
"""
|
||||
operations for source files changes
|
||||
"""
|
||||
|
||||
def changes_get(self, package_base: str, repository_id: RepositoryId | None = None) -> Changes:
|
||||
"""
|
||||
get changes for the specific package base if available
|
||||
|
||||
Args:
|
||||
package_base(str): package base to search
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
|
||||
Returns:
|
||||
Changes: changes for the package base if available
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> Changes:
|
||||
return next(
|
||||
(
|
||||
Changes(row["last_commit_sha"], row["changes"] or None)
|
||||
for row in connection.execute(
|
||||
"""
|
||||
select last_commit_sha, changes from package_changes
|
||||
where package_base = :package_base and repository = :repository
|
||||
""",
|
||||
{
|
||||
"package_base": package_base,
|
||||
"repository": repository_id.id,
|
||||
}
|
||||
)
|
||||
),
|
||||
Changes()
|
||||
)
|
||||
|
||||
return self.with_connection(run)
|
||||
|
||||
def changes_insert(self, package_base: str, changes: Changes, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
insert packages to build queue
|
||||
|
||||
Args:
|
||||
package_base(str): package base to insert
|
||||
changes(Changes): package changes (as in patch format)
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
insert into package_changes
|
||||
(package_base, last_commit_sha, changes, repository)
|
||||
values
|
||||
(:package_base, :last_commit_sha, :changes ,:repository)
|
||||
on conflict (package_base, repository) do update set
|
||||
last_commit_sha = :last_commit_sha, changes = :changes
|
||||
""",
|
||||
{
|
||||
"package_base": package_base,
|
||||
"last_commit_sha": changes.last_commit_sha,
|
||||
"changes": changes.changes,
|
||||
"repository": repository_id.id,
|
||||
})
|
||||
|
||||
if changes.last_commit_sha is None:
|
||||
return self.changes_remove(package_base, repository_id)
|
||||
return self.with_connection(run, commit=True)
|
||||
|
||||
def changes_remove(self, package_base: str | None, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
remove packages changes
|
||||
|
||||
Args:
|
||||
package_base(str | None): optional filter by package base
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
delete from package_changes
|
||||
where (:package_base is null or package_base = :package_base)
|
||||
and repository = :repository
|
||||
""",
|
||||
{
|
||||
"package_base": package_base,
|
||||
"repository": repository_id.id,
|
||||
})
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
|
||||
def hashes_get(self, repository_id: RepositoryId | None = None) -> dict[str, str]:
|
||||
"""
|
||||
extract last commit hashes if available
|
||||
|
||||
Args:
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
|
||||
Returns:
|
||||
dict[str, str]: map of package base to its last commit hash
|
||||
"""
|
||||
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> dict[str, str]:
|
||||
return {
|
||||
row["package_base"]: row["last_commit_sha"]
|
||||
for row in connection.execute(
|
||||
"""select package_base, last_commit_sha from package_changes where repository = :repository""",
|
||||
{"repository": repository_id.id}
|
||||
)
|
||||
}
|
||||
|
||||
return self.with_connection(run)
|
@ -246,6 +246,21 @@ class PackageOperations(Operations):
|
||||
)
|
||||
}
|
||||
|
||||
def package_base_update(self, package: Package, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
update package base only
|
||||
|
||||
Args:
|
||||
package(Package): package properties
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
self._package_update_insert_base(connection, package, repository_id)
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
|
||||
def package_remove(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
remove package from database
|
||||
@ -302,21 +317,6 @@ class PackageOperations(Operations):
|
||||
|
||||
return self.with_connection(lambda connection: list(run(connection)))
|
||||
|
||||
def remote_update(self, package: Package, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
update package remote source
|
||||
|
||||
Args:
|
||||
package(Package): package properties
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
self._package_update_insert_base(connection, package, repository_id)
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
|
||||
def remotes_get(self, repository_id: RepositoryId | None = None) -> dict[str, RemoteSource]:
|
||||
"""
|
||||
get packages remotes based on current settings
|
||||
|
@ -25,11 +25,12 @@ from typing import Self
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.migrations import Migrations
|
||||
from ahriman.core.database.operations import AuthOperations, BuildOperations, LogsOperations, PackageOperations, \
|
||||
PatchOperations
|
||||
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, LogsOperations, \
|
||||
PackageOperations, PatchOperations
|
||||
|
||||
|
||||
class SQLite(AuthOperations, BuildOperations, LogsOperations, PackageOperations, PatchOperations):
|
||||
# pylint: disable=too-many-ancestors
|
||||
class SQLite(AuthOperations, BuildOperations, ChangesOperations, LogsOperations, PackageOperations, PatchOperations):
|
||||
"""
|
||||
wrapper for sqlite3 database
|
||||
|
||||
|
@ -18,16 +18,17 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters.printer import Printer
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
|
||||
from ahriman.core.formatters.aur_printer import AurPrinter
|
||||
from ahriman.core.formatters.build_printer import BuildPrinter
|
||||
from ahriman.core.formatters.changes_printer import ChangesPrinter
|
||||
from ahriman.core.formatters.configuration_paths_printer import ConfigurationPathsPrinter
|
||||
from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
|
||||
from ahriman.core.formatters.package_printer import PackagePrinter
|
||||
from ahriman.core.formatters.patch_printer import PatchPrinter
|
||||
from ahriman.core.formatters.repository_printer import RepositoryPrinter
|
||||
from ahriman.core.formatters.status_printer import StatusPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.core.formatters.tree_printer import TreePrinter
|
||||
from ahriman.core.formatters.update_printer import UpdatePrinter
|
||||
from ahriman.core.formatters.user_printer import UserPrinter
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.core.util import pretty_datetime
|
||||
from ahriman.models.aur_package import AURPackage
|
||||
from ahriman.models.property import Property
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
|
64
src/ahriman/core/formatters/changes_printer.py
Normal file
64
src/ahriman/core/formatters/changes_printer.py
Normal file
@ -0,0 +1,64 @@
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters import Printer
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
class ChangesPrinter(Printer):
|
||||
"""
|
||||
print content of the changes object
|
||||
|
||||
Attributes:
|
||||
changes(Changes): package changes
|
||||
"""
|
||||
|
||||
def __init__(self, changes: Changes) -> None:
|
||||
"""
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
changes(Changes): package changes
|
||||
"""
|
||||
Printer.__init__(self)
|
||||
self.changes = changes
|
||||
|
||||
def properties(self) -> list[Property]:
|
||||
"""
|
||||
convert content into printable data
|
||||
|
||||
Returns:
|
||||
list[Property]: list of content properties
|
||||
"""
|
||||
if self.changes.is_empty:
|
||||
return []
|
||||
return [Property("", self.changes.changes, is_required=True, indent=0)]
|
||||
|
||||
# pylint: disable=redundant-returns-doc
|
||||
def title(self) -> str | None:
|
||||
"""
|
||||
generate entry title from content
|
||||
|
||||
Returns:
|
||||
str | None: content title if it can be generated and None otherwise
|
||||
"""
|
||||
if self.changes.is_empty:
|
||||
return None
|
||||
return self.changes.last_commit_sha
|
@ -19,7 +19,7 @@
|
||||
#
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.property import Property
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.property import Property
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.property import Property
|
||||
from ahriman.models.user import User
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters import StringPrinter
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
|
@ -25,45 +25,20 @@ from tempfile import TemporaryDirectory
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.core.repository.package_info import PackageInfo
|
||||
from ahriman.core.util import safe_filename
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.packagers import Packagers
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
class Executor(Cleaner):
|
||||
class Executor(PackageInfo, Cleaner):
|
||||
"""
|
||||
trait for common repository update processes
|
||||
"""
|
||||
|
||||
def load_archives(self, packages: Iterable[Path]) -> list[Package]:
|
||||
"""
|
||||
load packages from list of archives
|
||||
|
||||
Args:
|
||||
packages(Iterable[Path]): paths to package archives
|
||||
|
||||
Returns:
|
||||
list[Package]: list of read packages
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def packages(self) -> list[Package]:
|
||||
"""
|
||||
generate list of repository packages
|
||||
|
||||
Returns:
|
||||
list[Package]: list of packages properties
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def process_build(self, updates: Iterable[Package], packagers: Packagers | None = None, *,
|
||||
bump_pkgrel: bool = False) -> Result:
|
||||
"""
|
||||
@ -78,16 +53,18 @@ class Executor(Cleaner):
|
||||
Returns:
|
||||
Result: build result
|
||||
"""
|
||||
def build_single(package: Package, local_path: Path, packager_id: str | None) -> None:
|
||||
def build_single(package: Package, local_path: Path, packager_id: str | None) -> str | None:
|
||||
self.reporter.set_building(package.base)
|
||||
task = Task(package, self.configuration, self.architecture, self.paths)
|
||||
local_version = local_versions.get(package.base) if bump_pkgrel else None
|
||||
task.init(local_path, self.database, local_version)
|
||||
commit_sha = task.init(local_path, self.database, local_version)
|
||||
built = task.build(local_path, PACKAGER=packager_id)
|
||||
for src in built:
|
||||
dst = self.paths.packages / src.name
|
||||
shutil.move(src, dst)
|
||||
|
||||
return commit_sha
|
||||
|
||||
packagers = packagers or Packagers()
|
||||
local_versions = {package.base: package.version for package in self.packages()}
|
||||
|
||||
@ -97,7 +74,9 @@ class Executor(Cleaner):
|
||||
TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
||||
try:
|
||||
packager = self.packager(packagers, single.base)
|
||||
build_single(single, Path(dir_name), packager.packager_id)
|
||||
last_commit_sha = build_single(single, Path(dir_name), packager.packager_id)
|
||||
# clear changes and update commit hash
|
||||
self.reporter.package_changes_set(single.base, Changes(last_commit_sha))
|
||||
result.add_updated(single)
|
||||
except Exception:
|
||||
self.reporter.set_failed(single.base)
|
||||
@ -122,6 +101,7 @@ class Executor(Cleaner):
|
||||
self.database.build_queue_clear(package_base)
|
||||
self.database.patches_remove(package_base, [])
|
||||
self.database.logs_remove(package_base, None)
|
||||
self.database.changes_remove(package_base)
|
||||
self.reporter.package_remove(package_base) # we only update status page in case of base removal
|
||||
except Exception:
|
||||
self.logger.exception("could not remove base %s", package_base)
|
||||
|
126
src/ahriman/core/repository/package_info.py
Normal file
126
src/ahriman/core/repository/package_info.py
Normal file
@ -0,0 +1,126 @@
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.repository.repository_properties import RepositoryProperties
|
||||
from ahriman.core.util import package_like
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
class PackageInfo(RepositoryProperties):
|
||||
"""
|
||||
handler for the package information
|
||||
"""
|
||||
|
||||
def load_archives(self, packages: Iterable[Path]) -> list[Package]:
|
||||
"""
|
||||
load packages from list of archives
|
||||
|
||||
Args:
|
||||
packages(Iterable[Path]): paths to package archives
|
||||
|
||||
Returns:
|
||||
list[Package]: list of read packages
|
||||
"""
|
||||
sources = self.database.remotes_get()
|
||||
|
||||
result: dict[str, Package] = {}
|
||||
# we are iterating over bases, not single packages
|
||||
for full_path in packages:
|
||||
try:
|
||||
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:
|
||||
# force version to max of them
|
||||
self.logger.warning("version of %s differs, found %s and %s",
|
||||
current.base, current.version, local.version)
|
||||
if current.is_outdated(local, self.paths, calculate_version=False):
|
||||
current.version = local.version
|
||||
current.packages.update(local.packages)
|
||||
except Exception:
|
||||
self.logger.exception("could not load package from %s", full_path)
|
||||
return list(result.values())
|
||||
|
||||
def package_changes(self, package: Package, last_commit_sha: str | None) -> Changes:
|
||||
"""
|
||||
extract package change for the package since last commit if available
|
||||
|
||||
Args:
|
||||
package(Package): package properties
|
||||
last_commit_sha(str | None): last known commit hash
|
||||
|
||||
Returns:
|
||||
Changes: changes if available
|
||||
"""
|
||||
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
||||
dir_path = Path(dir_name)
|
||||
current_commit_sha = Sources.load(dir_path, package, self.database.patches_get(package.base), self.paths)
|
||||
|
||||
changes: str | None = None
|
||||
if current_commit_sha != last_commit_sha:
|
||||
changes = Sources.changes(dir_path, last_commit_sha)
|
||||
|
||||
return Changes(last_commit_sha, changes)
|
||||
|
||||
def packages(self) -> list[Package]:
|
||||
"""
|
||||
generate list of repository packages
|
||||
|
||||
Returns:
|
||||
list[Package]: list of packages properties
|
||||
"""
|
||||
return self.load_archives(filter(package_like, self.paths.repository.iterdir()))
|
||||
|
||||
def packages_built(self) -> list[Path]:
|
||||
"""
|
||||
get list of files in built packages directory
|
||||
|
||||
Returns:
|
||||
list[Path]: list of filenames from the directory
|
||||
"""
|
||||
return list(filter(package_like, self.paths.packages.iterdir()))
|
||||
|
||||
def packages_depend_on(self, packages: list[Package], depends_on: Iterable[str] | None) -> list[Package]:
|
||||
"""
|
||||
extract list of packages which depends on specified package
|
||||
|
||||
Args:
|
||||
packages(list[Package]): list of packages to be filtered
|
||||
depends_on(Iterable[str] | None): dependencies of the packages
|
||||
|
||||
Returns:
|
||||
list[Package]: list of repository packages which depend on specified packages
|
||||
"""
|
||||
if depends_on is None:
|
||||
return packages # no list provided extract everything by default
|
||||
depends_on = set(depends_on)
|
||||
|
||||
return [
|
||||
package
|
||||
for package in packages
|
||||
if depends_on.intersection(package.full_depends(self.pacman, packages))
|
||||
]
|
@ -17,8 +17,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from typing import Self
|
||||
|
||||
from ahriman.core import _Context, context
|
||||
@ -28,9 +26,7 @@ from ahriman.core.database import SQLite
|
||||
from ahriman.core.repository.executor import Executor
|
||||
from ahriman.core.repository.update_handler import UpdateHandler
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.util import package_like
|
||||
from ahriman.models.context_key import ContextKey
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
@ -101,74 +97,3 @@ class Repository(Executor, UpdateHandler):
|
||||
ctx.set(ContextKey("repository", type(self)), self)
|
||||
|
||||
context.set(ctx)
|
||||
|
||||
def load_archives(self, packages: Iterable[Path]) -> list[Package]:
|
||||
"""
|
||||
load packages from list of archives
|
||||
|
||||
Args:
|
||||
packages(Iterable[Path]): paths to package archives
|
||||
|
||||
Returns:
|
||||
list[Package]: list of read packages
|
||||
"""
|
||||
sources = self.database.remotes_get()
|
||||
|
||||
result: dict[str, Package] = {}
|
||||
# we are iterating over bases, not single packages
|
||||
for full_path in packages:
|
||||
try:
|
||||
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:
|
||||
# force version to max of them
|
||||
self.logger.warning("version of %s differs, found %s and %s",
|
||||
current.base, current.version, local.version)
|
||||
if current.is_outdated(local, self.paths, calculate_version=False):
|
||||
current.version = local.version
|
||||
current.packages.update(local.packages)
|
||||
except Exception:
|
||||
self.logger.exception("could not load package from %s", full_path)
|
||||
return list(result.values())
|
||||
|
||||
def packages(self) -> list[Package]:
|
||||
"""
|
||||
generate list of repository packages
|
||||
|
||||
Returns:
|
||||
list[Package]: list of packages properties
|
||||
"""
|
||||
return self.load_archives(filter(package_like, self.paths.repository.iterdir()))
|
||||
|
||||
def packages_built(self) -> list[Path]:
|
||||
"""
|
||||
get list of files in built packages directory
|
||||
|
||||
Returns:
|
||||
list[Path]: list of filenames from the directory
|
||||
"""
|
||||
return list(filter(package_like, self.paths.packages.iterdir()))
|
||||
|
||||
def packages_depend_on(self, packages: list[Package], depends_on: Iterable[str] | None) -> list[Package]:
|
||||
"""
|
||||
extract list of packages which depends on specified package
|
||||
|
||||
Args:
|
||||
packages(list[Package]): list of packages to be filtered
|
||||
depends_on(Iterable[str] | None): dependencies of the packages
|
||||
|
||||
Returns:
|
||||
list[Package]: list of repository packages which depend on specified packages
|
||||
"""
|
||||
if depends_on is None:
|
||||
return packages # no list provided extract everything by default
|
||||
depends_on = set(depends_on)
|
||||
|
||||
return [
|
||||
package
|
||||
for package in packages
|
||||
if depends_on.intersection(package.full_depends(self.pacman, packages))
|
||||
]
|
||||
|
@ -22,28 +22,17 @@ from collections.abc import Iterable
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.repository.cleaner import Cleaner
|
||||
from ahriman.core.repository.package_info import PackageInfo
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.remote_source import RemoteSource
|
||||
|
||||
|
||||
class UpdateHandler(Cleaner):
|
||||
class UpdateHandler(PackageInfo, Cleaner):
|
||||
"""
|
||||
trait to get package update list
|
||||
"""
|
||||
|
||||
def packages(self) -> list[Package]:
|
||||
"""
|
||||
generate list of repository packages
|
||||
|
||||
Returns:
|
||||
list[Package]: list of packages properties
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def updates_aur(self, filter_packages: Iterable[str], *, vcs: bool) -> list[Package]:
|
||||
"""
|
||||
check AUR for updates
|
||||
|
@ -23,6 +23,7 @@ import logging
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
@ -75,6 +76,28 @@ class Client:
|
||||
status(BuildStatusEnum): current package build status
|
||||
"""
|
||||
|
||||
def package_changes_get(self, package_base: str) -> Changes:
|
||||
"""
|
||||
get package changes
|
||||
|
||||
Args:
|
||||
package_base(str): package base to retrieve
|
||||
|
||||
Returns:
|
||||
Changes: package changes if available and empty object otherwise
|
||||
"""
|
||||
del package_base
|
||||
return Changes()
|
||||
|
||||
def package_changes_set(self, package_base: str, changes: Changes) -> None:
|
||||
"""
|
||||
update package changes
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
changes(Changes): changes descriptor
|
||||
"""
|
||||
|
||||
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
|
||||
"""
|
||||
get package status
|
||||
|
@ -21,6 +21,7 @@ from ahriman.core.database import SQLite
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
@ -113,6 +114,23 @@ class Watcher(LazyLogging):
|
||||
self._last_log_record_id = log_record_id
|
||||
self.database.logs_insert(log_record_id, created, record, self.repository_id)
|
||||
|
||||
def package_changes_get(self, package_base: str) -> Changes:
|
||||
"""
|
||||
retrieve package changes
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
|
||||
Returns:
|
||||
Changes: package changes if available
|
||||
|
||||
Raises:
|
||||
UnknownPackageError: if no package found
|
||||
"""
|
||||
if package_base not in self.known:
|
||||
raise UnknownPackageError(package_base)
|
||||
return self.database.changes_get(package_base, self.repository_id)
|
||||
|
||||
def package_get(self, package_base: str) -> tuple[Package, BuildStatus]:
|
||||
"""
|
||||
get current package base build status
|
||||
|
@ -26,6 +26,7 @@ from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.http import SyncAhrimanClient
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
@ -83,6 +84,18 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
address = f"http://{host}:{port}"
|
||||
return "web", address
|
||||
|
||||
def _changes_url(self, package_base: str) -> str:
|
||||
"""
|
||||
get url for the changes api
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
|
||||
Returns:
|
||||
str: full url for web service for logs
|
||||
"""
|
||||
return f"{self.address}/api/v1/packages/{package_base}/changes"
|
||||
|
||||
def _logs_url(self, package_base: str) -> str:
|
||||
"""
|
||||
get url for the logs api
|
||||
@ -134,6 +147,37 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
self.make_request("POST", self._package_url(package.base),
|
||||
params=self.repository_id.query(), json=payload)
|
||||
|
||||
def package_changes_get(self, package_base: str) -> Changes:
|
||||
"""
|
||||
get package changes
|
||||
|
||||
Args:
|
||||
package_base(str): package base to retrieve
|
||||
|
||||
Returns:
|
||||
Changes: package changes if available and empty object otherwise
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
response = self.make_request("GET", self._changes_url(package_base),
|
||||
params=self.repository_id.query())
|
||||
response_json = response.json()
|
||||
|
||||
return Changes.from_json(response_json)
|
||||
|
||||
return Changes()
|
||||
|
||||
def package_changes_set(self, package_base: str, changes: Changes) -> None:
|
||||
"""
|
||||
update package changes
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
changes(Changes): changes descriptor
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("POST", self._changes_url(package_base),
|
||||
params=self.repository_id.query(), json=changes.view())
|
||||
|
||||
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
|
||||
"""
|
||||
get package status
|
||||
|
71
src/ahriman/models/changes.py
Normal file
71
src/ahriman/models/changes.py
Normal file
@ -0,0 +1,71 @@
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from dataclasses import dataclass, fields
|
||||
from typing import Any, Self
|
||||
|
||||
from ahriman.core.util import dataclass_view, filter_json
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Changes:
|
||||
"""
|
||||
package source files changes holder
|
||||
|
||||
Attributes:
|
||||
last_commit_sha(str | None): last commit hash
|
||||
changes(str | None): package change since the last commit if available
|
||||
"""
|
||||
|
||||
last_commit_sha: str | None = None
|
||||
changes: str | None = None
|
||||
|
||||
@property
|
||||
def is_empty(self) -> bool:
|
||||
"""
|
||||
validate that changes are not empty
|
||||
|
||||
Returns:
|
||||
bool: ``True`` in case if changes are not set and ``False`` otherwise
|
||||
"""
|
||||
return self.changes is None
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, dump: dict[str, Any]) -> Self:
|
||||
"""
|
||||
construct changes from the json dump
|
||||
|
||||
Args:
|
||||
dump(dict[str, Any]): json dump body
|
||||
|
||||
Returns:
|
||||
Self: changes object
|
||||
"""
|
||||
# filter to only known fields
|
||||
known_fields = [pair.name for pair in fields(cls)]
|
||||
return cls(**filter_json(dump, known_fields))
|
||||
|
||||
def view(self) -> dict[str, Any]:
|
||||
"""
|
||||
generate json change view
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: json-friendly dictionary
|
||||
"""
|
||||
return dataclass_view(self)
|
@ -19,6 +19,7 @@
|
||||
#
|
||||
from ahriman.web.schemas.aur_package_schema import AURPackageSchema
|
||||
from ahriman.web.schemas.auth_schema import AuthSchema
|
||||
from ahriman.web.schemas.changes_schema import ChangesSchema
|
||||
from ahriman.web.schemas.counters_schema import CountersSchema
|
||||
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||
from ahriman.web.schemas.file_schema import FileSchema
|
||||
|
34
src/ahriman/web/schemas/changes_schema.py
Normal file
34
src/ahriman/web/schemas/changes_schema.py
Normal file
@ -0,0 +1,34 @@
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
|
||||
class ChangesSchema(Schema):
|
||||
"""
|
||||
response package changes schema
|
||||
"""
|
||||
|
||||
last_commit_sha = fields.String(metadata={
|
||||
"description": "Last recorded commit hash",
|
||||
"example": "f1875edca1eb8fc0e55c41d1cae5fa05b6b7c6",
|
||||
})
|
||||
changes = fields.String(metadata={
|
||||
"description": "Package changes in patch format",
|
||||
})
|
118
src/ahriman/web/views/v1/status/changes.py
Normal file
118
src/ahriman/web/views/v1/status/changes.py
Normal file
@ -0,0 +1,118 @@
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import aiohttp_apispec # type: ignore[import-untyped]
|
||||
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
||||
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ChangesSchema, ErrorSchema, PackageNameSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
|
||||
|
||||
class ChangesView(BaseView):
|
||||
"""
|
||||
package changes web view
|
||||
|
||||
Attributes:
|
||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
ROUTES = ["/api/v1/packages/{package}/changes"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Packages"],
|
||||
summary="Get package changes",
|
||||
description="Retrieve package changes since the last build",
|
||||
responses={
|
||||
200: {"description": "Success response", "schema": ChangesSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [GET_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
get package changes
|
||||
|
||||
Returns:
|
||||
Response: 200 with package change on success
|
||||
|
||||
Raises:
|
||||
HTTPNotFound: if package base is unknown
|
||||
"""
|
||||
package_base = self.request.match_info["package"]
|
||||
|
||||
try:
|
||||
changes = self.service().package_changes_get(package_base)
|
||||
except UnknownPackageError:
|
||||
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
|
||||
|
||||
return json_response(changes.view())
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Packages"],
|
||||
summary="Update package changes",
|
||||
description="Update package changes to the new ones",
|
||||
responses={
|
||||
204: {"description": "Success response"},
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
@aiohttp_apispec.json_schema(ChangesSchema)
|
||||
async def post(self) -> None:
|
||||
"""
|
||||
insert new package changes
|
||||
|
||||
Raises:
|
||||
HTTPBadRequest: if bad data is supplied
|
||||
HTTPNoContent: in case of success response
|
||||
"""
|
||||
package_base = self.request.match_info["package"]
|
||||
|
||||
try:
|
||||
data = await self.request.json()
|
||||
last_commit_sha = data.get("last_commit_sha") # empty/null meant removal
|
||||
change = data.get("changes")
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
changes = Changes(last_commit_sha, change)
|
||||
repository_id = self.repository_id()
|
||||
self.service(repository_id).database.changes_insert(package_base, changes, repository_id)
|
||||
|
||||
raise HTTPNoContent
|
Reference in New Issue
Block a user