Files
pkgmgr/pkgmgr/actions/branch/__init__.py

236 lines
6.9 KiB
Python
Raw Normal View History

# pkgmgr/actions/branch/__init__.py
#!/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
Refactor: Restructure pkgmgr into actions/, core/, and cli/ (full module breakup) This commit introduces a large-scale structural refactor of the pkgmgr codebase. All functionality has been moved from the previous flat top-level layout into three clearly separated namespaces: • pkgmgr.actions – high-level operations invoked by the CLI • pkgmgr.core – pure logic, helpers, repository utilities, versioning, git helpers, config IO, and command resolution • pkgmgr.cli – parser, dispatch, context, and command handlers Key improvements: - Moved all “branch”, “release”, “changelog”, repo-management actions, installer pipelines, and proxy execution logic into pkgmgr.actions.<domain>. - Reworked installer structure under pkgmgr.actions.repository.install.installers including OS-package installers, Nix, Python, and Makefile. - Consolidated all low-level functionality under pkgmgr.core: • git helpers → core/git • config load/save → core/config • repository helpers → core/repository • versioning & semver → core/version • command helpers (alias, resolve, run, ink) → core/command - Replaced pkgmgr.cli_core with pkgmgr.cli and updated all imports. - Added minimal __init__.py files for clean package exposure. - Updated all E2E, integration, and unit tests with new module paths. - Fixed patch targets so mocks point to the new structure. - Ensured backward compatibility at the CLI boundary (pkgmgr entry point unchanged). This refactor produces a cleaner, layered architecture: - `core` = logic - `actions` = orchestrated behaviour - `cli` = user interface Reference: ChatGPT-assisted refactor discussion https://chatgpt.com/share/6938221c-e24c-800f-8317-7732cedf39b9
2025-12-09 14:20:19 +01:00
(pkgmgr.cli.commands.branch) stays thin and testable.
"""
from __future__ import annotations
from typing import Optional
Refactor: Restructure pkgmgr into actions/, core/, and cli/ (full module breakup) This commit introduces a large-scale structural refactor of the pkgmgr codebase. All functionality has been moved from the previous flat top-level layout into three clearly separated namespaces: • pkgmgr.actions – high-level operations invoked by the CLI • pkgmgr.core – pure logic, helpers, repository utilities, versioning, git helpers, config IO, and command resolution • pkgmgr.cli – parser, dispatch, context, and command handlers Key improvements: - Moved all “branch”, “release”, “changelog”, repo-management actions, installer pipelines, and proxy execution logic into pkgmgr.actions.<domain>. - Reworked installer structure under pkgmgr.actions.repository.install.installers including OS-package installers, Nix, Python, and Makefile. - Consolidated all low-level functionality under pkgmgr.core: • git helpers → core/git • config load/save → core/config • repository helpers → core/repository • versioning & semver → core/version • command helpers (alias, resolve, run, ink) → core/command - Replaced pkgmgr.cli_core with pkgmgr.cli and updated all imports. - Added minimal __init__.py files for clean package exposure. - Updated all E2E, integration, and unit tests with new module paths. - Fixed patch targets so mocks point to the new structure. - Ensured backward compatibility at the CLI boundary (pkgmgr entry point unchanged). This refactor produces a cleaner, layered architecture: - `core` = logic - `actions` = orchestrated behaviour - `cli` = user interface Reference: ChatGPT-assisted refactor discussion https://chatgpt.com/share/6938221c-e24c-800f-8317-7732cedf39b9
2025-12-09 14:20:19 +01:00
from pkgmgr.core.git import run_git, GitError, get_current_branch
# ---------------------------------------------------------------------------
# Branch creation (open)
# ---------------------------------------------------------------------------
def open_branch(
name: Optional[str],
base_branch: str = "main",
fallback_base: str = "master",
cwd: str = ".",
) -> None:
"""
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:
1) git fetch origin
2) git checkout <resolved_base>
3) git pull origin <resolved_base>
4) git checkout -b <name>
5) git push -u origin <name>
If `name` is None or empty, the user is prompted to enter one.
"""
# Request name interactively if not provided
if not name:
name = input("Enter new branch name: ").strip()
if not name:
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
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", resolved_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to checkout base branch {resolved_base!r}: {exc}"
) from exc
# 3) Pull latest changes for base branch
try:
run_git(["pull", "origin", resolved_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to pull latest changes for base branch {resolved_base!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 {resolved_base!r}: {exc}"
) from exc
# 5) Push new branch to origin
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
2025-12-08 23:02:43 +01:00
# ---------------------------------------------------------------------------
# Base branch resolver (shared by open/close)
# ---------------------------------------------------------------------------
2025-12-08 23:02:43 +01:00
def _resolve_base_branch(
preferred: str,
fallback: str,
cwd: str,
) -> str:
"""
Resolve the base branch to use.
Try `preferred` first (default: main),
fall back to `fallback` (default: master).
2025-12-08 23:02:43 +01:00
Raise RuntimeError if neither exists.
"""
for candidate in (preferred, fallback):
try:
run_git(["rev-parse", "--verify", candidate], cwd=cwd)
return candidate
except GitError:
continue
raise RuntimeError(
f"Neither {preferred!r} nor {fallback!r} exist in this repository."
)
# ---------------------------------------------------------------------------
# Branch closing (merge + deletion)
# ---------------------------------------------------------------------------
2025-12-08 23:02:43 +01:00
def close_branch(
name: Optional[str],
base_branch: str = "main",
fallback_base: str = "master",
cwd: str = ".",
) -> None:
"""
Merge a feature branch into the base branch and delete it afterwards.
2025-12-08 23:02:43 +01:00
Steps:
1) Determine the branch name (argument or current branch)
2) Resolve base branch (main/master)
3) Ask for confirmation
4) git fetch origin
5) git checkout <base>
6) git pull origin <base>
7) git merge --no-ff <name>
8) git push origin <base>
9) Delete branch locally
10) Delete branch on origin (best effort)
2025-12-08 23:02:43 +01:00
"""
# 1) Determine which branch should be closed
2025-12-08 23:02:43 +01:00
if not name:
try:
name = get_current_branch(cwd=cwd)
except GitError as exc:
raise RuntimeError(f"Failed to detect current branch: {exc}") from exc
if not name:
raise RuntimeError("Branch name must not be empty.")
# 2) Resolve base branch
2025-12-08 23:02:43 +01:00
target_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
if name == target_base:
raise RuntimeError(
f"Refusing to close base branch {target_base!r}. "
"Please specify a feature branch."
)
# 3) Ask user for confirmation
2025-12-08 23:02:43 +01:00
prompt = (
f"Merge branch '{name}' into '{target_base}' and delete it afterwards? "
"(y/N): "
)
answer = input(prompt).strip().lower()
if answer != "y":
print("Aborted closing branch.")
return
# 4) Fetch from origin
try:
run_git(["fetch", "origin"], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to fetch from origin before closing branch {name!r}: {exc}"
) from exc
# 5) Checkout base
2025-12-08 23:02:43 +01:00
try:
run_git(["checkout", target_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to checkout base branch {target_base!r}: {exc}"
) from exc
# 6) Pull latest base state
2025-12-08 23:02:43 +01:00
try:
run_git(["pull", "origin", target_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to pull latest changes for base branch {target_base!r}: {exc}"
) from exc
# 7) Merge the feature branch
2025-12-08 23:02:43 +01:00
try:
run_git(["merge", "--no-ff", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to merge branch {name!r} into {target_base!r}: {exc}"
) from exc
# 8) Push updated base
try:
run_git(["push", "origin", target_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to push base branch {target_base!r} after merge: {exc}"
2025-12-08 23:02:43 +01:00
) from exc
# 9) Delete branch locally
2025-12-08 23:02:43 +01:00
try:
run_git(["branch", "-d", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to delete local branch {name!r}: {exc}"
2025-12-08 23:02:43 +01:00
) from exc
# 10) Delete branch on origin (best effort)
2025-12-08 23:02:43 +01:00
try:
run_git(["push", "origin", "--delete", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Branch {name!r} was deleted locally, but remote deletion failed: {exc}"
) from exc