enhanced-git 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gitai/util.py ADDED
@@ -0,0 +1,137 @@
1
+ """Utility functions for GitAI."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ import textwrap
7
+ import tomllib
8
+ from pathlib import Path
9
+ from typing import Any, NoReturn
10
+
11
+ from rich.console import Console
12
+
13
+ console = Console()
14
+
15
+
16
+ def run(cmd: list[str], cwd: Path | None = None, check: bool = True) -> str:
17
+ """Run a command and return its stdout."""
18
+ try:
19
+ result = subprocess.run(
20
+ cmd,
21
+ cwd=cwd,
22
+ capture_output=True,
23
+ text=True,
24
+ check=check,
25
+ )
26
+ return result.stdout.strip()
27
+ except subprocess.CalledProcessError as e:
28
+ error_msg = f"Command failed: {' '.join(cmd)}\n{e.stderr}"
29
+ console.print(f"[red]Error:[/red] {error_msg}")
30
+ if check:
31
+ sys.exit(1)
32
+ return ""
33
+
34
+
35
+ def find_git_root() -> Path:
36
+ """Find the root of the git repository."""
37
+ try:
38
+ result = run(["git", "rev-parse", "--show-toplevel"])
39
+ return Path(result)
40
+ except SystemExit:
41
+ console.print("[red]Error:[/red] Not in a git repository")
42
+ sys.exit(1)
43
+
44
+
45
+ def load_toml_config(path: Path) -> dict[str, Any]:
46
+ """Load TOML configuration file."""
47
+ if not path.exists():
48
+ return {}
49
+
50
+ try:
51
+ with open(path, "rb") as f:
52
+ return tomllib.load(f)
53
+ except Exception as e:
54
+ console.print(f"[yellow]Warning:[/yellow] Failed to load config {path}: {e}")
55
+ return {}
56
+
57
+
58
+ def wrap_text(text: str, width: int = 72) -> str:
59
+ """Wrap text to specified width."""
60
+ return textwrap.fill(
61
+ text, width=width, break_long_words=False, break_on_hyphens=False
62
+ )
63
+
64
+
65
+ def truncate_subject(subject: str, max_length: int = 70) -> str:
66
+ """Truncate subject line if too long."""
67
+ if len(subject) <= max_length:
68
+ return subject
69
+
70
+ # try to truncate at word boundary
71
+ truncated = subject[:max_length]
72
+ last_space = truncated.rfind(" ")
73
+ if last_space > max_length // 2: # don't truncate too much
74
+ truncated = truncated[:last_space]
75
+
76
+ return truncated.rstrip() + "..."
77
+
78
+
79
+ def extract_scope_from_path(path: str) -> str | None:
80
+ """Extract scope from file path."""
81
+ parts = Path(path).parts
82
+ if not parts:
83
+ return None
84
+
85
+ # use first directory as scope
86
+ first_dir = parts[0]
87
+ if first_dir in ("src", "lib", "app", "core", "api", "web", "cli", "utils"):
88
+ return first_dir
89
+
90
+ # for deeper paths, use the most specific meaningful directory
91
+ for part in reversed(parts[:-1]): # exclude filename
92
+ if part not in ("__pycache__", "node_modules", ".git", "tests", "docs"):
93
+ return part
94
+
95
+ return None
96
+
97
+
98
+ def make_executable(path: Path) -> None:
99
+ """Make a file executable."""
100
+ current_mode = path.stat().st_mode
101
+ path.chmod(current_mode | 0o111)
102
+
103
+
104
+ def backup_file(path: Path) -> Path | None:
105
+ """Create a backup of a file."""
106
+ if not path.exists():
107
+ return None
108
+
109
+ backup_path = path.with_suffix(path.suffix + ".backup")
110
+ counter = 1
111
+ while backup_path.exists():
112
+ backup_path = path.with_suffix(f"{path.suffix}.backup{counter}")
113
+ counter += 1
114
+
115
+ shutil.copy2(path, backup_path)
116
+ return backup_path
117
+
118
+
119
+ def exit_with_error(message: str) -> NoReturn:
120
+ """Exit with an error message."""
121
+ console.print(f"[red]Error:[/red] {message}")
122
+ sys.exit(1)
123
+
124
+
125
+ def print_success(message: str) -> None:
126
+ """Print a success message."""
127
+ console.print(f"[green]✓[/green] {message}")
128
+
129
+
130
+ def print_warning(message: str) -> None:
131
+ """Print a warning message."""
132
+ console.print(f"[yellow]Warning:[/yellow] {message}")
133
+
134
+
135
+ def print_info(message: str) -> None:
136
+ """Print an info message."""
137
+ console.print(f"[blue]Info:[/blue] {message}")