Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b483e178d | ||
|
|
78693225f1 | ||
|
|
ca08c84789 | ||
|
|
e930b422e5 | ||
|
|
0833d04376 | ||
|
|
55f36d76ec | ||
|
|
6a838ee84f | ||
|
|
4285bf4a54 | ||
|
|
640b1042c2 | ||
|
|
9357c4632e | ||
|
|
ca5d0d22f3 | ||
|
|
3875338fb7 | ||
|
|
196f55c58e | ||
|
|
9a149715f6 | ||
|
|
bf40533469 | ||
|
|
7bc7259988 | ||
|
|
66b96ac3a5 | ||
|
|
f974e0b14a | ||
|
|
de8c3f768d |
2
.github/workflows/test-container.yml
vendored
2
.github/workflows/test-container.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Test Distribution Containers
|
name: Test OS Containers
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
2
.github/workflows/test-e2e.yml
vendored
2
.github/workflows/test-e2e.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Test package-manager (e2e)
|
name: Test End-To-End
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|||||||
8
.github/workflows/test-integration.yml
vendored
8
.github/workflows/test-integration.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Test package-manager (integration)
|
name: Test Code Integration
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -21,9 +21,5 @@ jobs:
|
|||||||
- name: Show Docker version
|
- name: Show Docker version
|
||||||
run: docker version
|
run: docker version
|
||||||
|
|
||||||
# Build Arch test image (same as used in test-unit and test-e2e)
|
|
||||||
- name: Build test images
|
|
||||||
run: make build
|
|
||||||
|
|
||||||
- name: Run integration tests via make (Arch container)
|
- name: Run integration tests via make (Arch container)
|
||||||
run: make test-integration
|
run: make test-integration DISTROS="arch"
|
||||||
|
|||||||
4
.github/workflows/test-unit.yml
vendored
4
.github/workflows/test-unit.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Test package-manager (unit)
|
name: Test Units
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -22,4 +22,4 @@ jobs:
|
|||||||
run: docker version
|
run: docker version
|
||||||
|
|
||||||
- name: Run unit tests via make (Arch container)
|
- name: Run unit tests via make (Arch container)
|
||||||
run: make test-unit
|
run: make test-unit DISTROS="arch"
|
||||||
|
|||||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -1,3 +1,43 @@
|
|||||||
|
## [0.7.10] - 2025-12-09
|
||||||
|
|
||||||
|
* Fixed test_install_pkgmgr_shallow.py
|
||||||
|
|
||||||
|
|
||||||
|
## [0.7.9] - 2025-12-09
|
||||||
|
|
||||||
|
* 'main' and 'master' are now both accepted as branches for branch close merge
|
||||||
|
|
||||||
|
|
||||||
|
## [0.7.8] - 2025-12-09
|
||||||
|
|
||||||
|
* Missing pyproject.toml doesn't lead to an error during release
|
||||||
|
|
||||||
|
|
||||||
|
## [0.7.7] - 2025-12-09
|
||||||
|
|
||||||
|
* Added TEST_PATTERN parameter to execute dedicated tests
|
||||||
|
|
||||||
|
|
||||||
|
## [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
|
||||||
|
|
||||||
|
|
||||||
|
## [0.7.4] - 2025-12-09
|
||||||
|
|
||||||
|
* Fixed missing build in test workflow -> Tests pass now
|
||||||
|
|
||||||
|
|
||||||
|
## [0.7.3] - 2025-12-09
|
||||||
|
|
||||||
|
* Fixed bug: Ignored packages are now ignored
|
||||||
|
|
||||||
|
|
||||||
## [0.7.2] - 2025-12-09
|
## [0.7.2] - 2025-12-09
|
||||||
|
|
||||||
* Implemented Changelog Support for Fedora and Debian
|
* Implemented Changelog Support for Fedora and Debian
|
||||||
|
|||||||
16
Makefile
16
Makefile
@@ -12,7 +12,7 @@ NIX_CACHE_VOLUME := pkgmgr_nix_cache
|
|||||||
# Distro list and base images
|
# Distro list and base images
|
||||||
# (kept for documentation/reference; actual build logic is in scripts/build)
|
# (kept for documentation/reference; actual build logic is in scripts/build)
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
DISTROS := arch debian ubuntu fedora centos
|
DISTROS := arch debian ubuntu fedora centos
|
||||||
BASE_IMAGE_ARCH := archlinux:latest
|
BASE_IMAGE_ARCH := archlinux:latest
|
||||||
BASE_IMAGE_DEBIAN := debian:stable-slim
|
BASE_IMAGE_DEBIAN := debian:stable-slim
|
||||||
BASE_IMAGE_UBUNTU := ubuntu:latest
|
BASE_IMAGE_UBUNTU := ubuntu:latest
|
||||||
@@ -27,6 +27,10 @@ export BASE_IMAGE_UBUNTU
|
|||||||
export BASE_IMAGE_FEDORA
|
export BASE_IMAGE_FEDORA
|
||||||
export BASE_IMAGE_CENTOS
|
export BASE_IMAGE_CENTOS
|
||||||
|
|
||||||
|
# PYthon Unittest Pattern
|
||||||
|
TEST_PATTERN := test_*.py
|
||||||
|
export TEST_PATTERN
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# PKGMGR setup (developer wrapper -> scripts/installation/main.sh)
|
# PKGMGR setup (developer wrapper -> scripts/installation/main.sh)
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
@@ -46,16 +50,16 @@ build:
|
|||||||
# Test targets (delegated to scripts/test)
|
# Test targets (delegated to scripts/test)
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
test-unit:
|
test-unit: build-missing
|
||||||
@bash scripts/test/test-unit.sh
|
@bash scripts/test/test-unit.sh
|
||||||
|
|
||||||
test-integration:
|
test-integration: build-missing
|
||||||
@bash scripts/test/test-integration.sh
|
@bash scripts/test/test-integration.sh
|
||||||
|
|
||||||
test-e2e:
|
test-e2e: build-missing
|
||||||
@bash scripts/test/test-e2e.sh
|
@bash scripts/test/test-e2e.sh
|
||||||
|
|
||||||
test-container:
|
test-container: build-missing
|
||||||
@bash scripts/test/test-container.sh
|
@bash scripts/test/test-container.sh
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
@@ -65,7 +69,7 @@ build-missing:
|
|||||||
@bash scripts/build/build-image-missing.sh
|
@bash scripts/build/build-image-missing.sh
|
||||||
|
|
||||||
# Combined test target for local + CI (unit + e2e + integration)
|
# Combined test target for local + CI (unit + e2e + integration)
|
||||||
test: build-missing test-container test-unit test-e2e test-integration
|
test: test-container test-unit test-e2e test-integration
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# System install (native packages, calls scripts/installation/run-package.sh)
|
# System install (native packages, calls scripts/installation/run-package.sh)
|
||||||
|
|||||||
2
PKGBUILD
2
PKGBUILD
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
|
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
|
||||||
|
|
||||||
pkgname=package-manager
|
pkgname=package-manager
|
||||||
pkgver=0.7.2
|
pkgver=0.7.10
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
|
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
|||||||
48
debian/changelog
vendored
48
debian/changelog
vendored
@@ -1,3 +1,51 @@
|
|||||||
|
package-manager (0.7.10-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Fixed test_install_pkgmgr_shallow.py
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 22:57:08 +0100
|
||||||
|
|
||||||
|
package-manager (0.7.9-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* 'main' and 'master' are now both accepted as branches for branch close merge
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 21:19:13 +0100
|
||||||
|
|
||||||
|
package-manager (0.7.8-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Missing pyproject.toml doesn't lead to an error during release
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 21:03:24 +0100
|
||||||
|
|
||||||
|
package-manager (0.7.7-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Added TEST_PATTERN parameter to execute dedicated tests
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 17:54:38 +0100
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 16:45:42 +0100
|
||||||
|
|
||||||
|
package-manager (0.7.4-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Fixed missing build in test workflow -> Tests pass now
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 16:22:00 +0100
|
||||||
|
|
||||||
|
package-manager (0.7.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Fixed bug: Ignored packages are now ignored
|
||||||
|
|
||||||
|
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 16:08:31 +0100
|
||||||
|
|
||||||
package-manager (0.7.2-1) unstable; urgency=medium
|
package-manager (0.7.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
* Implemented Changelog Support for Fedora and Debian
|
* Implemented Changelog Support for Fedora and Debian
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
rec {
|
rec {
|
||||||
pkgmgr = pyPkgs.buildPythonApplication {
|
pkgmgr = pyPkgs.buildPythonApplication {
|
||||||
pname = "package-manager";
|
pname = "package-manager";
|
||||||
version = "0.7.2";
|
version = "0.7.10";
|
||||||
|
|
||||||
# Use the git repo as source
|
# Use the git repo as source
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: package-manager
|
Name: package-manager
|
||||||
Version: 0.7.2
|
Version: 0.7.10
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: Wrapper that runs Kevin's package-manager via Nix flake
|
Summary: Wrapper that runs Kevin's package-manager via Nix flake
|
||||||
|
|
||||||
@@ -77,6 +77,30 @@ echo ">>> package-manager removed. Nix itself was not removed."
|
|||||||
/usr/lib/package-manager/
|
/usr/lib/package-manager/
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.10-1
|
||||||
|
- Fixed test_install_pkgmgr_shallow.py
|
||||||
|
|
||||||
|
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.9-1
|
||||||
|
- 'main' and 'master' are now both accepted as branches for branch close merge
|
||||||
|
|
||||||
|
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.8-1
|
||||||
|
- Missing pyproject.toml doesn't lead to an error during release
|
||||||
|
|
||||||
|
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.7-1
|
||||||
|
- Added TEST_PATTERN parameter to execute dedicated tests
|
||||||
|
|
||||||
|
* 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
|
||||||
|
|
||||||
|
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.4-1
|
||||||
|
- Fixed missing build in test workflow -> Tests pass now
|
||||||
|
|
||||||
|
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.3-1
|
||||||
|
- Fixed bug: Ignored packages are now ignored
|
||||||
|
|
||||||
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.2-1
|
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.2-1
|
||||||
- Implemented Changelog Support for Fedora and Debian
|
- Implemented Changelog Support for Fedora and Debian
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# pkgmgr/branch_commands.py
|
# pkgmgr/actions/branch/__init__.py
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
@@ -16,30 +16,43 @@ from typing import Optional
|
|||||||
from pkgmgr.core.git import run_git, GitError, get_current_branch
|
from pkgmgr.core.git import run_git, GitError, get_current_branch
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Branch creation (open)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def open_branch(
|
def open_branch(
|
||||||
name: Optional[str],
|
name: Optional[str],
|
||||||
base_branch: str = "main",
|
base_branch: str = "main",
|
||||||
|
fallback_base: str = "master",
|
||||||
cwd: str = ".",
|
cwd: str = ".",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Create and push a new feature branch on top of `base_branch`.
|
Create and push a new feature branch on top of a base branch.
|
||||||
|
|
||||||
|
The base branch is resolved by:
|
||||||
|
1. Trying 'base_branch' (default: 'main')
|
||||||
|
2. Falling back to 'fallback_base' (default: 'master')
|
||||||
|
|
||||||
Steps:
|
Steps:
|
||||||
1) git fetch origin
|
1) git fetch origin
|
||||||
2) git checkout <base_branch>
|
2) git checkout <resolved_base>
|
||||||
3) git pull origin <base_branch>
|
3) git pull origin <resolved_base>
|
||||||
4) git checkout -b <name>
|
4) git checkout -b <name>
|
||||||
5) git push -u origin <name>
|
5) git push -u origin <name>
|
||||||
|
|
||||||
If `name` is None or empty, the user is prompted on stdin.
|
If `name` is None or empty, the user is prompted to enter one.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Request name interactively if not provided
|
||||||
if not name:
|
if not name:
|
||||||
name = input("Enter new branch name: ").strip()
|
name = input("Enter new branch name: ").strip()
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
raise RuntimeError("Branch name must not be empty.")
|
raise RuntimeError("Branch name must not be empty.")
|
||||||
|
|
||||||
|
# Resolve which base branch to use (main or master)
|
||||||
|
resolved_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
|
||||||
|
|
||||||
# 1) Fetch from origin
|
# 1) Fetch from origin
|
||||||
try:
|
try:
|
||||||
run_git(["fetch", "origin"], cwd=cwd)
|
run_git(["fetch", "origin"], cwd=cwd)
|
||||||
@@ -50,18 +63,18 @@ def open_branch(
|
|||||||
|
|
||||||
# 2) Checkout base branch
|
# 2) Checkout base branch
|
||||||
try:
|
try:
|
||||||
run_git(["checkout", base_branch], cwd=cwd)
|
run_git(["checkout", resolved_base], cwd=cwd)
|
||||||
except GitError as exc:
|
except GitError as exc:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Failed to checkout base branch {base_branch!r}: {exc}"
|
f"Failed to checkout base branch {resolved_base!r}: {exc}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
# 3) Pull latest changes on base
|
# 3) Pull latest changes for base branch
|
||||||
try:
|
try:
|
||||||
run_git(["pull", "origin", base_branch], cwd=cwd)
|
run_git(["pull", "origin", resolved_base], cwd=cwd)
|
||||||
except GitError as exc:
|
except GitError as exc:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Failed to pull latest changes for base branch {base_branch!r}: {exc}"
|
f"Failed to pull latest changes for base branch {resolved_base!r}: {exc}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
# 4) Create new branch
|
# 4) Create new branch
|
||||||
@@ -69,10 +82,10 @@ def open_branch(
|
|||||||
run_git(["checkout", "-b", name], cwd=cwd)
|
run_git(["checkout", "-b", name], cwd=cwd)
|
||||||
except GitError as exc:
|
except GitError as exc:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Failed to create new branch {name!r} from base {base_branch!r}: {exc}"
|
f"Failed to create new branch {name!r} from base {resolved_base!r}: {exc}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
# 5) Push and set upstream
|
# 5) Push new branch to origin
|
||||||
try:
|
try:
|
||||||
run_git(["push", "-u", "origin", name], cwd=cwd)
|
run_git(["push", "-u", "origin", name], cwd=cwd)
|
||||||
except GitError as exc:
|
except GitError as exc:
|
||||||
@@ -81,15 +94,21 @@ def open_branch(
|
|||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Base branch resolver (shared by open/close)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _resolve_base_branch(
|
def _resolve_base_branch(
|
||||||
preferred: str,
|
preferred: str,
|
||||||
fallback: str,
|
fallback: str,
|
||||||
cwd: str,
|
cwd: str,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Resolve the base branch to use for merging.
|
Resolve the base branch to use.
|
||||||
|
|
||||||
|
Try `preferred` first (default: main),
|
||||||
|
fall back to `fallback` (default: master).
|
||||||
|
|
||||||
Try `preferred` (default: main) first, then `fallback` (default: master).
|
|
||||||
Raise RuntimeError if neither exists.
|
Raise RuntimeError if neither exists.
|
||||||
"""
|
"""
|
||||||
for candidate in (preferred, fallback):
|
for candidate in (preferred, fallback):
|
||||||
@@ -104,6 +123,10 @@ def _resolve_base_branch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Branch closing (merge + deletion)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def close_branch(
|
def close_branch(
|
||||||
name: Optional[str],
|
name: Optional[str],
|
||||||
base_branch: str = "main",
|
base_branch: str = "main",
|
||||||
@@ -111,23 +134,22 @@ def close_branch(
|
|||||||
cwd: str = ".",
|
cwd: str = ".",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Merge a feature branch into the main/master branch and optionally delete it.
|
Merge a feature branch into the base branch and delete it afterwards.
|
||||||
|
|
||||||
Steps:
|
Steps:
|
||||||
1) Determine branch name (argument or current branch)
|
1) Determine the branch name (argument or current branch)
|
||||||
2) Resolve base branch (prefers `base_branch`, falls back to `fallback_base`)
|
2) Resolve base branch (main/master)
|
||||||
3) Ask for confirmation (y/N)
|
3) Ask for confirmation
|
||||||
4) git fetch origin
|
4) git fetch origin
|
||||||
5) git checkout <base>
|
5) git checkout <base>
|
||||||
6) git pull origin <base>
|
6) git pull origin <base>
|
||||||
7) git merge --no-ff <name>
|
7) git merge --no-ff <name>
|
||||||
8) git push origin <base>
|
8) git push origin <base>
|
||||||
9) Delete branch locally and on origin
|
9) Delete branch locally
|
||||||
|
10) Delete branch on origin (best effort)
|
||||||
If the user does not confirm with 'y', the operation is aborted.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 1) Determine which branch to close
|
# 1) Determine which branch should be closed
|
||||||
if not name:
|
if not name:
|
||||||
try:
|
try:
|
||||||
name = get_current_branch(cwd=cwd)
|
name = get_current_branch(cwd=cwd)
|
||||||
@@ -137,7 +159,7 @@ def close_branch(
|
|||||||
if not name:
|
if not name:
|
||||||
raise RuntimeError("Branch name must not be empty.")
|
raise RuntimeError("Branch name must not be empty.")
|
||||||
|
|
||||||
# 2) Resolve base branch (main/master)
|
# 2) Resolve base branch
|
||||||
target_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
|
target_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
|
||||||
|
|
||||||
if name == target_base:
|
if name == target_base:
|
||||||
@@ -146,7 +168,7 @@ def close_branch(
|
|||||||
"Please specify a feature branch."
|
"Please specify a feature branch."
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3) Confirmation prompt
|
# 3) Ask user for confirmation
|
||||||
prompt = (
|
prompt = (
|
||||||
f"Merge branch '{name}' into '{target_base}' and delete it afterwards? "
|
f"Merge branch '{name}' into '{target_base}' and delete it afterwards? "
|
||||||
"(y/N): "
|
"(y/N): "
|
||||||
@@ -164,7 +186,7 @@ def close_branch(
|
|||||||
f"Failed to fetch from origin before closing branch {name!r}: {exc}"
|
f"Failed to fetch from origin before closing branch {name!r}: {exc}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
# 5) Checkout base branch
|
# 5) Checkout base
|
||||||
try:
|
try:
|
||||||
run_git(["checkout", target_base], cwd=cwd)
|
run_git(["checkout", target_base], cwd=cwd)
|
||||||
except GitError as exc:
|
except GitError as exc:
|
||||||
@@ -172,7 +194,7 @@ def close_branch(
|
|||||||
f"Failed to checkout base branch {target_base!r}: {exc}"
|
f"Failed to checkout base branch {target_base!r}: {exc}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
# 6) Pull latest base
|
# 6) Pull latest base state
|
||||||
try:
|
try:
|
||||||
run_git(["pull", "origin", target_base], cwd=cwd)
|
run_git(["pull", "origin", target_base], cwd=cwd)
|
||||||
except GitError as exc:
|
except GitError as exc:
|
||||||
@@ -180,7 +202,7 @@ def close_branch(
|
|||||||
f"Failed to pull latest changes for base branch {target_base!r}: {exc}"
|
f"Failed to pull latest changes for base branch {target_base!r}: {exc}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
# 7) Merge feature branch into base
|
# 7) Merge the feature branch
|
||||||
try:
|
try:
|
||||||
run_git(["merge", "--no-ff", name], cwd=cwd)
|
run_git(["merge", "--no-ff", name], cwd=cwd)
|
||||||
except GitError as exc:
|
except GitError as exc:
|
||||||
@@ -193,22 +215,21 @@ def close_branch(
|
|||||||
run_git(["push", "origin", target_base], cwd=cwd)
|
run_git(["push", "origin", target_base], cwd=cwd)
|
||||||
except GitError as exc:
|
except GitError as exc:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Failed to push base branch {target_base!r} to origin after merge: {exc}"
|
f"Failed to push base branch {target_base!r} after merge: {exc}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
# 9) Delete feature branch locally
|
# 9) Delete branch locally
|
||||||
try:
|
try:
|
||||||
run_git(["branch", "-d", name], cwd=cwd)
|
run_git(["branch", "-d", name], cwd=cwd)
|
||||||
except GitError as exc:
|
except GitError as exc:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Failed to delete local branch {name!r} after merge: {exc}"
|
f"Failed to delete local branch {name!r}: {exc}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
# 10) Delete feature branch on origin (best effort)
|
# 10) Delete branch on origin (best effort)
|
||||||
try:
|
try:
|
||||||
run_git(["push", "origin", "--delete", name], cwd=cwd)
|
run_git(["push", "origin", "--delete", name], cwd=cwd)
|
||||||
except GitError as exc:
|
except GitError as exc:
|
||||||
# Remote delete is nice-to-have; surface as RuntimeError for clarity.
|
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Branch {name!r} was deleted locally, but remote deletion failed: {exc}"
|
f"Branch {name!r} was deleted locally, but remote deletion failed: {exc}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ def _open_editor_for_changelog(initial_message: Optional[str] = None) -> str:
|
|||||||
# File update helpers (pyproject + extra packaging + changelog)
|
# File update helpers (pyproject + extra packaging + changelog)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def update_pyproject_version(
|
def update_pyproject_version(
|
||||||
pyproject_path: str,
|
pyproject_path: str,
|
||||||
new_version: str,
|
new_version: str,
|
||||||
@@ -99,13 +98,25 @@ def update_pyproject_version(
|
|||||||
version = "X.Y.Z"
|
version = "X.Y.Z"
|
||||||
|
|
||||||
and replaces the version part with the given new_version string.
|
and replaces the version part with the given new_version string.
|
||||||
|
|
||||||
|
If the file does not exist, it is skipped without failing the release.
|
||||||
"""
|
"""
|
||||||
|
if not os.path.exists(pyproject_path):
|
||||||
|
print(
|
||||||
|
f"[INFO] pyproject.toml not found at: {pyproject_path}, "
|
||||||
|
"skipping version update."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(pyproject_path, "r", encoding="utf-8") as f:
|
with open(pyproject_path, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
except FileNotFoundError:
|
except OSError as exc:
|
||||||
print(f"[ERROR] pyproject.toml not found at: {pyproject_path}")
|
print(
|
||||||
sys.exit(1)
|
f"[WARN] Could not read pyproject.toml at {pyproject_path}: {exc}. "
|
||||||
|
"Skipping version update."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
pattern = r'^(version\s*=\s*")([^"]+)(")'
|
pattern = r'^(version\s*=\s*")([^"]+)(")'
|
||||||
new_content, count = re.subn(
|
new_content, count = re.subn(
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ Behavior:
|
|||||||
* Then install the flake outputs (`pkgmgr`, `default`) via `nix profile install`.
|
* Then install the flake outputs (`pkgmgr`, `default`) via `nix profile install`.
|
||||||
- Failure installing `pkgmgr` is treated as fatal.
|
- Failure installing `pkgmgr` is treated as fatal.
|
||||||
- Failure installing `default` is logged as an error/warning but does not abort.
|
- Failure installing `default` is logged as an error/warning but does not abort.
|
||||||
|
|
||||||
|
Special handling for dev shells / CI:
|
||||||
|
- If IN_NIX_SHELL is set (e.g. inside `nix develop`), the installer is
|
||||||
|
disabled. In that environment the flake outputs are already provided
|
||||||
|
by the dev shell and we must not touch the user profile.
|
||||||
|
- If PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 is set, the installer is
|
||||||
|
globally disabled (useful for CI or debugging).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -36,14 +43,45 @@ class NixFlakeInstaller(BaseInstaller):
|
|||||||
FLAKE_FILE = "flake.nix"
|
FLAKE_FILE = "flake.nix"
|
||||||
PROFILE_NAME = "package-manager"
|
PROFILE_NAME = "package-manager"
|
||||||
|
|
||||||
|
def _in_nix_shell(self) -> bool:
|
||||||
|
"""
|
||||||
|
Return True if we appear to be running inside a Nix dev shell.
|
||||||
|
|
||||||
|
Nix sets IN_NIX_SHELL in `nix develop` environments. In that case
|
||||||
|
the flake outputs are already available, and touching the user
|
||||||
|
profile (nix profile install/remove) is undesirable.
|
||||||
|
"""
|
||||||
|
return bool(os.environ.get("IN_NIX_SHELL"))
|
||||||
|
|
||||||
def supports(self, ctx: "RepoContext") -> bool:
|
def supports(self, ctx: "RepoContext") -> bool:
|
||||||
"""
|
"""
|
||||||
Only support repositories that:
|
Only support repositories that:
|
||||||
- Have a flake.nix
|
- Are NOT inside a Nix dev shell (IN_NIX_SHELL unset),
|
||||||
|
- Are NOT explicitly disabled via PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1,
|
||||||
|
- Have a flake.nix,
|
||||||
- And have the `nix` command available.
|
- And have the `nix` command available.
|
||||||
"""
|
"""
|
||||||
|
# 1) Skip when running inside a dev shell – flake is already active.
|
||||||
|
if self._in_nix_shell():
|
||||||
|
print(
|
||||||
|
"[INFO] IN_NIX_SHELL detected; skipping NixFlakeInstaller. "
|
||||||
|
"Flake outputs are provided by the development shell."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 2) Optional global kill-switch for CI or debugging.
|
||||||
|
if os.environ.get("PKGMGR_DISABLE_NIX_FLAKE_INSTALLER") == "1":
|
||||||
|
print(
|
||||||
|
"[INFO] PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 – "
|
||||||
|
"NixFlakeInstaller is disabled."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 3) Nix must be available.
|
||||||
if shutil.which("nix") is None:
|
if shutil.which("nix") is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# 4) flake.nix must exist in the repository.
|
||||||
flake_path = os.path.join(ctx.repo_dir, self.FLAKE_FILE)
|
flake_path = os.path.join(ctx.repo_dir, self.FLAKE_FILE)
|
||||||
return os.path.exists(flake_path)
|
return os.path.exists(flake_path)
|
||||||
|
|
||||||
@@ -76,6 +114,14 @@ class NixFlakeInstaller(BaseInstaller):
|
|||||||
Any failure installing `pkgmgr` is treated as fatal (SystemExit).
|
Any failure installing `pkgmgr` is treated as fatal (SystemExit).
|
||||||
A failure installing `default` is logged but does not abort.
|
A failure installing `default` is logged but does not abort.
|
||||||
"""
|
"""
|
||||||
|
# Extra guard in case run() is called directly without supports().
|
||||||
|
if self._in_nix_shell():
|
||||||
|
print(
|
||||||
|
"[INFO] IN_NIX_SHELL detected in run(); "
|
||||||
|
"skipping Nix flake profile installation."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Reuse supports() to keep logic in one place
|
# Reuse supports() to keep logic in one place
|
||||||
if not self.supports(ctx): # type: ignore[arg-type]
|
if not self.supports(ctx): # type: ignore[arg-type]
|
||||||
return
|
return
|
||||||
@@ -91,7 +137,12 @@ class NixFlakeInstaller(BaseInstaller):
|
|||||||
try:
|
try:
|
||||||
# For 'default' we don't want the process to exit on error
|
# For 'default' we don't want the process to exit on error
|
||||||
allow_failure = output == "default"
|
allow_failure = output == "default"
|
||||||
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview, allow_failure=allow_failure)
|
run_command(
|
||||||
|
cmd,
|
||||||
|
cwd=ctx.repo_dir,
|
||||||
|
preview=ctx.preview,
|
||||||
|
allow_failure=allow_failure,
|
||||||
|
)
|
||||||
print(f"Nix flake output '{output}' successfully installed.")
|
print(f"Nix flake output '{output}' successfully installed.")
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
print(f"[Error] Failed to install Nix flake output '{output}': {e}")
|
print(f"[Error] Failed to install Nix flake output '{output}': {e}")
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ apt/dpkg tooling are available.
|
|||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from pkgmgr.actions.repository.install.context import RepoContext
|
from pkgmgr.actions.repository.install.context import RepoContext
|
||||||
@@ -68,6 +67,32 @@ class DebianControlInstaller(BaseInstaller):
|
|||||||
pattern = os.path.join(parent, "*.deb")
|
pattern = os.path.join(parent, "*.deb")
|
||||||
return sorted(glob.glob(pattern))
|
return sorted(glob.glob(pattern))
|
||||||
|
|
||||||
|
def _privileged_prefix(self) -> str | None:
|
||||||
|
"""
|
||||||
|
Determine how to run privileged commands:
|
||||||
|
|
||||||
|
- If 'sudo' is available, return 'sudo '.
|
||||||
|
- If we are running as root (e.g. inside CI/container), return ''.
|
||||||
|
- Otherwise, return None, meaning we cannot safely elevate.
|
||||||
|
|
||||||
|
Callers are responsible for handling the None case (usually by
|
||||||
|
warning and skipping automatic installation).
|
||||||
|
"""
|
||||||
|
sudo_path = shutil.which("sudo")
|
||||||
|
|
||||||
|
is_root = False
|
||||||
|
try:
|
||||||
|
is_root = os.geteuid() == 0
|
||||||
|
except AttributeError: # pragma: no cover - non-POSIX platforms
|
||||||
|
# On non-POSIX systems, fall back to assuming "not root".
|
||||||
|
is_root = False
|
||||||
|
|
||||||
|
if sudo_path is not None:
|
||||||
|
return "sudo "
|
||||||
|
if is_root:
|
||||||
|
return ""
|
||||||
|
return None
|
||||||
|
|
||||||
def _install_build_dependencies(self, ctx: RepoContext) -> None:
|
def _install_build_dependencies(self, ctx: RepoContext) -> None:
|
||||||
"""
|
"""
|
||||||
Install build dependencies using `apt-get build-dep ./`.
|
Install build dependencies using `apt-get build-dep ./`.
|
||||||
@@ -86,12 +111,25 @@ class DebianControlInstaller(BaseInstaller):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
prefix = self._privileged_prefix()
|
||||||
|
if prefix is None:
|
||||||
|
print(
|
||||||
|
"[Warning] Neither 'sudo' is available nor running as root. "
|
||||||
|
"Skipping automatic build-dep installation for Debian. "
|
||||||
|
"Please install build dependencies from debian/control manually."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Update package lists first for reliable build-dep resolution.
|
# Update package lists first for reliable build-dep resolution.
|
||||||
run_command("sudo apt-get update", cwd=ctx.repo_dir, preview=ctx.preview)
|
run_command(
|
||||||
|
f"{prefix}apt-get update",
|
||||||
|
cwd=ctx.repo_dir,
|
||||||
|
preview=ctx.preview,
|
||||||
|
)
|
||||||
|
|
||||||
# Install build dependencies based on debian/control in the current tree.
|
# Install build dependencies based on debian/control in the current tree.
|
||||||
# `apt-get build-dep ./` uses the source in the current directory.
|
# `apt-get build-dep ./` uses the source in the current directory.
|
||||||
builddep_cmd = "sudo apt-get build-dep -y ./"
|
builddep_cmd = f"{prefix}apt-get build-dep -y ./"
|
||||||
run_command(builddep_cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
run_command(builddep_cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
||||||
|
|
||||||
def run(self, ctx: RepoContext) -> None:
|
def run(self, ctx: RepoContext) -> None:
|
||||||
@@ -101,7 +139,7 @@ class DebianControlInstaller(BaseInstaller):
|
|||||||
Steps:
|
Steps:
|
||||||
1. apt-get build-dep ./ (automatic build dependency installation)
|
1. apt-get build-dep ./ (automatic build dependency installation)
|
||||||
2. dpkg-buildpackage -b -us -uc
|
2. dpkg-buildpackage -b -us -uc
|
||||||
3. sudo dpkg -i ../*.deb
|
3. sudo dpkg -i ../*.deb (or plain dpkg -i when running as root)
|
||||||
"""
|
"""
|
||||||
control_path = self._control_path(ctx)
|
control_path = self._control_path(ctx)
|
||||||
if not os.path.exists(control_path):
|
if not os.path.exists(control_path):
|
||||||
@@ -123,7 +161,17 @@ class DebianControlInstaller(BaseInstaller):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
prefix = self._privileged_prefix()
|
||||||
|
if prefix is None:
|
||||||
|
print(
|
||||||
|
"[Warning] Neither 'sudo' is available nor running as root. "
|
||||||
|
"Skipping automatic .deb installation. "
|
||||||
|
"You can manually install the following files with dpkg -i:\n "
|
||||||
|
+ "\n ".join(debs)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# 4) Install .deb files
|
# 4) Install .deb files
|
||||||
install_cmd = "sudo dpkg -i " + " ".join(os.path.basename(d) for d in debs)
|
install_cmd = prefix + "dpkg -i " + " ".join(os.path.basename(d) for d in debs)
|
||||||
parent = os.path.dirname(ctx.repo_dir)
|
parent = os.path.dirname(ctx.repo_dir)
|
||||||
run_command(install_cmd, cwd=parent, preview=ctx.preview)
|
run_command(install_cmd, cwd=parent, preview=ctx.preview)
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ Installer for RPM-based packages defined in *.spec files.
|
|||||||
This installer:
|
This installer:
|
||||||
|
|
||||||
1. Installs build dependencies via dnf/yum builddep (where available)
|
1. Installs build dependencies via dnf/yum builddep (where available)
|
||||||
2. Uses rpmbuild to build RPMs from the provided .spec file
|
2. Prepares a source tarball in ~/rpmbuild/SOURCES based on the .spec
|
||||||
3. Installs the resulting RPMs via `rpm -i`
|
3. Uses rpmbuild to build RPMs from the provided .spec file
|
||||||
|
4. Installs the resulting RPMs via the system package manager (dnf/yum)
|
||||||
|
or rpm as a fallback.
|
||||||
|
|
||||||
It targets RPM-based systems (Fedora / RHEL / CentOS / Rocky / Alma, etc.).
|
It targets RPM-based systems (Fedora / RHEL / CentOS / Rocky / Alma, etc.).
|
||||||
"""
|
"""
|
||||||
@@ -16,8 +18,8 @@ It targets RPM-based systems (Fedora / RHEL / CentOS / Rocky / Alma, etc.).
|
|||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import tarfile
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from pkgmgr.actions.repository.install.context import RepoContext
|
from pkgmgr.actions.repository.install.context import RepoContext
|
||||||
from pkgmgr.actions.repository.install.installers.base import BaseInstaller
|
from pkgmgr.actions.repository.install.installers.base import BaseInstaller
|
||||||
@@ -59,6 +61,117 @@ class RpmSpecInstaller(BaseInstaller):
|
|||||||
return None
|
return None
|
||||||
return matches[0]
|
return matches[0]
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Helpers for preparing rpmbuild topdir and source tarball
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _rpmbuild_topdir(self) -> str:
|
||||||
|
"""
|
||||||
|
Return the rpmbuild topdir that rpmbuild will use by default.
|
||||||
|
|
||||||
|
By default this is: ~/rpmbuild
|
||||||
|
|
||||||
|
In the self-install tests, $HOME is set to /tmp/pkgmgr-self-install,
|
||||||
|
so this becomes /tmp/pkgmgr-self-install/rpmbuild which matches the
|
||||||
|
paths in the RPM build logs.
|
||||||
|
"""
|
||||||
|
home = os.path.expanduser("~")
|
||||||
|
return os.path.join(home, "rpmbuild")
|
||||||
|
|
||||||
|
def _ensure_rpmbuild_tree(self, topdir: str) -> None:
|
||||||
|
"""
|
||||||
|
Ensure the standard rpmbuild directory tree exists:
|
||||||
|
|
||||||
|
<topdir>/
|
||||||
|
BUILD/
|
||||||
|
BUILDROOT/
|
||||||
|
RPMS/
|
||||||
|
SOURCES/
|
||||||
|
SPECS/
|
||||||
|
SRPMS/
|
||||||
|
"""
|
||||||
|
for sub in ("BUILD", "BUILDROOT", "RPMS", "SOURCES", "SPECS", "SRPMS"):
|
||||||
|
os.makedirs(os.path.join(topdir, sub), exist_ok=True)
|
||||||
|
|
||||||
|
def _parse_name_version(self, spec_path: str) -> Optional[Tuple[str, str]]:
|
||||||
|
"""
|
||||||
|
Parse Name and Version from the given .spec file.
|
||||||
|
|
||||||
|
Returns (name, version) or None if either cannot be determined.
|
||||||
|
"""
|
||||||
|
name = None
|
||||||
|
version = None
|
||||||
|
|
||||||
|
with open(spec_path, "r", encoding="utf-8") as f:
|
||||||
|
for raw_line in f:
|
||||||
|
line = raw_line.strip()
|
||||||
|
# Ignore comments
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
lower = line.lower()
|
||||||
|
if lower.startswith("name:"):
|
||||||
|
# e.g. "Name: package-manager"
|
||||||
|
parts = line.split(":", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
name = parts[1].strip()
|
||||||
|
elif lower.startswith("version:"):
|
||||||
|
# e.g. "Version: 0.7.7"
|
||||||
|
parts = line.split(":", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
version = parts[1].strip()
|
||||||
|
|
||||||
|
if name and version:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not name or not version:
|
||||||
|
print(
|
||||||
|
"[Warning] Could not determine Name/Version from spec file "
|
||||||
|
f"'{spec_path}'. Skipping RPM source tarball preparation."
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return name, version
|
||||||
|
|
||||||
|
def _prepare_source_tarball(self, ctx: RepoContext, spec_path: str) -> None:
|
||||||
|
"""
|
||||||
|
Prepare a source tarball in <HOME>/rpmbuild/SOURCES that matches
|
||||||
|
the Name/Version in the .spec file.
|
||||||
|
"""
|
||||||
|
parsed = self._parse_name_version(spec_path)
|
||||||
|
if parsed is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
name, version = parsed
|
||||||
|
topdir = self._rpmbuild_topdir()
|
||||||
|
self._ensure_rpmbuild_tree(topdir)
|
||||||
|
|
||||||
|
build_dir = os.path.join(topdir, "BUILD")
|
||||||
|
sources_dir = os.path.join(topdir, "SOURCES")
|
||||||
|
|
||||||
|
source_root = os.path.join(build_dir, f"{name}-{version}")
|
||||||
|
tarball_path = os.path.join(sources_dir, f"{name}-{version}.tar.gz")
|
||||||
|
|
||||||
|
# Clean any previous build directory for this name/version.
|
||||||
|
if os.path.exists(source_root):
|
||||||
|
shutil.rmtree(source_root)
|
||||||
|
|
||||||
|
# Copy the repository tree into BUILD/<name>-<version>.
|
||||||
|
shutil.copytree(ctx.repo_dir, source_root)
|
||||||
|
|
||||||
|
# Create the tarball with the top-level directory <name>-<version>.
|
||||||
|
if os.path.exists(tarball_path):
|
||||||
|
os.remove(tarball_path)
|
||||||
|
|
||||||
|
with tarfile.open(tarball_path, "w:gz") as tar:
|
||||||
|
tar.add(source_root, arcname=f"{name}-{version}")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[INFO] Prepared RPM source tarball at '{tarball_path}' "
|
||||||
|
f"from '{ctx.repo_dir}'."
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def supports(self, ctx: RepoContext) -> bool:
|
def supports(self, ctx: RepoContext) -> bool:
|
||||||
"""
|
"""
|
||||||
This installer is supported if:
|
This installer is supported if:
|
||||||
@@ -77,26 +190,13 @@ class RpmSpecInstaller(BaseInstaller):
|
|||||||
By default, rpmbuild outputs RPMs into:
|
By default, rpmbuild outputs RPMs into:
|
||||||
~/rpmbuild/RPMS/*/*.rpm
|
~/rpmbuild/RPMS/*/*.rpm
|
||||||
"""
|
"""
|
||||||
home = os.path.expanduser("~")
|
topdir = self._rpmbuild_topdir()
|
||||||
pattern = os.path.join(home, "rpmbuild", "RPMS", "**", "*.rpm")
|
pattern = os.path.join(topdir, "RPMS", "**", "*.rpm")
|
||||||
return sorted(glob.glob(pattern, recursive=True))
|
return sorted(glob.glob(pattern, recursive=True))
|
||||||
|
|
||||||
def _install_build_dependencies(self, ctx: RepoContext, spec_path: str) -> None:
|
def _install_build_dependencies(self, ctx: RepoContext, spec_path: str) -> None:
|
||||||
"""
|
"""
|
||||||
Install build dependencies for the given .spec file.
|
Install build dependencies for the given .spec file.
|
||||||
|
|
||||||
Strategy (best-effort):
|
|
||||||
|
|
||||||
1. If dnf is available:
|
|
||||||
sudo dnf builddep -y <spec>
|
|
||||||
2. Else if yum-builddep is available:
|
|
||||||
sudo yum-builddep -y <spec>
|
|
||||||
3. Else if yum is available:
|
|
||||||
sudo yum-builddep -y <spec> # Some systems provide it via yum plugin
|
|
||||||
4. Otherwise: print a warning and skip automatic builddep install.
|
|
||||||
|
|
||||||
Any failure in builddep installation is treated as fatal (SystemExit),
|
|
||||||
consistent with other installer steps.
|
|
||||||
"""
|
"""
|
||||||
spec_basename = os.path.basename(spec_path)
|
spec_basename = os.path.basename(spec_path)
|
||||||
|
|
||||||
@@ -105,7 +205,6 @@ class RpmSpecInstaller(BaseInstaller):
|
|||||||
elif shutil.which("yum-builddep") is not None:
|
elif shutil.which("yum-builddep") is not None:
|
||||||
cmd = f"sudo yum-builddep -y {spec_basename}"
|
cmd = f"sudo yum-builddep -y {spec_basename}"
|
||||||
elif shutil.which("yum") is not None:
|
elif shutil.which("yum") is not None:
|
||||||
# Some distributions ship yum-builddep as a plugin.
|
|
||||||
cmd = f"sudo yum-builddep -y {spec_basename}"
|
cmd = f"sudo yum-builddep -y {spec_basename}"
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
@@ -114,33 +213,17 @@ class RpmSpecInstaller(BaseInstaller):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Run builddep in the repository directory so relative spec paths work.
|
|
||||||
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
||||||
|
|
||||||
def run(self, ctx: RepoContext) -> None:
|
def _install_built_rpms(self, ctx: RepoContext, rpms: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
Build and install RPM-based packages.
|
Install or upgrade the built RPMs.
|
||||||
|
|
||||||
Steps:
|
Strategy:
|
||||||
1. dnf/yum builddep <spec> (automatic build dependency installation)
|
- Prefer dnf install -y <rpms> (handles upgrades cleanly)
|
||||||
2. rpmbuild -ba path/to/spec
|
- Else yum install -y <rpms>
|
||||||
3. sudo rpm -i ~/rpmbuild/RPMS/*/*.rpm
|
- Else fallback to rpm -Uvh <rpms> (upgrade/replace existing)
|
||||||
"""
|
"""
|
||||||
spec_path = self._spec_path(ctx)
|
|
||||||
if not spec_path:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 1) Install build dependencies
|
|
||||||
self._install_build_dependencies(ctx, spec_path)
|
|
||||||
|
|
||||||
# 2) Build RPMs
|
|
||||||
# Use the full spec path, but run in the repo directory.
|
|
||||||
spec_basename = os.path.basename(spec_path)
|
|
||||||
build_cmd = f"rpmbuild -ba {spec_basename}"
|
|
||||||
run_command(build_cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
|
||||||
|
|
||||||
# 3) Find built RPMs
|
|
||||||
rpms = self._find_built_rpms()
|
|
||||||
if not rpms:
|
if not rpms:
|
||||||
print(
|
print(
|
||||||
"[Warning] No RPM files found after rpmbuild. "
|
"[Warning] No RPM files found after rpmbuild. "
|
||||||
@@ -148,13 +231,52 @@ class RpmSpecInstaller(BaseInstaller):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 4) Install RPMs
|
dnf = shutil.which("dnf")
|
||||||
if shutil.which("rpm") is None:
|
yum = shutil.which("yum")
|
||||||
|
rpm = shutil.which("rpm")
|
||||||
|
|
||||||
|
if dnf is not None:
|
||||||
|
install_cmd = "sudo dnf install -y " + " ".join(rpms)
|
||||||
|
elif yum is not None:
|
||||||
|
install_cmd = "sudo yum install -y " + " ".join(rpms)
|
||||||
|
elif rpm is not None:
|
||||||
|
# Fallback: use rpm in upgrade mode so an existing older
|
||||||
|
# version is replaced instead of causing file conflicts.
|
||||||
|
install_cmd = "sudo rpm -Uvh " + " ".join(rpms)
|
||||||
|
else:
|
||||||
print(
|
print(
|
||||||
"[Warning] rpm binary not found on PATH. "
|
"[Warning] No suitable RPM installer (dnf/yum/rpm) found. "
|
||||||
"Cannot install built RPMs."
|
"Cannot install built RPMs."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
install_cmd = "sudo rpm -i " + " ".join(rpms)
|
|
||||||
run_command(install_cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
run_command(install_cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
||||||
|
|
||||||
|
def run(self, ctx: RepoContext) -> None:
|
||||||
|
"""
|
||||||
|
Build and install RPM-based packages.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. Prepare source tarball in ~/rpmbuild/SOURCES matching Name/Version
|
||||||
|
2. dnf/yum builddep <spec> (automatic build dependency installation)
|
||||||
|
3. rpmbuild -ba path/to/spec
|
||||||
|
4. Install built RPMs via dnf/yum (or rpm as fallback)
|
||||||
|
"""
|
||||||
|
spec_path = self._spec_path(ctx)
|
||||||
|
if not spec_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1) Prepare source tarball so rpmbuild finds Source0 in SOURCES.
|
||||||
|
self._prepare_source_tarball(ctx, spec_path)
|
||||||
|
|
||||||
|
# 2) Install build dependencies
|
||||||
|
self._install_build_dependencies(ctx, spec_path)
|
||||||
|
|
||||||
|
# 3) Build RPMs
|
||||||
|
spec_basename = os.path.basename(spec_path)
|
||||||
|
build_cmd = f"rpmbuild -ba {spec_basename}"
|
||||||
|
run_command(build_cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
||||||
|
|
||||||
|
# 4) Find and install built RPMs
|
||||||
|
rpms = self._find_built_rpms()
|
||||||
|
self._install_built_rpms(ctx, rpms)
|
||||||
|
|||||||
@@ -11,15 +11,28 @@ Strategy:
|
|||||||
3. "pip" from PATH as last resort
|
3. "pip" from PATH as last resort
|
||||||
- If pyproject.toml exists: pip install .
|
- If pyproject.toml exists: pip install .
|
||||||
|
|
||||||
All installation failures are treated as fatal errors (SystemExit).
|
All installation failures are treated as fatal errors (SystemExit),
|
||||||
|
except when we explicitly skip the installer:
|
||||||
|
|
||||||
|
- If IN_NIX_SHELL is set, we assume Python is managed by Nix and
|
||||||
|
skip this installer entirely.
|
||||||
|
- If PKGMGR_DISABLE_PYTHON_INSTALLER=1 is set, the installer is
|
||||||
|
globally disabled (useful for CI or debugging).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from pkgmgr.actions.repository.install.installers.base import BaseInstaller
|
from pkgmgr.actions.repository.install.installers.base import BaseInstaller
|
||||||
from pkgmgr.core.command.run import run_command
|
from pkgmgr.core.command.run import run_command
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pkgmgr.actions.repository.install.context import RepoContext
|
||||||
|
from pkgmgr.actions.repository.install import InstallContext
|
||||||
|
|
||||||
|
|
||||||
class PythonInstaller(BaseInstaller):
|
class PythonInstaller(BaseInstaller):
|
||||||
"""Install Python projects and dependencies via pip."""
|
"""Install Python projects and dependencies via pip."""
|
||||||
@@ -27,19 +40,54 @@ class PythonInstaller(BaseInstaller):
|
|||||||
# Logical layer name, used by capability matchers.
|
# Logical layer name, used by capability matchers.
|
||||||
layer = "python"
|
layer = "python"
|
||||||
|
|
||||||
def supports(self, ctx) -> bool:
|
def _in_nix_shell(self) -> bool:
|
||||||
|
"""
|
||||||
|
Return True if we appear to be running inside a Nix dev shell.
|
||||||
|
|
||||||
|
Nix sets IN_NIX_SHELL in `nix develop` environments. In that case
|
||||||
|
the Python environment is already provided by Nix, so we must not
|
||||||
|
attempt an additional pip-based installation.
|
||||||
|
"""
|
||||||
|
return bool(os.environ.get("IN_NIX_SHELL"))
|
||||||
|
|
||||||
|
def supports(self, ctx: "RepoContext") -> bool:
|
||||||
"""
|
"""
|
||||||
Return True if this installer should handle the given repository.
|
Return True if this installer should handle the given repository.
|
||||||
|
|
||||||
Only pyproject.toml is supported as the single source of truth
|
Only pyproject.toml is supported as the single source of truth
|
||||||
for Python dependencies and packaging metadata.
|
for Python dependencies and packaging metadata.
|
||||||
|
|
||||||
|
The installer is *disabled* when:
|
||||||
|
- IN_NIX_SHELL is set (Python managed by Nix dev shell), or
|
||||||
|
- PKGMGR_DISABLE_PYTHON_INSTALLER=1 is set.
|
||||||
"""
|
"""
|
||||||
|
# 1) Skip in Nix dev shells – Python is managed by the flake/devShell.
|
||||||
|
if self._in_nix_shell():
|
||||||
|
print(
|
||||||
|
"[INFO] IN_NIX_SHELL detected; skipping PythonInstaller. "
|
||||||
|
"Python runtime is provided by the Nix dev shell."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 2) Optional global kill-switch.
|
||||||
|
if os.environ.get("PKGMGR_DISABLE_PYTHON_INSTALLER") == "1":
|
||||||
|
print(
|
||||||
|
"[INFO] PKGMGR_DISABLE_PYTHON_INSTALLER=1 – "
|
||||||
|
"PythonInstaller is disabled."
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
repo_dir = ctx.repo_dir
|
repo_dir = ctx.repo_dir
|
||||||
return os.path.exists(os.path.join(repo_dir, "pyproject.toml"))
|
return os.path.exists(os.path.join(repo_dir, "pyproject.toml"))
|
||||||
|
|
||||||
def _pip_cmd(self) -> str:
|
def _pip_cmd(self) -> str:
|
||||||
"""
|
"""
|
||||||
Resolve the pip command to use.
|
Resolve the pip command to use.
|
||||||
|
|
||||||
|
Order:
|
||||||
|
1) PKGMGR_PIP (explicit override)
|
||||||
|
2) sys.executable -m pip
|
||||||
|
3) plain "pip"
|
||||||
"""
|
"""
|
||||||
explicit = os.environ.get("PKGMGR_PIP", "").strip()
|
explicit = os.environ.get("PKGMGR_PIP", "").strip()
|
||||||
if explicit:
|
if explicit:
|
||||||
@@ -50,12 +98,23 @@ class PythonInstaller(BaseInstaller):
|
|||||||
|
|
||||||
return "pip"
|
return "pip"
|
||||||
|
|
||||||
def run(self, ctx) -> None:
|
def run(self, ctx: "InstallContext") -> None:
|
||||||
"""
|
"""
|
||||||
Install Python project defined via pyproject.toml.
|
Install Python project defined via pyproject.toml.
|
||||||
|
|
||||||
Any pip failure is propagated as SystemExit.
|
Any pip failure is propagated as SystemExit.
|
||||||
"""
|
"""
|
||||||
|
# Extra guard in case run() is called directly without supports().
|
||||||
|
if self._in_nix_shell():
|
||||||
|
print(
|
||||||
|
"[INFO] IN_NIX_SHELL detected in PythonInstaller.run(); "
|
||||||
|
"skipping pip-based installation."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.supports(ctx): # type: ignore[arg-type]
|
||||||
|
return
|
||||||
|
|
||||||
pip_cmd = self._pip_cmd()
|
pip_cmd = self._pip_cmd()
|
||||||
|
|
||||||
pyproject = os.path.join(ctx.repo_dir, "pyproject.toml")
|
pyproject = os.path.join(ctx.repo_dir, "pyproject.toml")
|
||||||
|
|||||||
@@ -1,35 +1,57 @@
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pkgmgr.core.repository.identifier import get_repo_identifier
|
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||||
from pkgmgr.core.repository.dir import get_repo_dir
|
from pkgmgr.core.repository.dir import get_repo_dir
|
||||||
from pkgmgr.core.repository.verify import verify_repository
|
from pkgmgr.core.repository.verify import verify_repository
|
||||||
|
|
||||||
|
|
||||||
def pull_with_verification(
|
def pull_with_verification(
|
||||||
selected_repos,
|
selected_repos,
|
||||||
repositories_base_dir,
|
repositories_base_dir,
|
||||||
all_repos,
|
all_repos,
|
||||||
extra_args,
|
extra_args,
|
||||||
no_verification,
|
no_verification,
|
||||||
preview:bool):
|
preview: bool,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Executes "git pull" for each repository with verification.
|
Execute `git pull` for each repository with verification.
|
||||||
|
|
||||||
Uses the verify_repository function in "pull" mode.
|
- Uses verify_repository() in "pull" mode.
|
||||||
If verification fails (and verification info is set) and --no-verification is not enabled,
|
- If verification fails (and verification info is set) and
|
||||||
the user is prompted to confirm the pull.
|
--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:
|
for repo in selected_repos:
|
||||||
repo_identifier = get_repo_identifier(repo, all_repos)
|
repo_identifier = get_repo_identifier(repo, all_repos)
|
||||||
repo_dir = get_repo_dir(repositories_base_dir, repo)
|
repo_dir = get_repo_dir(repositories_base_dir, repo)
|
||||||
|
|
||||||
if not os.path.exists(repo_dir):
|
if not os.path.exists(repo_dir):
|
||||||
print(f"Repository directory '{repo_dir}' not found for {repo_identifier}.")
|
print(f"Repository directory '{repo_dir}' not found for {repo_identifier}.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
verified_info = repo.get("verified")
|
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}:")
|
print(f"Warning: Verification failed for {repo_identifier}:")
|
||||||
for err in errors:
|
for err in errors:
|
||||||
print(f" - {err}")
|
print(f" - {err}")
|
||||||
@@ -37,12 +59,19 @@ def pull_with_verification(
|
|||||||
if choice != "y":
|
if choice != "y":
|
||||||
continue
|
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:
|
if preview:
|
||||||
|
# Preview mode: only show the command, do not execute or prompt.
|
||||||
print(f"[Preview] In '{repo_dir}': {full_cmd}")
|
print(f"[Preview] In '{repo_dir}': {full_cmd}")
|
||||||
else:
|
else:
|
||||||
print(f"Running in '{repo_dir}': {full_cmd}")
|
print(f"Running in '{repo_dir}': {full_cmd}")
|
||||||
result = subprocess.run(full_cmd, cwd=repo_dir, shell=True)
|
result = subprocess.run(full_cmd, cwd=repo_dir, shell=True)
|
||||||
if result.returncode != 0:
|
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)
|
sys.exit(result.returncode)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import re
|
|||||||
from typing import Any, Dict, List, Sequence
|
from typing import Any, Dict, List, Sequence
|
||||||
|
|
||||||
from pkgmgr.core.repository.resolve import resolve_repos
|
from pkgmgr.core.repository.resolve import resolve_repos
|
||||||
|
from pkgmgr.core.repository.ignored import filter_ignored
|
||||||
|
|
||||||
Repository = Dict[str, Any]
|
Repository = Dict[str, Any]
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ def _apply_filters(
|
|||||||
if not _match_pattern(ident_str, string_pattern):
|
if not _match_pattern(ident_str, string_pattern):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Category filter: nur echte Kategorien, KEINE Tags
|
# Category filter: only real categories, NOT tags
|
||||||
if category_patterns:
|
if category_patterns:
|
||||||
cats: List[str] = []
|
cats: List[str] = []
|
||||||
cats.extend(map(str, repo.get("category_files", [])))
|
cats.extend(map(str, repo.get("category_files", [])))
|
||||||
@@ -106,7 +107,7 @@ def _apply_filters(
|
|||||||
if not ok:
|
if not ok:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Tag filter: ausschließlich YAML-Tags
|
# Tag filter: YAML tags only
|
||||||
if tag_patterns:
|
if tag_patterns:
|
||||||
tags: List[str] = list(map(str, repo.get("tags", [])))
|
tags: List[str] = list(map(str, repo.get("tags", [])))
|
||||||
if not tags:
|
if not tags:
|
||||||
@@ -124,16 +125,38 @@ def _apply_filters(
|
|||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_filter_ignored(args, repos: List[Repository]) -> List[Repository]:
|
||||||
|
"""
|
||||||
|
Apply ignore filtering unless the caller explicitly opted to include ignored
|
||||||
|
repositories (via args.include_ignored).
|
||||||
|
|
||||||
|
Note: this helper is used only for *implicit* selections (all / filters /
|
||||||
|
by-directory). For *explicit* identifiers we do NOT filter ignored repos,
|
||||||
|
so the user can still target them directly if desired.
|
||||||
|
"""
|
||||||
|
include_ignored: bool = bool(getattr(args, "include_ignored", False))
|
||||||
|
if include_ignored:
|
||||||
|
return repos
|
||||||
|
return filter_ignored(repos)
|
||||||
|
|
||||||
|
|
||||||
def get_selected_repos(args, all_repositories: List[Repository]) -> List[Repository]:
|
def get_selected_repos(args, all_repositories: List[Repository]) -> List[Repository]:
|
||||||
"""
|
"""
|
||||||
Compute the list of repositories selected by CLI arguments.
|
Compute the list of repositories selected by CLI arguments.
|
||||||
|
|
||||||
Modes:
|
Modes:
|
||||||
- If identifiers are given: select via resolve_repos() from all_repositories.
|
- If identifiers are given: select via resolve_repos() from all_repositories.
|
||||||
- Else if any of --category/--string/--tag is used: start from all_repositories
|
Ignored repositories are *not* filtered here, so explicit identifiers
|
||||||
and apply filters.
|
always win.
|
||||||
- Else if --all is set: select all_repositories.
|
- Else if any of --category/--string/--tag is used: start from
|
||||||
- Else: try to select the repository of the current working directory.
|
all_repositories, apply filters and then drop ignored repos.
|
||||||
|
- Else if --all is set: select all_repositories and then drop ignored repos.
|
||||||
|
- Else: try to select the repository of the current working directory
|
||||||
|
and then drop it if it is ignored.
|
||||||
|
|
||||||
|
The ignore filter can be bypassed by setting args.include_ignored = True
|
||||||
|
(e.g. via a CLI flag --include-ignored).
|
||||||
"""
|
"""
|
||||||
identifiers: List[str] = getattr(args, "identifiers", []) or []
|
identifiers: List[str] = getattr(args, "identifiers", []) or []
|
||||||
use_all: bool = bool(getattr(args, "all", False))
|
use_all: bool = bool(getattr(args, "all", False))
|
||||||
@@ -143,18 +166,25 @@ def get_selected_repos(args, all_repositories: List[Repository]) -> List[Reposit
|
|||||||
|
|
||||||
has_filters = bool(category_patterns or string_pattern or tag_patterns)
|
has_filters = bool(category_patterns or string_pattern or tag_patterns)
|
||||||
|
|
||||||
# 1) Explicit identifiers win
|
# 1) Explicit identifiers win and bypass ignore filtering
|
||||||
if identifiers:
|
if identifiers:
|
||||||
base = resolve_repos(identifiers, all_repositories)
|
base = resolve_repos(identifiers, all_repositories)
|
||||||
return _apply_filters(base, string_pattern, category_patterns, tag_patterns)
|
return _apply_filters(base, string_pattern, category_patterns, tag_patterns)
|
||||||
|
|
||||||
# 2) Filter-only mode: start from all repositories
|
# 2) Filter-only mode: start from all repositories
|
||||||
if has_filters:
|
if has_filters:
|
||||||
return _apply_filters(list(all_repositories), string_pattern, category_patterns, tag_patterns)
|
base = _apply_filters(
|
||||||
|
list(all_repositories),
|
||||||
|
string_pattern,
|
||||||
|
category_patterns,
|
||||||
|
tag_patterns,
|
||||||
|
)
|
||||||
|
return _maybe_filter_ignored(args, base)
|
||||||
|
|
||||||
# 3) --all (no filters): all repos
|
# 3) --all (no filters): all repos
|
||||||
if use_all:
|
if use_all:
|
||||||
return list(all_repositories)
|
base = list(all_repositories)
|
||||||
|
return _maybe_filter_ignored(args, base)
|
||||||
|
|
||||||
# 4) Fallback: try to select repository of current working directory
|
# 4) Fallback: try to select repository of current working directory
|
||||||
cwd = os.path.abspath(os.getcwd())
|
cwd = os.path.abspath(os.getcwd())
|
||||||
@@ -164,7 +194,7 @@ def get_selected_repos(args, all_repositories: List[Repository]) -> List[Reposit
|
|||||||
if os.path.abspath(str(repo.get("directory", ""))) == cwd
|
if os.path.abspath(str(repo.get("directory", ""))) == cwd
|
||||||
]
|
]
|
||||||
if by_dir:
|
if by_dir:
|
||||||
return by_dir
|
return _maybe_filter_ignored(args, by_dir)
|
||||||
|
|
||||||
# No specific match -> empty list
|
# No specific match -> empty list
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "package-manager"
|
name = "package-manager"
|
||||||
version = "0.7.2"
|
version = "0.7.10"
|
||||||
description = "Kevin's package-manager tool (pkgmgr)"
|
description = "Kevin's package-manager tool (pkgmgr)"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
@@ -2,28 +2,59 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Ensure Nix has access to a valid CA bundle (TLS trust store)
|
# Detect and export a valid CA bundle so Nix, Git, curl and Python tooling
|
||||||
|
# can successfully perform HTTPS requests on all distros (Debian, Ubuntu,
|
||||||
|
# Fedora, RHEL, CentOS, etc.)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if [[ -z "${NIX_SSL_CERT_FILE:-}" ]]; then
|
detect_ca_bundle() {
|
||||||
if [[ -f /etc/ssl/certs/ca-certificates.crt ]]; then
|
# Common CA bundle locations across major Linux distributions
|
||||||
# Debian/Ubuntu-style path
|
local candidates=(
|
||||||
export NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
/etc/ssl/certs/ca-certificates.crt # Debian/Ubuntu
|
||||||
echo "[docker] Using CA bundle: ${NIX_SSL_CERT_FILE}"
|
/etc/ssl/cert.pem # Some distros
|
||||||
elif [[ -f /etc/pki/tls/certs/ca-bundle.crt ]]; then
|
/etc/pki/tls/certs/ca-bundle.crt # Fedora/RHEL/CentOS
|
||||||
# Fedora/RHEL/CentOS-style path
|
/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem # CentOS/RHEL extracted bundle
|
||||||
export NIX_SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt
|
/etc/ssl/ca-bundle.pem # Generic fallback
|
||||||
echo "[docker] Using CA bundle: ${NIX_SSL_CERT_FILE}"
|
)
|
||||||
else
|
|
||||||
echo "[docker] WARNING: No CA bundle found for Nix (NIX_SSL_CERT_FILE not set)."
|
for path in "${candidates[@]}"; do
|
||||||
echo "[docker] HTTPS access for Nix flakes may fail."
|
if [[ -f "$path" ]]; then
|
||||||
fi
|
echo "$path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use existing NIX_SSL_CERT_FILE if provided, otherwise auto-detect
|
||||||
|
CA_BUNDLE="${NIX_SSL_CERT_FILE:-}"
|
||||||
|
|
||||||
|
if [[ -z "${CA_BUNDLE}" ]]; then
|
||||||
|
CA_BUNDLE="$(detect_ca_bundle || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${CA_BUNDLE}" ]]; then
|
||||||
|
# Export for Nix (critical)
|
||||||
|
export NIX_SSL_CERT_FILE="${CA_BUNDLE}"
|
||||||
|
|
||||||
|
# Export for Git, Python requests, curl, etc.
|
||||||
|
export SSL_CERT_FILE="${CA_BUNDLE}"
|
||||||
|
export REQUESTS_CA_BUNDLE="${CA_BUNDLE}"
|
||||||
|
export GIT_SSL_CAINFO="${CA_BUNDLE}"
|
||||||
|
|
||||||
|
echo "[docker] Using CA bundle: ${CA_BUNDLE}"
|
||||||
|
else
|
||||||
|
echo "[docker] WARNING: No CA certificate bundle found."
|
||||||
|
echo "[docker] HTTPS access for Nix flakes and other tools may fail."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
echo "[docker] Starting package-manager container"
|
echo "[docker] Starting package-manager container"
|
||||||
|
|
||||||
# Distro info for logging
|
# ---------------------------------------------------------------------------
|
||||||
|
# Log distribution info
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
if [[ -f /etc/os-release ]]; then
|
if [[ -f /etc/os-release ]]; then
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
@@ -34,9 +65,9 @@ fi
|
|||||||
echo "[docker] Using /src as working directory"
|
echo "[docker] Using /src as working directory"
|
||||||
cd /src
|
cd /src
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# DEV mode: build/install package-manager from current /src
|
# DEV mode: rebuild package-manager from the mounted /src tree
|
||||||
# ------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if [[ "${PKGMGR_DEV:-0}" == "1" ]]; then
|
if [[ "${PKGMGR_DEV:-0}" == "1" ]]; then
|
||||||
echo "[docker] DEV mode enabled (PKGMGR_DEV=1)"
|
echo "[docker] DEV mode enabled (PKGMGR_DEV=1)"
|
||||||
echo "[docker] Rebuilding package-manager from /src via scripts/installation/run-package.sh..."
|
echo "[docker] Rebuilding package-manager from /src via scripts/installation/run-package.sh..."
|
||||||
@@ -49,9 +80,9 @@ if [[ "${PKGMGR_DEV:-0}" == "1" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Hand-off to pkgmgr / arbitrary command
|
# Hand off to pkgmgr or arbitrary command
|
||||||
# ------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if [[ $# -eq 0 ]]; then
|
if [[ $# -eq 0 ]]; then
|
||||||
echo "[docker] No arguments provided. Showing pkgmgr help..."
|
echo "[docker] No arguments provided. Showing pkgmgr help..."
|
||||||
exec pkgmgr --help
|
exec pkgmgr --help
|
||||||
|
|||||||
@@ -97,11 +97,32 @@ if [[ "${IN_CONTAINER}" -eq 1 && "${EUID:-0}" -eq 0 ]]; then
|
|||||||
useradd -m -r -g nixbld -s /usr/bin/bash nix
|
useradd -m -r -g nixbld -s /usr/bin/bash nix
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create /nix directory and hand it to nix user (prevents installer sudo prompt)
|
# Ensure /nix exists and is writable by the "nix" user.
|
||||||
|
#
|
||||||
|
# In some base images (or previous runs), /nix may already exist and be
|
||||||
|
# owned by root. In that case the Nix single-user installer will abort with:
|
||||||
|
#
|
||||||
|
# "directory /nix exists, but is not writable by you"
|
||||||
|
#
|
||||||
|
# To keep container runs idempotent and robust, we always enforce
|
||||||
|
# ownership nix:nixbld here.
|
||||||
if [[ ! -d /nix ]]; then
|
if [[ ! -d /nix ]]; then
|
||||||
echo "[init-nix] Creating /nix with owner nix:nixbld..."
|
echo "[init-nix] Creating /nix with owner nix:nixbld..."
|
||||||
mkdir -m 0755 /nix
|
mkdir -m 0755 /nix
|
||||||
chown nix:nixbld /nix
|
chown nix:nixbld /nix
|
||||||
|
else
|
||||||
|
current_owner="$(stat -c '%U' /nix 2>/dev/null || echo '?')"
|
||||||
|
current_group="$(stat -c '%G' /nix 2>/dev/null || echo '?')"
|
||||||
|
if [[ "${current_owner}" != "nix" || "${current_group}" != "nixbld" ]]; then
|
||||||
|
echo "[init-nix] /nix already exists with owner ${current_owner}:${current_group} – fixing to nix:nixbld..."
|
||||||
|
chown -R nix:nixbld /nix
|
||||||
|
else
|
||||||
|
echo "[init-nix] /nix already exists with correct owner nix:nixbld."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -w /nix ]]; then
|
||||||
|
echo "[init-nix] WARNING: /nix is still not writable after chown; Nix installer may fail."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run Nix single-user installer as "nix"
|
# Run Nix single-user installer as "nix"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ dnf -y install \
|
|||||||
bash \
|
bash \
|
||||||
curl-minimal \
|
curl-minimal \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
sudo \
|
||||||
xz
|
xz
|
||||||
|
|
||||||
dnf clean all
|
dnf clean all
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ for distro in $DISTROS; do
|
|||||||
# Run the command and capture the output
|
# Run the command and capture the output
|
||||||
if OUTPUT=$(docker run --rm \
|
if OUTPUT=$(docker run --rm \
|
||||||
-e PKGMGR_DEV=1 \
|
-e PKGMGR_DEV=1 \
|
||||||
|
-v pkgmgr_nix_store:/nix \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/src" \
|
||||||
-v "pkgmgr_nix_cache:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache:/root/.cache/nix" \
|
||||||
"$IMAGE" 2>&1); then
|
"$IMAGE" 2>&1); then
|
||||||
|
|||||||
@@ -8,16 +8,12 @@ for distro in $DISTROS; do
|
|||||||
echo ">>> Running E2E tests: $distro"
|
echo ">>> Running E2E tests: $distro"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
MOUNT_NIX=""
|
|
||||||
if [[ "$distro" == "arch" ]]; then
|
|
||||||
MOUNT_NIX="-v pkgmgr_nix_store:/nix"
|
|
||||||
fi
|
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/src" \
|
||||||
$MOUNT_NIX \
|
-v pkgmgr_nix_store:/nix \
|
||||||
-v "pkgmgr_nix_cache:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache:/root/.cache/nix" \
|
||||||
-e PKGMGR_DEV=1 \
|
-e PKGMGR_DEV=1 \
|
||||||
|
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||||
--workdir /src \
|
--workdir /src \
|
||||||
--entrypoint bash \
|
--entrypoint bash \
|
||||||
"package-manager-test-$distro" \
|
"package-manager-test-$distro" \
|
||||||
@@ -51,6 +47,6 @@ for distro in $DISTROS; do
|
|||||||
nix develop .#default --no-write-lock-file -c \
|
nix develop .#default --no-write-lock-file -c \
|
||||||
python3 -m unittest discover \
|
python3 -m unittest discover \
|
||||||
-s /src/tests/e2e \
|
-s /src/tests/e2e \
|
||||||
-p "test_*.py";
|
-p "$TEST_PATTERN";
|
||||||
'
|
'
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ echo "============================================================"
|
|||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/src" \
|
||||||
|
-v pkgmgr_nix_store:/nix \
|
||||||
-v "pkgmgr_nix_cache:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache:/root/.cache/nix" \
|
||||||
--workdir /src \
|
--workdir /src \
|
||||||
-e PKGMGR_DEV=1 \
|
-e PKGMGR_DEV=1 \
|
||||||
|
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||||
--entrypoint bash \
|
--entrypoint bash \
|
||||||
"package-manager-test-arch" \
|
"package-manager-test-arch" \
|
||||||
-c '
|
-c '
|
||||||
@@ -19,5 +21,5 @@ docker run --rm \
|
|||||||
python -m unittest discover \
|
python -m unittest discover \
|
||||||
-s tests/integration \
|
-s tests/integration \
|
||||||
-t /src \
|
-t /src \
|
||||||
-p "test_*.py";
|
-p "$TEST_PATTERN";
|
||||||
'
|
'
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ echo "============================================================"
|
|||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/src" \
|
||||||
-v "pkgmgr_nix_cache:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache:/root/.cache/nix" \
|
||||||
|
-v pkgmgr_nix_store:/nix \
|
||||||
--workdir /src \
|
--workdir /src \
|
||||||
-e PKGMGR_DEV=1 \
|
-e PKGMGR_DEV=1 \
|
||||||
|
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||||
--entrypoint bash \
|
--entrypoint bash \
|
||||||
"package-manager-test-arch" \
|
"package-manager-test-arch" \
|
||||||
-c '
|
-c '
|
||||||
@@ -19,5 +21,5 @@ docker run --rm \
|
|||||||
python -m unittest discover \
|
python -m unittest discover \
|
||||||
-s tests/unit \
|
-s tests/unit \
|
||||||
-t /src \
|
-t /src \
|
||||||
-p "test_*.py";
|
-p "$TEST_PATTERN";
|
||||||
'
|
'
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ def remove_pkgmgr_from_nix_profile() -> None:
|
|||||||
prints a descriptive format without an index column inside the container.
|
prints a descriptive format without an index column inside the container.
|
||||||
|
|
||||||
Instead, we directly try to remove possible names:
|
Instead, we directly try to remove possible names:
|
||||||
- 'pkgmgr' (the actual name shown in `nix profile list`)
|
- 'pkgmgr'
|
||||||
- 'package-manager' (the name mentioned in Nix's own error hints)
|
- 'package-manager'
|
||||||
"""
|
"""
|
||||||
for spec in ("pkgmgr", "package-manager"):
|
for spec in ("pkgmgr", "package-manager"):
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
@@ -45,18 +45,34 @@ def remove_pkgmgr_from_nix_profile() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def configure_git_safe_directory() -> None:
|
||||||
|
"""
|
||||||
|
Configure Git to treat /src as a safe directory.
|
||||||
|
|
||||||
|
Needed because /src is a bind-mounted repository in CI, often owned by a
|
||||||
|
different UID. Modern Git aborts with:
|
||||||
|
'fatal: detected dubious ownership in repository at /src/.git'
|
||||||
|
|
||||||
|
This fix applies ONLY inside this test container.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["git", "config", "--global", "--add", "safe.directory", "/src"],
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("[WARN] git not found – skipping safe.directory configuration")
|
||||||
|
|
||||||
|
|
||||||
def pkgmgr_help_debug() -> None:
|
def pkgmgr_help_debug() -> None:
|
||||||
"""
|
"""
|
||||||
Run `pkgmgr --help` after installation *inside an interactive bash shell*,
|
Run `pkgmgr --help` after installation *inside an interactive bash shell*,
|
||||||
print its output and return code, but never fail the test.
|
print its output and return code, but never fail the test.
|
||||||
|
|
||||||
Reason:
|
This ensures the installer’s shell RC changes are actually loaded.
|
||||||
- The installer adds venv/alias setup into shell rc files (~/.bashrc, ~/.zshrc)
|
|
||||||
- Those changes are only applied in a new interactive shell session.
|
|
||||||
"""
|
"""
|
||||||
print("\n--- PKGMGR HELP (after installation, via bash -i) ---")
|
print("\n--- PKGMGR HELP (after installation, via bash -i) ---")
|
||||||
|
|
||||||
# Simulate a fresh interactive bash, so ~/.bashrc gets sourced
|
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
["bash", "-i", "-c", "pkgmgr --help"],
|
["bash", "-i", "-c", "pkgmgr --help"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
@@ -76,29 +92,43 @@ def pkgmgr_help_debug() -> None:
|
|||||||
print(f"returncode: {proc.returncode}")
|
print(f"returncode: {proc.returncode}")
|
||||||
print("--- END ---\n")
|
print("--- END ---\n")
|
||||||
|
|
||||||
if proc.returncode != 0:
|
|
||||||
raise AssertionError(f"'pkgmgr --help' failed with exit code {proc.returncode}")
|
|
||||||
|
|
||||||
# Wichtig: Hier KEIN AssertionError mehr – das ist reine Debug-Ausgabe.
|
|
||||||
# Falls du später hart testen willst, kannst du optional:
|
|
||||||
# if proc.returncode != 0:
|
|
||||||
# self.fail("...")
|
|
||||||
# aber aktuell nur Sichtprüfung.
|
|
||||||
|
|
||||||
|
|
||||||
class TestIntegrationInstalPKGMGRShallow(unittest.TestCase):
|
class TestIntegrationInstalPKGMGRShallow(unittest.TestCase):
|
||||||
def test_install_pkgmgr_self_install(self) -> None:
|
def test_install_pkgmgr_self_install(self) -> None:
|
||||||
# Debug before cleanup
|
"""
|
||||||
nix_profile_list_debug("BEFORE CLEANUP")
|
End-to-end test that runs "python main.py install pkgmgr ..." inside
|
||||||
|
the test container.
|
||||||
|
|
||||||
# Cleanup: aggressively try to drop any pkgmgr/profile entries
|
HOME is isolated to avoid permission problems with Nix & repositories.
|
||||||
remove_pkgmgr_from_nix_profile()
|
"""
|
||||||
|
temp_home = "/tmp/pkgmgr-self-install"
|
||||||
# Debug after cleanup
|
os.makedirs(temp_home, exist_ok=True)
|
||||||
nix_profile_list_debug("AFTER CLEANUP")
|
|
||||||
|
|
||||||
original_argv = sys.argv
|
original_argv = sys.argv
|
||||||
|
original_environ = os.environ.copy()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Isolate HOME so that ~ expands to /tmp/pkgmgr-self-install
|
||||||
|
os.environ["HOME"] = temp_home
|
||||||
|
|
||||||
|
# Optional XDG override for a fully isolated environment
|
||||||
|
os.environ.setdefault("XDG_CONFIG_HOME", os.path.join(temp_home, ".config"))
|
||||||
|
os.environ.setdefault("XDG_CACHE_HOME", os.path.join(temp_home, ".cache"))
|
||||||
|
os.environ.setdefault("XDG_DATA_HOME", os.path.join(temp_home, ".local", "share"))
|
||||||
|
|
||||||
|
# 🔧 IMPORTANT FIX: allow Git to access /src safely
|
||||||
|
configure_git_safe_directory()
|
||||||
|
|
||||||
|
# Debug before cleanup
|
||||||
|
nix_profile_list_debug("BEFORE CLEANUP")
|
||||||
|
|
||||||
|
# Cleanup: drop any pkgmgr entries from nix profile
|
||||||
|
remove_pkgmgr_from_nix_profile()
|
||||||
|
|
||||||
|
# Debug after cleanup
|
||||||
|
nix_profile_list_debug("AFTER CLEANUP")
|
||||||
|
|
||||||
|
# Prepare argv for module execution
|
||||||
sys.argv = [
|
sys.argv = [
|
||||||
"python",
|
"python",
|
||||||
"install",
|
"install",
|
||||||
@@ -107,13 +137,18 @@ class TestIntegrationInstalPKGMGRShallow(unittest.TestCase):
|
|||||||
"shallow",
|
"shallow",
|
||||||
"--no-verification",
|
"--no-verification",
|
||||||
]
|
]
|
||||||
# Führt die Installation via main.py aus
|
|
||||||
|
# Execute installation via main.py
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("main", run_name="__main__")
|
||||||
|
|
||||||
# Nach erfolgreicher Installation: pkgmgr --help anzeigen (Debug)
|
# Debug: interactive shell test
|
||||||
pkgmgr_help_debug()
|
pkgmgr_help_debug()
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
# Restore system state
|
||||||
sys.argv = original_argv
|
sys.argv = original_argv
|
||||||
|
os.environ.clear()
|
||||||
|
os.environ.update(original_environ)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -80,6 +80,21 @@ class TestUpdatePyprojectVersion(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertNotEqual(cm.exception.code, 0)
|
self.assertNotEqual(cm.exception.code, 0)
|
||||||
|
|
||||||
|
def test_update_pyproject_version_missing_file_is_skipped(self) -> None:
|
||||||
|
"""
|
||||||
|
If pyproject.toml does not exist, the function must not raise
|
||||||
|
and must not create the file. It should simply be skipped.
|
||||||
|
"""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
path = os.path.join(tmpdir, "pyproject.toml")
|
||||||
|
self.assertFalse(os.path.exists(path))
|
||||||
|
|
||||||
|
# Must not raise an exception
|
||||||
|
update_pyproject_version(path, "1.2.3", preview=False)
|
||||||
|
|
||||||
|
# Still no file created
|
||||||
|
self.assertFalse(os.path.exists(path))
|
||||||
|
|
||||||
|
|
||||||
class TestUpdateFlakeVersion(unittest.TestCase):
|
class TestUpdateFlakeVersion(unittest.TestCase):
|
||||||
def test_update_flake_version_normal(self) -> None:
|
def test_update_flake_version_normal(self) -> None:
|
||||||
@@ -352,11 +367,11 @@ class TestUpdateSpecChangelog(unittest.TestCase):
|
|||||||
with open(path, "r", encoding="utf-8") as f:
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# Neue Stanza muss nach %changelog stehen
|
# New stanza must appear after the %changelog marker
|
||||||
self.assertIn("%changelog", content)
|
self.assertIn("%changelog", content)
|
||||||
self.assertIn("Fedora changelog entry", content)
|
self.assertIn("Fedora changelog entry", content)
|
||||||
self.assertIn("Test Maintainer <test@example.com>", content)
|
self.assertIn("Test Maintainer <test@example.com>", content)
|
||||||
# Alte Einträge müssen erhalten bleiben
|
# Old entries must still be present
|
||||||
self.assertIn("Old Maintainer <old@example.com>", content)
|
self.assertIn("Old Maintainer <old@example.com>", content)
|
||||||
|
|
||||||
def test_update_spec_changelog_preview_does_not_write(self) -> None:
|
def test_update_spec_changelog_preview_does_not_write(self) -> None:
|
||||||
@@ -396,7 +411,7 @@ class TestUpdateSpecChangelog(unittest.TestCase):
|
|||||||
with open(path, "r", encoding="utf-8") as f:
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# Im Preview-Modus darf nichts verändert werden
|
# In preview mode, the spec file must not change
|
||||||
self.assertEqual(content, original)
|
self.assertEqual(content, original)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
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()
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from types import SimpleNamespace
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from pkgmgr.actions.branch import open_branch
|
from pkgmgr.actions.branch import open_branch
|
||||||
@@ -13,9 +12,10 @@ class TestOpenBranch(unittest.TestCase):
|
|||||||
def test_open_branch_with_explicit_name_and_default_base(self, mock_run_git) -> None:
|
def test_open_branch_with_explicit_name_and_default_base(self, mock_run_git) -> None:
|
||||||
"""
|
"""
|
||||||
open_branch(name, base='main') should:
|
open_branch(name, base='main') should:
|
||||||
|
- resolve base branch (prefers 'main', falls back to 'master')
|
||||||
- fetch origin
|
- fetch origin
|
||||||
- checkout base
|
- checkout resolved base
|
||||||
- pull base
|
- pull resolved base
|
||||||
- create new branch
|
- create new branch
|
||||||
- push with upstream
|
- push with upstream
|
||||||
"""
|
"""
|
||||||
@@ -25,6 +25,7 @@ class TestOpenBranch(unittest.TestCase):
|
|||||||
|
|
||||||
# We expect a specific sequence of Git calls.
|
# We expect a specific sequence of Git calls.
|
||||||
expected_calls = [
|
expected_calls = [
|
||||||
|
(["rev-parse", "--verify", "main"], "/repo"),
|
||||||
(["fetch", "origin"], "/repo"),
|
(["fetch", "origin"], "/repo"),
|
||||||
(["checkout", "main"], "/repo"),
|
(["checkout", "main"], "/repo"),
|
||||||
(["pull", "origin", "main"], "/repo"),
|
(["pull", "origin", "main"], "/repo"),
|
||||||
@@ -50,7 +51,7 @@ class TestOpenBranch(unittest.TestCase):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
If name is None/empty, open_branch should prompt via input()
|
If name is None/empty, open_branch should prompt via input()
|
||||||
and still perform the full Git sequence.
|
and still perform the full Git sequence on the resolved base.
|
||||||
"""
|
"""
|
||||||
mock_run_git.return_value = ""
|
mock_run_git.return_value = ""
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ class TestOpenBranch(unittest.TestCase):
|
|||||||
mock_input.assert_called_once()
|
mock_input.assert_called_once()
|
||||||
|
|
||||||
expected_calls = [
|
expected_calls = [
|
||||||
|
(["rev-parse", "--verify", "develop"], "/repo"),
|
||||||
(["fetch", "origin"], "/repo"),
|
(["fetch", "origin"], "/repo"),
|
||||||
(["checkout", "develop"], "/repo"),
|
(["checkout", "develop"], "/repo"),
|
||||||
(["pull", "origin", "develop"], "/repo"),
|
(["pull", "origin", "develop"], "/repo"),
|
||||||
@@ -76,15 +78,20 @@ class TestOpenBranch(unittest.TestCase):
|
|||||||
self.assertEqual(kwargs.get("cwd"), cwd_expected)
|
self.assertEqual(kwargs.get("cwd"), cwd_expected)
|
||||||
|
|
||||||
@patch("pkgmgr.actions.branch.run_git")
|
@patch("pkgmgr.actions.branch.run_git")
|
||||||
def test_open_branch_raises_runtimeerror_on_git_failure(self, mock_run_git) -> None:
|
def test_open_branch_raises_runtimeerror_on_fetch_failure(self, mock_run_git) -> None:
|
||||||
"""
|
"""
|
||||||
If a GitError occurs (e.g. fetch fails), open_branch should
|
If a GitError occurs on fetch, open_branch should raise a RuntimeError
|
||||||
raise a RuntimeError with a helpful message.
|
with a helpful message.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def side_effect(args, cwd="."):
|
def side_effect(args, cwd="."):
|
||||||
# Simulate a failure on the first call (fetch)
|
# First call: base resolution (rev-parse) should succeed
|
||||||
raise GitError("simulated fetch failure")
|
if args == ["rev-parse", "--verify", "main"]:
|
||||||
|
return ""
|
||||||
|
# Second call: fetch should fail
|
||||||
|
if args == ["fetch", "origin"]:
|
||||||
|
raise GitError("simulated fetch failure")
|
||||||
|
return ""
|
||||||
|
|
||||||
mock_run_git.side_effect = side_effect
|
mock_run_git.side_effect = side_effect
|
||||||
|
|
||||||
@@ -95,6 +102,45 @@ class TestOpenBranch(unittest.TestCase):
|
|||||||
self.assertIn("Failed to fetch from origin", msg)
|
self.assertIn("Failed to fetch from origin", msg)
|
||||||
self.assertIn("simulated fetch failure", msg)
|
self.assertIn("simulated fetch failure", msg)
|
||||||
|
|
||||||
|
@patch("pkgmgr.actions.branch.run_git")
|
||||||
|
def test_open_branch_uses_fallback_master_if_main_missing(self, mock_run_git) -> None:
|
||||||
|
"""
|
||||||
|
If the preferred base (e.g. 'main') does not exist, open_branch should
|
||||||
|
fall back to the fallback base (default: 'master').
|
||||||
|
"""
|
||||||
|
|
||||||
|
def side_effect(args, cwd="."):
|
||||||
|
# First: rev-parse main -> fails
|
||||||
|
if args == ["rev-parse", "--verify", "main"]:
|
||||||
|
raise GitError("main does not exist")
|
||||||
|
# Second: rev-parse master -> succeeds
|
||||||
|
if args == ["rev-parse", "--verify", "master"]:
|
||||||
|
return ""
|
||||||
|
# Then normal flow on master
|
||||||
|
return ""
|
||||||
|
|
||||||
|
mock_run_git.side_effect = side_effect
|
||||||
|
|
||||||
|
open_branch(name="feature/fallback", base_branch="main", cwd="/repo")
|
||||||
|
|
||||||
|
expected_calls = [
|
||||||
|
(["rev-parse", "--verify", "main"], "/repo"),
|
||||||
|
(["rev-parse", "--verify", "master"], "/repo"),
|
||||||
|
(["fetch", "origin"], "/repo"),
|
||||||
|
(["checkout", "master"], "/repo"),
|
||||||
|
(["pull", "origin", "master"], "/repo"),
|
||||||
|
(["checkout", "-b", "feature/fallback"], "/repo"),
|
||||||
|
(["push", "-u", "origin", "feature/fallback"], "/repo"),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(mock_run_git.call_count, len(expected_calls))
|
||||||
|
for call, (args_expected, cwd_expected) in zip(
|
||||||
|
mock_run_git.call_args_list, expected_calls
|
||||||
|
):
|
||||||
|
args, kwargs = call
|
||||||
|
self.assertEqual(args[0], args_expected)
|
||||||
|
self.assertEqual(kwargs.get("cwd"), cwd_expected)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
29
tests/unit/pkgmgr/core/repository/test_ignored.py
Normal file
29
tests/unit/pkgmgr/core/repository/test_ignored.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from pkgmgr.core.repository.ignored import filter_ignored
|
||||||
|
|
||||||
|
|
||||||
|
class TestFilterIgnored(unittest.TestCase):
|
||||||
|
def test_filter_ignored_removes_repos_with_ignore_true(self) -> None:
|
||||||
|
repos = [
|
||||||
|
{"provider": "github.com", "account": "user", "repository": "a", "ignore": True},
|
||||||
|
{"provider": "github.com", "account": "user", "repository": "b", "ignore": False},
|
||||||
|
{"provider": "github.com", "account": "user", "repository": "c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
result = filter_ignored(repos)
|
||||||
|
|
||||||
|
identifiers = {(r["provider"], r["account"], r["repository"]) for r in result}
|
||||||
|
self.assertNotIn(("github.com", "user", "a"), identifiers)
|
||||||
|
self.assertIn(("github.com", "user", "b"), identifiers)
|
||||||
|
self.assertIn(("github.com", "user", "c"), identifiers)
|
||||||
|
|
||||||
|
def test_filter_ignored_empty_list_returns_empty_list(self) -> None:
|
||||||
|
self.assertEqual(filter_ignored([]), [])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
180
tests/unit/pkgmgr/core/repository/test_selected.py
Normal file
180
tests/unit/pkgmgr/core/repository/test_selected.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from pkgmgr.core.repository.selected import get_selected_repos
|
||||||
|
|
||||||
|
|
||||||
|
def _repo(
|
||||||
|
provider: str,
|
||||||
|
account: str,
|
||||||
|
repository: str,
|
||||||
|
ignore: bool | None = None,
|
||||||
|
directory: str | None = None,
|
||||||
|
):
|
||||||
|
repo = {
|
||||||
|
"provider": provider,
|
||||||
|
"account": account,
|
||||||
|
"repository": repository,
|
||||||
|
}
|
||||||
|
if ignore is not None:
|
||||||
|
repo["ignore"] = ignore
|
||||||
|
if directory is not None:
|
||||||
|
repo["directory"] = directory
|
||||||
|
return repo
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetSelectedRepos(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.repo_ignored = _repo(
|
||||||
|
"github.com",
|
||||||
|
"user",
|
||||||
|
"ignored-repo",
|
||||||
|
ignore=True,
|
||||||
|
directory="/repos/github.com/user/ignored-repo",
|
||||||
|
)
|
||||||
|
self.repo_visible = _repo(
|
||||||
|
"github.com",
|
||||||
|
"user",
|
||||||
|
"visible-repo",
|
||||||
|
ignore=False,
|
||||||
|
directory="/repos/github.com/user/visible-repo",
|
||||||
|
)
|
||||||
|
self.all_repos = [self.repo_ignored, self.repo_visible]
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 1) Explizite Identifier – ignorierte Repos dürfen ausgewählt werden
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_identifiers_bypass_ignore_filter(self) -> None:
|
||||||
|
args = SimpleNamespace(
|
||||||
|
identifiers=["ignored-repo"], # matches by repository name
|
||||||
|
all=False,
|
||||||
|
category=[],
|
||||||
|
string="",
|
||||||
|
tag=[],
|
||||||
|
include_ignored=False, # should be ignored for explicit identifiers
|
||||||
|
)
|
||||||
|
|
||||||
|
selected = get_selected_repos(args, self.all_repos)
|
||||||
|
|
||||||
|
self.assertEqual(len(selected), 1)
|
||||||
|
self.assertIs(selected[0], self.repo_ignored)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 2) Filter-only Modus – ignorierte Repos werden rausgefiltert
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_filter_mode_excludes_ignored_by_default(self) -> None:
|
||||||
|
# string-Filter, der beide Repos matchen würde
|
||||||
|
args = SimpleNamespace(
|
||||||
|
identifiers=[],
|
||||||
|
all=False,
|
||||||
|
category=[],
|
||||||
|
string="repo", # substring in beiden Namen
|
||||||
|
tag=[],
|
||||||
|
include_ignored=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
selected = get_selected_repos(args, self.all_repos)
|
||||||
|
|
||||||
|
self.assertEqual(len(selected), 1)
|
||||||
|
self.assertIs(selected[0], self.repo_visible)
|
||||||
|
|
||||||
|
def test_filter_mode_can_include_ignored_when_flag_set(self) -> None:
|
||||||
|
args = SimpleNamespace(
|
||||||
|
identifiers=[],
|
||||||
|
all=False,
|
||||||
|
category=[],
|
||||||
|
string="repo",
|
||||||
|
tag=[],
|
||||||
|
include_ignored=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
selected = get_selected_repos(args, self.all_repos)
|
||||||
|
|
||||||
|
# Beide Repos sollten erscheinen, weil include_ignored=True
|
||||||
|
self.assertEqual({r["repository"] for r in selected}, {"ignored-repo", "visible-repo"})
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 3) --all Modus – ignorierte Repos werden per Default entfernt
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_all_mode_excludes_ignored_by_default(self) -> None:
|
||||||
|
args = SimpleNamespace(
|
||||||
|
identifiers=[],
|
||||||
|
all=True,
|
||||||
|
category=[],
|
||||||
|
string="",
|
||||||
|
tag=[],
|
||||||
|
include_ignored=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
selected = get_selected_repos(args, self.all_repos)
|
||||||
|
|
||||||
|
self.assertEqual(len(selected), 1)
|
||||||
|
self.assertIs(selected[0], self.repo_visible)
|
||||||
|
|
||||||
|
def test_all_mode_can_include_ignored_when_flag_set(self) -> None:
|
||||||
|
args = SimpleNamespace(
|
||||||
|
identifiers=[],
|
||||||
|
all=True,
|
||||||
|
category=[],
|
||||||
|
string="",
|
||||||
|
tag=[],
|
||||||
|
include_ignored=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
selected = get_selected_repos(args, self.all_repos)
|
||||||
|
|
||||||
|
self.assertEqual(len(selected), 2)
|
||||||
|
self.assertCountEqual(
|
||||||
|
[r["repository"] for r in selected],
|
||||||
|
["ignored-repo", "visible-repo"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 4) CWD-Modus – Repo anhand des aktuellen Verzeichnisses auswählen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_cwd_selection_excludes_ignored_by_default(self) -> None:
|
||||||
|
# Wir lassen CWD auf das Verzeichnis des ignorierten Repos zeigen.
|
||||||
|
cwd = os.path.abspath(self.repo_ignored["directory"])
|
||||||
|
|
||||||
|
args = SimpleNamespace(
|
||||||
|
identifiers=[],
|
||||||
|
all=False,
|
||||||
|
category=[],
|
||||||
|
string="",
|
||||||
|
tag=[],
|
||||||
|
include_ignored=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("os.getcwd", return_value=cwd):
|
||||||
|
selected = get_selected_repos(args, self.all_repos)
|
||||||
|
|
||||||
|
# Da das einzige Repo für dieses Verzeichnis ignoriert ist,
|
||||||
|
# sollte die Auswahl leer sein.
|
||||||
|
self.assertEqual(selected, [])
|
||||||
|
|
||||||
|
def test_cwd_selection_can_include_ignored_when_flag_set(self) -> None:
|
||||||
|
cwd = os.path.abspath(self.repo_ignored["directory"])
|
||||||
|
|
||||||
|
args = SimpleNamespace(
|
||||||
|
identifiers=[],
|
||||||
|
all=False,
|
||||||
|
category=[],
|
||||||
|
string="",
|
||||||
|
tag=[],
|
||||||
|
include_ignored=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("os.getcwd", return_value=cwd):
|
||||||
|
selected = get_selected_repos(args, self.all_repos)
|
||||||
|
|
||||||
|
self.assertEqual(len(selected), 1)
|
||||||
|
self.assertIs(selected[0], self.repo_ignored)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user