diff --git a/CHANGELOG.md b/CHANGELOG.md index a128505..8aae99c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [0.5.0] - 2025-12-09 + +* Add pkgmgr branch close subcommand, extend CLI parser wiring, and add unit tests for branch handling and version version-selection logic (see ChatGPT conversation: https://chatgpt.com/share/693762a3-9ea8-800f-a640-bc78170953d1) + + ## [0.4.3] - 2025-12-09 * Implement current-directory repository selection for release and proxy commands, unify selection semantics across CLI layers, extend release workflow with --close, integrate branch closing logic, fix wiring for get_repo_identifier/get_repo_dir, update packaging files (PKGBUILD, spec, flake.nix, pyproject), and add comprehensive unit/e2e tests for release and branch commands (see ChatGPT conversation: https://chatgpt.com/share/69375cfe-9e00-800f-bd65-1bd5937e1696) diff --git a/PKGBUILD b/PKGBUILD index 43c7a7f..c8e1b82 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Kevin Veen-Birkenbach pkgname=package-manager -pkgver=0.4.3 +pkgver=0.5.0 pkgrel=1 pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)." arch=('any') diff --git a/debian/changelog b/debian/changelog index b926f13..ca2a5c7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +package-manager (0.5.0-1) unstable; urgency=medium + + * Add pkgmgr branch close subcommand, extend CLI parser wiring, and add unit tests for branch handling and version version-selection logic (see ChatGPT conversation: https://chatgpt.com/share/693762a3-9ea8-800f-a640-bc78170953d1) + + -- Kevin Veen-Birkenbach Tue, 09 Dec 2025 00:44:16 +0100 + package-manager (0.4.3-1) unstable; urgency=medium * Implement current-directory repository selection for release and proxy commands, unify selection semantics across CLI layers, extend release workflow with --close, integrate branch closing logic, fix wiring for get_repo_identifier/get_repo_dir, update packaging files (PKGBUILD, spec, flake.nix, pyproject), and add comprehensive unit/e2e tests for release and branch commands (see ChatGPT conversation: https://chatgpt.com/share/69375cfe-9e00-800f-bd65-1bd5937e1696) diff --git a/flake.nix b/flake.nix index 75510bf..1602e1e 100644 --- a/flake.nix +++ b/flake.nix @@ -31,7 +31,7 @@ rec { pkgmgr = pyPkgs.buildPythonApplication { pname = "package-manager"; - version = "0.4.3"; + version = "0.5.0"; # Use the git repo as source src = ./.; diff --git a/package-manager.spec b/package-manager.spec index fd3db8b..f505d76 100644 --- a/package-manager.spec +++ b/package-manager.spec @@ -1,5 +1,5 @@ Name: package-manager -Version: 0.4.3 +Version: 0.5.0 Release: 1%{?dist} Summary: Wrapper that runs Kevin's package-manager via Nix flake diff --git a/pkgmgr/cli_core/parser.py b/pkgmgr/cli_core/parser.py index 4c57032..51671ae 100644 --- a/pkgmgr/cli_core/parser.py +++ b/pkgmgr/cli_core/parser.py @@ -316,7 +316,7 @@ def create_parser(description_text: str) -> argparse.ArgumentParser: # ------------------------------------------------------------ branch_parser = subparsers.add_parser( "branch", - help="Branch-related utilities (e.g. open feature branches)", + help="Branch-related utilities (e.g. open/close feature branches)", ) branch_subparsers = branch_parser.add_subparsers( dest="subcommand", @@ -342,6 +342,27 @@ def create_parser(description_text: str) -> argparse.ArgumentParser: help="Base branch to create the new branch from (default: main)", ) + branch_close = branch_subparsers.add_parser( + "close", + help="Merge a feature branch into base and delete it", + ) + branch_close.add_argument( + "name", + nargs="?", + help=( + "Name of the branch to close (optional; current branch is used " + "if omitted)" + ), + ) + branch_close.add_argument( + "--base", + default="main", + help=( + "Base branch to merge into (default: main; falls back to master " + "internally if main does not exist)" + ), + ) + # ------------------------------------------------------------ # release # ------------------------------------------------------------ diff --git a/pyproject.toml b/pyproject.toml index 918a25f..66b3b7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "package-manager" -version = "0.4.3" +version = "0.5.0" description = "Kevin's package-manager tool (pkgmgr)" readme = "README.md" requires-python = ">=3.11" diff --git a/tests/unit/pkgmgr/cli_core/test_branch_cli.py b/tests/unit/pkgmgr/cli_core/test_branch_cli.py new file mode 100644 index 0000000..63f6f47 --- /dev/null +++ b/tests/unit/pkgmgr/cli_core/test_branch_cli.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Unit tests for the `pkgmgr branch` CLI wiring. + +These tests verify that: + - The argument parser creates the correct structure for + `branch open` and `branch close`. + - `handle_branch` calls the corresponding helper functions + with the expected arguments (including base branch and cwd). +""" + +from __future__ import annotations + +import unittest +from unittest.mock import patch + +from pkgmgr.cli_core.parser import create_parser +from pkgmgr.cli_core.commands.branch import handle_branch + + +class TestBranchCLI(unittest.TestCase): + """ + Tests for the branch subcommands implemented in cli_core. + """ + + def _create_parser(self): + """ + Create the top-level parser with a minimal description. + """ + return create_parser("pkgmgr test parser") + + @patch("pkgmgr.cli_core.commands.branch.open_branch") + def test_branch_open_with_name_and_base(self, mock_open_branch): + """ + Ensure that `pkgmgr branch open --base ` calls + open_branch() with the correct parameters. + """ + parser = self._create_parser() + args = parser.parse_args( + ["branch", "open", "feature/test-branch", "--base", "develop"] + ) + + # Sanity check: parser wiring + self.assertEqual(args.command, "branch") + self.assertEqual(args.subcommand, "open") + self.assertEqual(args.name, "feature/test-branch") + self.assertEqual(args.base, "develop") + + # ctx is currently unused by handle_branch, so we can pass None + handle_branch(args, ctx=None) + + mock_open_branch.assert_called_once() + _args, kwargs = mock_open_branch.call_args + + self.assertEqual(kwargs.get("name"), "feature/test-branch") + self.assertEqual(kwargs.get("base_branch"), "develop") + self.assertEqual(kwargs.get("cwd"), ".") + + @patch("pkgmgr.cli_core.commands.branch.close_branch") + def test_branch_close_with_name_and_base(self, mock_close_branch): + """ + Ensure that `pkgmgr branch close --base ` calls + close_branch() with the correct parameters. + """ + parser = self._create_parser() + args = parser.parse_args( + ["branch", "close", "feature/old-branch", "--base", "main"] + ) + + # Sanity check: parser wiring + self.assertEqual(args.command, "branch") + self.assertEqual(args.subcommand, "close") + self.assertEqual(args.name, "feature/old-branch") + self.assertEqual(args.base, "main") + + handle_branch(args, ctx=None) + + mock_close_branch.assert_called_once() + _args, kwargs = mock_close_branch.call_args + + self.assertEqual(kwargs.get("name"), "feature/old-branch") + self.assertEqual(kwargs.get("base_branch"), "main") + self.assertEqual(kwargs.get("cwd"), ".") + + @patch("pkgmgr.cli_core.commands.branch.close_branch") + def test_branch_close_without_name_uses_none(self, mock_close_branch): + """ + Ensure that `pkgmgr branch close` without a name passes name=None + into close_branch(), leaving branch resolution to the helper. + """ + parser = self._create_parser() + args = parser.parse_args(["branch", "close"]) + + # Parser wiring: no name → None + self.assertEqual(args.command, "branch") + self.assertEqual(args.subcommand, "close") + self.assertIsNone(args.name) + + handle_branch(args, ctx=None) + + mock_close_branch.assert_called_once() + _args, kwargs = mock_close_branch.call_args + + self.assertIsNone(kwargs.get("name")) + self.assertEqual(kwargs.get("base_branch"), "main") + self.assertEqual(kwargs.get("cwd"), ".") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/pkgmgr/test_cli.py b/tests/unit/pkgmgr/test_cli.py index 4a3658c..837f586 100644 --- a/tests/unit/pkgmgr/test_cli.py +++ b/tests/unit/pkgmgr/test_cli.py @@ -42,7 +42,7 @@ def _fake_config() -> Dict[str, Any]: "workspaces": "/tmp/pkgmgr-workspaces", }, # The actual list of repositories is not used directly by the tests, - # because we mock get_selected_repos(). It must exist, though. + # because we mock the selection logic. It must exist, though. "repositories": [], } @@ -54,8 +54,9 @@ class TestCliVersion(unittest.TestCase): Each test: - Runs in a temporary working directory. - Uses a fake configuration via load_config(). - - Uses a mocked get_selected_repos() that returns a single repo - pointing to the temporary directory. + - Uses the same selection logic as the new CLI: + * dispatch_command() calls _select_repo_for_current_directory() + when there is no explicit selection. """ def setUp(self) -> None: @@ -64,31 +65,30 @@ class TestCliVersion(unittest.TestCase): self._old_cwd = os.getcwd() os.chdir(self._tmp_dir.name) + # Define a fake repo pointing to our temp dir + self._fake_repo = { + "provider": "github.com", + "account": "test", + "repository": "pkgmgr-test", + "directory": self._tmp_dir.name, + } + # Patch load_config so cli.main() does not read real config files self._patch_load_config = mock.patch( "pkgmgr.cli.load_config", return_value=_fake_config() ) self.mock_load_config = self._patch_load_config.start() - # Patch get_selected_repos so that 'version' operates on our temp dir. - # In the new modular CLI this function is used inside - # pkgmgr.cli_core.dispatch, so we patch it there. - def _fake_selected_repos(args, all_repositories): - return [ - { - "provider": "github.com", - "account": "test", - "repository": "pkgmgr-test", - "directory": self._tmp_dir.name, - } - ] - - - self._patch_get_selected_repos = mock.patch( - "pkgmgr.cli_core.dispatch.get_selected_repos", - side_effect=_fake_selected_repos, + # Patch the "current directory" selection used by dispatch_command(). + # This matches the new behaviour: without explicit identifiers, + # version uses _select_repo_for_current_directory(ctx). + self._patch_select_repo_for_current_directory = mock.patch( + "pkgmgr.cli_core.dispatch._select_repo_for_current_directory", + return_value=[self._fake_repo], + ) + self.mock_select_repo_for_current_directory = ( + self._patch_select_repo_for_current_directory.start() ) - self.mock_get_selected_repos = self._patch_get_selected_repos.start() # Keep a reference to the original sys.argv, so we can restore it self._old_argv = list(sys.argv) @@ -98,7 +98,7 @@ class TestCliVersion(unittest.TestCase): sys.argv = self._old_argv # Stop all patches - self._patch_get_selected_repos.stop() + self._patch_select_repo_for_current_directory.stop() self._patch_load_config.stop() # Restore working directory @@ -224,7 +224,7 @@ class TestCliVersion(unittest.TestCase): # Arrange: pyproject.toml exists self._write_pyproject("0.0.1") - # Arrange: no tags returned (again: patch handle_version's get_tags) + # Arrange: no tags returned with mock.patch( "pkgmgr.cli_core.commands.version.get_tags", return_value=[],