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.
- enhanced_git-1.0.0.dist-info/METADATA +349 -0
- enhanced_git-1.0.0.dist-info/RECORD +18 -0
- enhanced_git-1.0.0.dist-info/WHEEL +4 -0
- enhanced_git-1.0.0.dist-info/entry_points.txt +2 -0
- enhanced_git-1.0.0.dist-info/licenses/LICENSE +21 -0
- gitai/__init__.py +3 -0
- gitai/changelog.py +251 -0
- gitai/cli.py +166 -0
- gitai/commit.py +338 -0
- gitai/config.py +120 -0
- gitai/constants.py +134 -0
- gitai/diff.py +167 -0
- gitai/hook.py +81 -0
- gitai/providers/__init__.py +1 -0
- gitai/providers/base.py +71 -0
- gitai/providers/ollama_provider.py +86 -0
- gitai/providers/openai_provider.py +78 -0
- gitai/util.py +137 -0
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}")
|