Coverage for src / rhiza_tools / commands / _shared.py: 74%
31 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-23 01:10 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-23 01:10 +0000
1"""Shared utilities for rhiza-tools commands.
3This module provides common helpers used across multiple command modules
4(bump, release, rollback) to avoid duplication and ensure consistency.
6Utilities:
7 - COOL_STYLE: Shared questionary styling for interactive prompts
8 - run_git_command: Execute git commands with standard error handling
9 - get_current_version: Read the project version from pyproject.toml
10 - get_current_git_branch: Safely determine the current git branch
11 - validate_pyproject_exists: Guard against missing pyproject.toml
12"""
14import subprocess # nosec B404 - subprocess needed for git operations
15from pathlib import Path
16from typing import Any, cast
18import questionary as qs
19import tomlkit
20import typer
22from rhiza_tools import console
24COOL_STYLE = qs.Style(
25 [
26 ("separator", "fg:#cc5454"),
27 ("qmark", "fg:#2FA4A9 bold"),
28 ("question", ""),
29 ("selected", "fg:#2FA4A9 bold"),
30 ("pointer", "fg:#2FA4A9 bold"),
31 ("highlighted", "fg:#2FA4A9 bold"),
32 ("answer", "fg:#2FA4A9 bold"),
33 ("text", "fg:#ffffff"),
34 ("disabled", "fg:#858585 italic"),
35 ]
36)
39def run_git_command(command: list[str], check: bool = True) -> subprocess.CompletedProcess[str]:
40 """Run a git command and return the result.
42 Args:
43 command: The git command to run as a list of arguments.
44 check: If True, raise an exception on non-zero exit code.
46 Returns:
47 CompletedProcess instance with stdout, stderr, and returncode.
49 Raises:
50 subprocess.CalledProcessError: If check=True and command fails.
52 Example:
53 >>> result = run_git_command(["git", "status", "--porcelain"]) # doctest: +SKIP
54 >>> print(result.stdout) # doctest: +SKIP
55 """
56 result = subprocess.run(command, capture_output=True, text=True, check=False) # nosec B603 - git commands are trusted # noqa: S603
57 if check and result.returncode != 0:
58 console.error(f"Git command failed: {' '.join(command)}")
59 console.error(f"Error: {result.stderr}")
60 raise subprocess.CalledProcessError(result.returncode, command, result.stdout, result.stderr)
61 return result
64def get_current_version() -> str:
65 """Read current version from pyproject.toml.
67 Returns:
68 The current version string from the project.version field.
70 Raises:
71 typer.Exit: If pyproject.toml cannot be read or parsed.
73 Example:
74 >>> version = get_current_version() # doctest: +SKIP
75 >>> print(version) # doctest: +SKIP
76 0.1.0
77 """
78 try:
79 with open("pyproject.toml") as f:
80 data = tomlkit.parse(f.read())
81 project = cast(dict[str, Any], data["project"])
82 return str(project["version"])
83 except Exception as e:
84 console.error(f"Failed to read version from pyproject.toml: {e}")
85 raise typer.Exit(code=1) from None
88def get_current_git_branch() -> str:
89 """Get the current git branch name.
91 This is the safe variant that returns ``"unknown"`` on failure,
92 suitable for display purposes. For strict validation use
93 :func:`run_git_command` directly.
95 Returns:
96 Current branch name or "unknown" if unable to determine.
97 """
98 result = run_git_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], check=False)
99 return result.stdout.strip() if result.returncode == 0 else "unknown"
102def validate_pyproject_exists() -> None:
103 """Validate that pyproject.toml exists in the current directory.
105 Raises:
106 typer.Exit: If pyproject.toml is not found.
107 """
108 if not Path("pyproject.toml").exists():
109 console.error("pyproject.toml not found in current directory")
110 raise typer.Exit(code=1)