Add branch CLI command and tests (see ChatGPT conversation: https://chatgpt.com/share/69370ce1-8090-800f-8b08-8ecfa5089a74)

Signed-off-by: Kevin Veen-Birkenbach <kevin@veen.world>
This commit is contained in:
Kevin Veen-Birkenbach
2025-12-08 18:37:59 +01:00
parent 22b65f83d3
commit b9b64fed7d
8 changed files with 391 additions and 2 deletions

80
pkgmgr/branch_commands.py Normal file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
High-level helpers for branch-related operations.
This module encapsulates the actual Git logic so the CLI layer
(pkgmgr.cli_core.commands.branch) stays thin and testable.
"""
from __future__ import annotations
from typing import Optional
from pkgmgr.git_utils import run_git, GitError
def open_branch(
name: Optional[str],
base_branch: str = "main",
cwd: str = ".",
) -> None:
"""
Create and push a new feature branch on top of `base_branch`.
Steps:
1) git fetch origin
2) git checkout <base_branch>
3) git pull origin <base_branch>
4) git checkout -b <name>
5) git push -u origin <name>
If `name` is None or empty, the user is prompted on stdin.
"""
if not name:
name = input("Enter new branch name: ").strip()
if not name:
raise RuntimeError("Branch name must not be empty.")
# 1) Fetch from origin
try:
run_git(["fetch", "origin"], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to fetch from origin before creating branch {name!r}: {exc}"
) from exc
# 2) Checkout base branch
try:
run_git(["checkout", base_branch], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to checkout base branch {base_branch!r}: {exc}"
) from exc
# 3) Pull latest changes on base
try:
run_git(["pull", "origin", base_branch], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to pull latest changes for base branch {base_branch!r}: {exc}"
) from exc
# 4) Create new branch
try:
run_git(["checkout", "-b", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to create new branch {name!r} from base {base_branch!r}: {exc}"
) from exc
# 5) Push and set upstream
try:
run_git(["push", "-u", "origin", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to push new branch {name!r} to origin: {exc}"
) from exc

View File

@@ -5,6 +5,7 @@ from .release import handle_release
from .version import handle_version
from .make import handle_make
from .changelog import handle_changelog
from .branch import handle_branch
__all__ = [
"handle_repos_command",
@@ -14,4 +15,5 @@ __all__ = [
"handle_version",
"handle_make",
"handle_changelog",
"handle_branch",
]

View File

@@ -0,0 +1,25 @@
from __future__ import annotations
import sys
from pkgmgr.cli_core.context import CLIContext
from pkgmgr.branch_commands import open_branch
def handle_branch(args, ctx: CLIContext) -> None:
"""
Handle `pkgmgr branch` subcommands.
Currently supported:
- pkgmgr branch open [<name>] [--base <branch>]
"""
if args.subcommand == "open":
open_branch(
name=getattr(args, "name", None),
base_branch=getattr(args, "base", "main"),
cwd=".",
)
return
print(f"Unknown branch subcommand: {args.subcommand}")
sys.exit(2)

View File

@@ -15,6 +15,7 @@ from pkgmgr.cli_core.commands import (
handle_config,
handle_make,
handle_changelog,
handle_branch,
)
@@ -47,6 +48,7 @@ def dispatch_command(args, ctx: CLIContext) -> None:
"version",
"make",
"changelog",
# intentionally NOT "branch" it operates on cwd only
]
if args.command in commands_with_selection:
@@ -83,6 +85,10 @@ def dispatch_command(args, ctx: CLIContext) -> None:
handle_config(args, ctx)
elif args.command == "make":
handle_make(args, ctx, selected)
elif args.command == "branch":
# Branch commands currently operate on the current working
# directory only, not on the pkgmgr repository selection.
handle_branch(args, ctx)
else:
print(f"Unknown command: {args.command}")
sys.exit(2)

View File

@@ -269,6 +269,35 @@ def create_parser(description_text: str) -> argparse.ArgumentParser:
default=[],
)
# ------------------------------------------------------------
# branch
# ------------------------------------------------------------
branch_parser = subparsers.add_parser(
"branch",
help="Branch-related utilities (e.g. open feature branches)",
)
branch_subparsers = branch_parser.add_subparsers(
dest="subcommand",
help="Branch subcommands",
required=True,
)
branch_open = branch_subparsers.add_parser(
"open",
help="Create and push a new branch on top of a base branch",
)
branch_open.add_argument(
"name",
nargs="?",
help="Name of the new branch (optional; will be asked interactively if omitted)",
)
branch_open.add_argument(
"--base",
default="main",
help="Base branch to create the new branch from (default: main)",
)
# ------------------------------------------------------------
# release
# ------------------------------------------------------------
@@ -306,8 +335,6 @@ def create_parser(description_text: str) -> argparse.ArgumentParser:
)
add_identifier_arguments(version_parser)
# ------------------------------------------------------------
# changelog
# ------------------------------------------------------------