Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3875338fb7 | ||
|
|
196f55c58e |
@@ -1,3 +1,8 @@
|
||||
## [0.7.6] - 2025-12-09
|
||||
|
||||
* Fixed pull --preview bug in e2e test
|
||||
|
||||
|
||||
## [0.7.5] - 2025-12-09
|
||||
|
||||
* Fixed wrong directory permissions for nix
|
||||
|
||||
2
PKGBUILD
2
PKGBUILD
@@ -1,7 +1,7 @@
|
||||
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
|
||||
|
||||
pkgname=package-manager
|
||||
pkgver=0.7.5
|
||||
pkgver=0.7.6
|
||||
pkgrel=1
|
||||
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
|
||||
arch=('any')
|
||||
|
||||
6
debian/changelog
vendored
6
debian/changelog
vendored
@@ -1,3 +1,9 @@
|
||||
package-manager (0.7.6-1) unstable; urgency=medium
|
||||
|
||||
* Fixed pull --preview bug in e2e test
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 17:14:19 +0100
|
||||
|
||||
package-manager (0.7.5-1) unstable; urgency=medium
|
||||
|
||||
* Fixed wrong directory permissions for nix
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
rec {
|
||||
pkgmgr = pyPkgs.buildPythonApplication {
|
||||
pname = "package-manager";
|
||||
version = "0.7.5";
|
||||
version = "0.7.6";
|
||||
|
||||
# Use the git repo as source
|
||||
src = ./.;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: package-manager
|
||||
Version: 0.7.5
|
||||
Version: 0.7.6
|
||||
Release: 1%{?dist}
|
||||
Summary: Wrapper that runs Kevin's package-manager via Nix flake
|
||||
|
||||
@@ -77,6 +77,9 @@ echo ">>> package-manager removed. Nix itself was not removed."
|
||||
/usr/lib/package-manager/
|
||||
|
||||
%changelog
|
||||
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.6-1
|
||||
- Fixed pull --preview bug in e2e test
|
||||
|
||||
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.5-1
|
||||
- Fixed wrong directory permissions for nix
|
||||
|
||||
|
||||
@@ -1,35 +1,57 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||
from pkgmgr.core.repository.dir import get_repo_dir
|
||||
from pkgmgr.core.repository.verify import verify_repository
|
||||
|
||||
|
||||
def pull_with_verification(
|
||||
selected_repos,
|
||||
repositories_base_dir,
|
||||
all_repos,
|
||||
extra_args,
|
||||
no_verification,
|
||||
preview:bool):
|
||||
preview: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Executes "git pull" for each repository with verification.
|
||||
|
||||
Uses the verify_repository function in "pull" mode.
|
||||
If verification fails (and verification info is set) and --no-verification is not enabled,
|
||||
the user is prompted to confirm the pull.
|
||||
Execute `git pull` for each repository with verification.
|
||||
|
||||
- Uses verify_repository() in "pull" mode.
|
||||
- If verification fails (and verification info is set) and
|
||||
--no-verification is not enabled, the user is prompted to confirm
|
||||
the pull.
|
||||
- In preview mode, no interactive prompts are performed and no
|
||||
Git commands are executed; only the would-be command is printed.
|
||||
"""
|
||||
for repo in selected_repos:
|
||||
repo_identifier = get_repo_identifier(repo, all_repos)
|
||||
repo_dir = get_repo_dir(repositories_base_dir, repo)
|
||||
|
||||
if not os.path.exists(repo_dir):
|
||||
print(f"Repository directory '{repo_dir}' not found for {repo_identifier}.")
|
||||
continue
|
||||
|
||||
verified_info = repo.get("verified")
|
||||
verified_ok, errors, commit_hash, signing_key = verify_repository(repo, repo_dir, mode="pull", no_verification=no_verification)
|
||||
verified_ok, errors, commit_hash, signing_key = verify_repository(
|
||||
repo,
|
||||
repo_dir,
|
||||
mode="pull",
|
||||
no_verification=no_verification,
|
||||
)
|
||||
|
||||
if not no_verification and verified_info and not verified_ok:
|
||||
# Only prompt the user if:
|
||||
# - we are NOT in preview mode
|
||||
# - verification is enabled
|
||||
# - the repo has verification info configured
|
||||
# - verification failed
|
||||
if (
|
||||
not preview
|
||||
and not no_verification
|
||||
and verified_info
|
||||
and not verified_ok
|
||||
):
|
||||
print(f"Warning: Verification failed for {repo_identifier}:")
|
||||
for err in errors:
|
||||
print(f" - {err}")
|
||||
@@ -37,12 +59,19 @@ def pull_with_verification(
|
||||
if choice != "y":
|
||||
continue
|
||||
|
||||
full_cmd = f"git pull {' '.join(extra_args)}"
|
||||
# Build the git pull command (include extra args if present)
|
||||
args_part = " ".join(extra_args) if extra_args else ""
|
||||
full_cmd = f"git pull{(' ' + args_part) if args_part else ''}"
|
||||
|
||||
if preview:
|
||||
# Preview mode: only show the command, do not execute or prompt.
|
||||
print(f"[Preview] In '{repo_dir}': {full_cmd}")
|
||||
else:
|
||||
print(f"Running in '{repo_dir}': {full_cmd}")
|
||||
result = subprocess.run(full_cmd, cwd=repo_dir, shell=True)
|
||||
if result.returncode != 0:
|
||||
print(f"'git pull' for {repo_identifier} failed with exit code {result.returncode}.")
|
||||
print(
|
||||
f"'git pull' for {repo_identifier} failed "
|
||||
f"with exit code {result.returncode}."
|
||||
)
|
||||
sys.exit(result.returncode)
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "package-manager"
|
||||
version = "0.7.5"
|
||||
version = "0.7.6"
|
||||
description = "Kevin's package-manager tool (pkgmgr)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
309
tests/unit/pkgmgr/actions/repos/test_pull_with_verification.py
Normal file
309
tests/unit/pkgmgr/actions/repos/test_pull_with_verification.py
Normal file
@@ -0,0 +1,309 @@
|
||||
import io
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from pkgmgr.actions.repository.pull import pull_with_verification
|
||||
|
||||
|
||||
class TestPullWithVerification(unittest.TestCase):
|
||||
"""
|
||||
Comprehensive unit tests for pull_with_verification().
|
||||
|
||||
These tests verify:
|
||||
- Preview mode behaviour
|
||||
- Verification logic (prompting, bypassing, skipping)
|
||||
- subprocess.run invocation
|
||||
- Repository directory existence checks
|
||||
- Handling of extra git pull arguments
|
||||
"""
|
||||
|
||||
def _setup_mocks(self, mock_exists, mock_get_repo_id, mock_get_repo_dir,
|
||||
mock_verify, exists=True, verified_ok=True,
|
||||
errors=None, verified_info=True):
|
||||
"""Helper to configure repetitive mock behavior."""
|
||||
repo = {
|
||||
"name": "pkgmgr",
|
||||
"verified": {"gpg_keys": ["ABCDEF"]} if verified_info else None,
|
||||
}
|
||||
mock_exists.return_value = exists
|
||||
mock_get_repo_id.return_value = "pkgmgr"
|
||||
mock_get_repo_dir.return_value = "/fake/base/pkgmgr"
|
||||
mock_verify.return_value = (
|
||||
verified_ok,
|
||||
errors or [],
|
||||
"deadbeef", # commit hash
|
||||
"ABCDEF", # signing key
|
||||
)
|
||||
return repo
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
@patch("pkgmgr.actions.repository.pull.subprocess.run")
|
||||
@patch("pkgmgr.actions.repository.pull.verify_repository")
|
||||
@patch("pkgmgr.actions.repository.pull.get_repo_dir")
|
||||
@patch("pkgmgr.actions.repository.pull.get_repo_identifier")
|
||||
@patch("pkgmgr.actions.repository.pull.os.path.exists")
|
||||
@patch("builtins.input")
|
||||
def test_preview_mode_non_interactive(
|
||||
self,
|
||||
mock_input,
|
||||
mock_exists,
|
||||
mock_get_repo_id,
|
||||
mock_get_repo_dir,
|
||||
mock_verify,
|
||||
mock_subprocess,
|
||||
):
|
||||
"""
|
||||
Preview mode must NEVER request user input and must NEVER execute git.
|
||||
It must only print the preview command.
|
||||
"""
|
||||
repo = self._setup_mocks(
|
||||
mock_exists,
|
||||
mock_get_repo_id,
|
||||
mock_get_repo_dir,
|
||||
mock_verify,
|
||||
exists=True,
|
||||
verified_ok=False,
|
||||
errors=["bad signature"],
|
||||
verified_info=True,
|
||||
)
|
||||
|
||||
buf = io.StringIO()
|
||||
with patch("sys.stdout", new=buf):
|
||||
pull_with_verification(
|
||||
selected_repos=[repo],
|
||||
repositories_base_dir="/fake/base",
|
||||
all_repos=[repo],
|
||||
extra_args=["--ff-only"],
|
||||
no_verification=False,
|
||||
preview=True,
|
||||
)
|
||||
|
||||
output = buf.getvalue()
|
||||
self.assertIn(
|
||||
"[Preview] In '/fake/base/pkgmgr': git pull --ff-only",
|
||||
output,
|
||||
)
|
||||
|
||||
mock_input.assert_not_called()
|
||||
mock_subprocess.assert_not_called()
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
@patch("pkgmgr.actions.repository.pull.subprocess.run")
|
||||
@patch("pkgmgr.actions.repository.pull.verify_repository")
|
||||
@patch("pkgmgr.actions.repository.pull.get_repo_dir")
|
||||
@patch("pkgmgr.actions.repository.pull.get_repo_identifier")
|
||||
@patch("pkgmgr.actions.repository.pull.os.path.exists")
|
||||
@patch("builtins.input")
|
||||
def test_verification_failure_user_declines(
|
||||
self,
|
||||
mock_input,
|
||||
mock_exists,
|
||||
mock_get_repo_id,
|
||||
mock_get_repo_dir,
|
||||
mock_verify,
|
||||
mock_subprocess,
|
||||
):
|
||||
"""
|
||||
If verification fails and preview=False, the user is prompted.
|
||||
If the user declines ('n'), no git command is executed.
|
||||
"""
|
||||
repo = self._setup_mocks(
|
||||
mock_exists,
|
||||
mock_get_repo_id,
|
||||
mock_get_repo_dir,
|
||||
mock_verify,
|
||||
verified_ok=False,
|
||||
errors=["signature invalid"],
|
||||
)
|
||||
|
||||
mock_input.return_value = "n"
|
||||
|
||||
buf = io.StringIO()
|
||||
with patch("sys.stdout", new=buf):
|
||||
pull_with_verification(
|
||||
selected_repos=[repo],
|
||||
repositories_base_dir="/fake/base",
|
||||
all_repos=[repo],
|
||||
extra_args=[],
|
||||
no_verification=False,
|
||||
preview=False,
|
||||
)
|
||||
|
||||
mock_input.assert_called_once()
|
||||
mock_subprocess.assert_not_called()
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
@patch("pkgmgr.actions.repository.pull.subprocess.run")
|
||||
@patch("pkgmgr.actions.repository.pull.verify_repository")
|
||||
@patch("pkgmgr.actions.repository.pull.get_repo_dir")
|
||||
@patch("pkgmgr.actions.repository.pull.get_repo_identifier")
|
||||
@patch("pkgmgr.actions.repository.pull.os.path.exists")
|
||||
@patch("builtins.input")
|
||||
def test_verification_failure_user_accepts_runs_git(
|
||||
self,
|
||||
mock_input,
|
||||
mock_exists,
|
||||
mock_get_repo_id,
|
||||
mock_get_repo_dir,
|
||||
mock_verify,
|
||||
mock_subprocess,
|
||||
):
|
||||
"""
|
||||
If verification fails and the user accepts ('y'),
|
||||
then the git pull should be executed.
|
||||
"""
|
||||
repo = self._setup_mocks(
|
||||
mock_exists,
|
||||
mock_get_repo_id,
|
||||
mock_get_repo_dir,
|
||||
mock_verify,
|
||||
verified_ok=False,
|
||||
errors=["invalid"],
|
||||
)
|
||||
|
||||
mock_input.return_value = "y"
|
||||
mock_subprocess.return_value = MagicMock(returncode=0)
|
||||
|
||||
pull_with_verification(
|
||||
selected_repos=[repo],
|
||||
repositories_base_dir="/fake/base",
|
||||
all_repos=[repo],
|
||||
extra_args=[],
|
||||
no_verification=False,
|
||||
preview=False,
|
||||
)
|
||||
|
||||
mock_subprocess.assert_called_once()
|
||||
mock_input.assert_called_once()
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
@patch("pkgmgr.actions.repository.pull.subprocess.run")
|
||||
@patch("pkgmgr.actions.repository.pull.verify_repository")
|
||||
@patch("pkgmgr.actions.repository.pull.get_repo_dir")
|
||||
@patch("pkgmgr.actions.repository.pull.get_repo_identifier")
|
||||
@patch("pkgmgr.actions.repository.pull.os.path.exists")
|
||||
@patch("builtins.input")
|
||||
def test_verification_success_no_prompt(
|
||||
self,
|
||||
mock_input,
|
||||
mock_exists,
|
||||
mock_get_repo_id,
|
||||
mock_get_repo_dir,
|
||||
mock_verify,
|
||||
mock_subprocess,
|
||||
):
|
||||
"""
|
||||
If verification is successful, the user should NOT be prompted,
|
||||
and git pull should run immediately.
|
||||
"""
|
||||
repo = self._setup_mocks(
|
||||
mock_exists,
|
||||
mock_get_repo_id,
|
||||
mock_get_repo_dir,
|
||||
mock_verify,
|
||||
verified_ok=True,
|
||||
)
|
||||
|
||||
mock_subprocess.return_value = MagicMock(returncode=0)
|
||||
|
||||
pull_with_verification(
|
||||
selected_repos=[repo],
|
||||
repositories_base_dir="/fake/base",
|
||||
all_repos=[repo],
|
||||
extra_args=["--rebase"],
|
||||
no_verification=False,
|
||||
preview=False,
|
||||
)
|
||||
|
||||
mock_input.assert_not_called()
|
||||
mock_subprocess.assert_called_once()
|
||||
cmd = mock_subprocess.call_args[0][0]
|
||||
self.assertIn("git pull --rebase", cmd)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
@patch("pkgmgr.actions.repository.pull.subprocess.run")
|
||||
@patch("pkgmgr.actions.repository.pull.verify_repository")
|
||||
@patch("pkgmgr.actions.repository.pull.get_repo_dir")
|
||||
@patch("pkgmgr.actions.repository.pull.get_repo_identifier")
|
||||
@patch("pkgmgr.actions.repository.pull.os.path.exists")
|
||||
@patch("builtins.input")
|
||||
def test_directory_missing_skips_repo(
|
||||
self,
|
||||
mock_input,
|
||||
mock_exists,
|
||||
mock_get_repo_id,
|
||||
mock_get_repo_dir,
|
||||
mock_verify,
|
||||
mock_subprocess,
|
||||
):
|
||||
"""
|
||||
If the repository directory does not exist, the repo must be skipped
|
||||
silently and no git command executed.
|
||||
"""
|
||||
repo = self._setup_mocks(
|
||||
mock_exists,
|
||||
mock_get_repo_id,
|
||||
mock_get_repo_dir,
|
||||
mock_verify,
|
||||
exists=False,
|
||||
)
|
||||
|
||||
buf = io.StringIO()
|
||||
with patch("sys.stdout", new=buf):
|
||||
pull_with_verification(
|
||||
selected_repos=[repo],
|
||||
repositories_base_dir="/fake/base",
|
||||
all_repos=[repo],
|
||||
extra_args=[],
|
||||
no_verification=False,
|
||||
preview=False,
|
||||
)
|
||||
|
||||
output = buf.getvalue()
|
||||
self.assertIn("not found", output)
|
||||
|
||||
mock_input.assert_not_called()
|
||||
mock_subprocess.assert_not_called()
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
@patch("pkgmgr.actions.repository.pull.subprocess.run")
|
||||
@patch("pkgmgr.actions.repository.pull.verify_repository")
|
||||
@patch("pkgmgr.actions.repository.pull.get_repo_dir")
|
||||
@patch("pkgmgr.actions.repository.pull.get_repo_identifier")
|
||||
@patch("pkgmgr.actions.repository.pull.os.path.exists")
|
||||
@patch("builtins.input")
|
||||
def test_no_verification_flag_skips_prompt(
|
||||
self,
|
||||
mock_input,
|
||||
mock_exists,
|
||||
mock_get_repo_id,
|
||||
mock_get_repo_dir,
|
||||
mock_verify,
|
||||
mock_subprocess,
|
||||
):
|
||||
"""
|
||||
If no_verification=True, verification failures must NOT prompt.
|
||||
Git pull should run directly.
|
||||
"""
|
||||
repo = self._setup_mocks(
|
||||
mock_exists,
|
||||
mock_get_repo_id,
|
||||
mock_get_repo_dir,
|
||||
mock_verify,
|
||||
verified_ok=False,
|
||||
errors=["invalid"],
|
||||
)
|
||||
|
||||
mock_subprocess.return_value = MagicMock(returncode=0)
|
||||
|
||||
pull_with_verification(
|
||||
selected_repos=[repo],
|
||||
repositories_base_dir="/fake/base",
|
||||
all_repos=[repo],
|
||||
extra_args=[],
|
||||
no_verification=True,
|
||||
preview=False,
|
||||
)
|
||||
|
||||
mock_input.assert_not_called()
|
||||
mock_subprocess.assert_called_once()
|
||||
Reference in New Issue
Block a user