Skip to content

Commit e99f666

Browse files
feat(goal): configuration management system
changes: - file: manager.py area: service added: [install_hooks, HooksManager, uninstall_hooks, install_precommit, is_hooks_configured, run_hooks, +6 more] - file: strategies.py area: core modified: [LargeFileStrategy, __init__] stats: lines: "+331/-3 (net +328)" files: 4 complexity: "Large structural change (normalized)"
1 parent 21881d3 commit e99f666

11 files changed

Lines changed: 341 additions & 7 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/tmp/tmpjepz6y1j.txt
2+
/tmp/tmp861fca5n.txt
13
/tmp/tmpxf9e6i4t.txt
24
/tmp/tmplsf9fd_l.txt
35
/tmp/tmp1fqbnpsf.txt

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## [Unreleased]
22

3+
## [2.1.110] - 2026-03-27
4+
35
## [2.1.109] - 2026-03-27
46

57
### Docs

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Goal
55

66
<p align="center">
7-
<img src="https://img.shields.io/badge/version-2.1.109-blue.svg" alt="Version">
7+
<img src="https://img.shields.io/badge/version-2.1.110-blue.svg" alt="Version">
88
<img src="https://img.shields.io/badge/python-3.8+-blue.svg" alt="Python">
99
<img src="https://img.shields.io/badge/license-Apache%202.0-blue.svg" alt="License">
1010
<img src="https://img.shields.io/badge/pypi-goal-orange.svg" alt="PyPI">

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.1.109
1+
2.1.110

goal.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# goal.yaml - Goal configuration file
2-
# Generated: 2026-03-27 13:39:45
2+
# Generated: 2026-03-27 13:45:57
33
# Documentation: https://github.com/wronai/goal#configuration
44
#
55
# This file configures Goal's behavior for:

goal/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Goal - Automated git push with smart commit messages, changelog updates, and version tagging."""
22

3-
__version__ = "2.1.109"
3+
__version__ = "2.1.110"

goal/hooks/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Pre-commit hooks integration for Goal.
2+
3+
Provides functionality for:
4+
- Installing pre-commit hooks
5+
- Running validation checks before commits
6+
- Managing hook configuration
7+
"""
8+
9+
from .manager import HooksManager, install_hooks, uninstall_hooks, run_hooks
10+
from .config import get_hook_config, create_precommit_config
11+
12+
__all__ = [
13+
'HooksManager',
14+
'install_hooks',
15+
'uninstall_hooks',
16+
'run_hooks',
17+
'get_hook_config',
18+
'create_precommit_config',
19+
]

goal/hooks/manager.py

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
"""Pre-commit hooks manager for Goal.
2+
3+
This module provides functionality to install, manage, and run pre-commit hooks
4+
that integrate with Goal's validation system.
5+
"""
6+
7+
import os
8+
import sys
9+
import subprocess
10+
import yaml
11+
from pathlib import Path
12+
from typing import List, Dict, Any, Optional, Tuple
13+
import click
14+
15+
from ..validators.file_validator import (
16+
validate_files,
17+
ValidationError,
18+
FileSizeError,
19+
TokenDetectedError,
20+
DotFolderError
21+
)
22+
from ..git_ops import get_staged_files, run_git
23+
24+
25+
class HooksManager:
26+
"""Manages pre-commit hooks for Goal."""
27+
28+
def __init__(self, project_dir: Optional[Path] = None):
29+
"""Initialize hooks manager.
30+
31+
Args:
32+
project_dir: Project directory (defaults to current directory)
33+
"""
34+
self.project_dir = project_dir or Path.cwd()
35+
self.precommit_file = self.project_dir / '.pre-commit-config.yaml'
36+
self.goal_hook_script = self.project_dir / '.goal' / 'pre-commit-hook.py'
37+
38+
def is_precommit_installed(self) -> bool:
39+
"""Check if pre-commit is installed."""
40+
try:
41+
result = subprocess.run(['pre-commit', '--version'],
42+
capture_output=True, check=True)
43+
return True
44+
except (subprocess.CalledProcessError, FileNotFoundError):
45+
return False
46+
47+
def is_hooks_configured(self) -> bool:
48+
"""Check if Goal hooks are configured."""
49+
return self.precommit_file.exists() and self.goal_hook_script.exists()
50+
51+
def install_precommit(self) -> bool:
52+
"""Install pre-commit if not already installed."""
53+
if self.is_precommit_installed():
54+
click.echo(click.style("✓ pre-commit is already installed", fg='green'))
55+
return True
56+
57+
click.echo("Installing pre-commit...")
58+
try:
59+
subprocess.run([sys.executable, '-m', 'pip', 'install', 'pre-commit'],
60+
check=True)
61+
click.echo(click.style("✓ pre-commit installed successfully", fg='green'))
62+
return True
63+
except subprocess.CalledProcessError:
64+
click.echo(click.style("✗ Failed to install pre-commit", fg='red'))
65+
click.echo("Please install it manually: pip install pre-commit")
66+
return False
67+
68+
def create_hook_script(self) -> None:
69+
"""Create the Goal hook script."""
70+
hook_dir = self.goal_hook_script.parent
71+
hook_dir.mkdir(exist_ok=True)
72+
73+
hook_content = '''#!/usr/bin/env python3
74+
"""Goal pre-commit hook.
75+
76+
This script runs Goal's validation checks before commit.
77+
"""
78+
79+
import sys
80+
from pathlib import Path
81+
82+
# Add goal to Python path
83+
goal_path = Path(__file__).parent.parent
84+
sys.path.insert(0, str(goal_path))
85+
86+
from goal.hooks.manager import HooksManager
87+
88+
def main():
89+
"""Run pre-commit validation."""
90+
manager = HooksManager()
91+
success = manager.run_validation()
92+
sys.exit(0 if success else 1)
93+
94+
if __name__ == '__main__':
95+
main()
96+
'''
97+
98+
self.goal_hook_script.write_text(hook_content)
99+
self.goal_hook_script.chmod(0o755)
100+
101+
def create_precommit_config(self, force: bool = False) -> bool:
102+
"""Create .pre-commit-config.yaml with Goal hooks.
103+
104+
Args:
105+
force: Overwrite existing config
106+
107+
Returns:
108+
True if config was created
109+
"""
110+
if self.precommit_file.exists() and not force:
111+
click.echo(click.style("⚠ .pre-commit-config.yaml already exists", fg='yellow'))
112+
click.echo("Use --force to overwrite")
113+
return False
114+
115+
config = {
116+
'repos': [
117+
{
118+
'repo': 'local',
119+
'hooks': [
120+
{
121+
'id': 'goal-validation',
122+
'name': 'Goal validation',
123+
'entry': str(self.goal_hook_script),
124+
'language': 'system',
125+
'always_run': True,
126+
'pass_filenames': False
127+
}
128+
]
129+
}
130+
]
131+
}
132+
133+
with open(self.precommit_file, 'w') as f:
134+
yaml.dump(config, f, default_flow_style=False)
135+
136+
click.echo(click.style(f"✓ Created {self.precommit_file}", fg='green'))
137+
return True
138+
139+
def install_hooks(self, force: bool = False) -> bool:
140+
"""Install Goal pre-commit hooks.
141+
142+
Args:
143+
force: Reinstall even if already installed
144+
145+
Returns:
146+
True if installation succeeded
147+
"""
148+
click.echo(click.style("🔧 Installing Goal pre-commit hooks...", fg='cyan'))
149+
150+
# Install pre-commit if needed
151+
if not self.install_precommit():
152+
return False
153+
154+
# Create hook script
155+
self.create_hook_script()
156+
click.echo(click.style(f"✓ Created hook script", fg='green'))
157+
158+
# Create config
159+
if not self.create_precommit_config(force):
160+
return False
161+
162+
# Install hooks
163+
try:
164+
subprocess.run(['pre-commit', 'install'], check=True, cwd=self.project_dir)
165+
click.echo(click.style("✓ Pre-commit hooks installed", fg='green'))
166+
click.echo()
167+
click.echo(click.style("Hooks will now run before each commit:", fg='cyan'))
168+
click.echo(" • File size validation")
169+
click.echo(" • API token detection")
170+
click.echo(" • Dot folder detection")
171+
return True
172+
except subprocess.CalledProcessError:
173+
click.echo(click.style("✗ Failed to install pre-commit hooks", fg='red'))
174+
return False
175+
176+
def uninstall_hooks(self) -> bool:
177+
"""Uninstall Goal pre-commit hooks.
178+
179+
Returns:
180+
True if uninstallation succeeded
181+
"""
182+
click.echo(click.style("🗑️ Uninstalling Goal pre-commit hooks...", fg='cyan'))
183+
184+
# Uninstall hooks
185+
try:
186+
subprocess.run(['pre-commit', 'uninstall'], check=True, cwd=self.project_dir)
187+
except subprocess.CalledProcessError:
188+
pass # Hooks might not be installed
189+
190+
# Remove config
191+
if self.precommit_file.exists():
192+
self.precommit_file.unlink()
193+
click.echo(click.style(f"✓ Removed {self.precommit_file}", fg='green'))
194+
195+
# Remove hook script
196+
if self.goal_hook_script.exists():
197+
self.goal_hook_script.unlink()
198+
click.echo(click.style(f"✓ Removed hook script", fg='green'))
199+
200+
click.echo(click.style("✓ Pre-commit hooks uninstalled", fg='green'))
201+
return True
202+
203+
def run_validation(self, files: Optional[List[str]] = None) -> bool:
204+
"""Run Goal validation checks.
205+
206+
Args:
207+
files: List of files to validate (defaults to staged files)
208+
209+
Returns:
210+
True if all validations passed
211+
"""
212+
if files is None:
213+
files = get_staged_files()
214+
215+
if not files:
216+
return True
217+
218+
try:
219+
validate_files(files)
220+
return True
221+
except ValidationError as e:
222+
click.echo(click.style(f"✗ Validation Error: {e}", fg='red'))
223+
return False
224+
225+
def run_hooks(self, all_files: bool = False) -> bool:
226+
"""Run pre-commit hooks manually.
227+
228+
Args:
229+
all_files: Run on all files instead of just staged
230+
231+
Returns:
232+
True if all hooks passed
233+
"""
234+
if not self.is_hooks_configured():
235+
click.echo(click.style("⚠ Goal hooks are not installed", fg='yellow'))
236+
click.echo("Run 'goal hooks install' to set them up")
237+
return False
238+
239+
click.echo(click.style("🔍 Running pre-commit hooks...", fg='cyan'))
240+
241+
try:
242+
cmd = ['pre-commit', 'run', '--all-files'] if all_files else ['pre-commit', 'run']
243+
subprocess.run(cmd, check=True, cwd=self.project_dir)
244+
click.echo(click.style("✓ All hooks passed", fg='green'))
245+
return True
246+
except subprocess.CalledProcessError:
247+
click.echo(click.style("✗ Some hooks failed", fg='red'))
248+
return False
249+
250+
def status(self) -> None:
251+
"""Show hooks status."""
252+
click.echo()
253+
click.echo(click.style("Pre-commit Hooks Status:", fg='cyan', bold=True))
254+
click.echo("-" * 30)
255+
256+
# Check pre-commit installation
257+
if self.is_precommit_installed():
258+
result = subprocess.run(['pre-commit', '--version'],
259+
capture_output=True, text=True)
260+
click.echo(f"pre-commit: {click.style(result.stdout.strip(), fg='green')}")
261+
else:
262+
click.echo(f"pre-commit: {click.style('Not installed', fg='red')}")
263+
264+
# Check Goal hooks
265+
if self.is_hooks_configured():
266+
click.echo(f"Goal hooks: {click.style('Configured', fg='green')}")
267+
else:
268+
click.echo(f"Goal hooks: {click.style('Not configured', fg='yellow')}")
269+
270+
271+
def install_hooks(project_dir: Optional[Path] = None, force: bool = False) -> bool:
272+
"""Install Goal pre-commit hooks.
273+
274+
Args:
275+
project_dir: Project directory (defaults to current directory)
276+
force: Reinstall even if already installed
277+
278+
Returns:
279+
True if installation succeeded
280+
"""
281+
manager = HooksManager(project_dir)
282+
return manager.install_hooks(force)
283+
284+
285+
def uninstall_hooks(project_dir: Optional[Path] = None) -> bool:
286+
"""Uninstall Goal pre-commit hooks.
287+
288+
Args:
289+
project_dir: Project directory (defaults to current directory)
290+
291+
Returns:
292+
True if uninstallation succeeded
293+
"""
294+
manager = HooksManager(project_dir)
295+
return manager.uninstall_hooks()
296+
297+
298+
def run_hooks(project_dir: Optional[Path] = None, all_files: bool = False) -> bool:
299+
"""Run pre-commit hooks manually.
300+
301+
Args:
302+
project_dir: Project directory (defaults to current directory)
303+
all_files: Run on all files instead of just staged
304+
305+
Returns:
306+
True if all hooks passed
307+
"""
308+
manager = HooksManager(project_dir)
309+
return manager.run_hooks(all_files)

goal/recovery/strategies.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ def recover(self, error_output: str) -> bool:
131131
class LargeFileStrategy(RecoveryStrategy):
132132
"""Handles large file errors."""
133133

134-
def __init__(self, repo_path: str):
135-
super().__init__(repo_path)
134+
def __init__(self, repo_path: str, config=None):
135+
super().__init__(repo_path, config)
136136
self.last_error = None
137137

138138
def can_handle(self, error_output: str) -> bool:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "goal"
7-
version = "2.1.109"
7+
version = "2.1.110"
88
description = "Goal - Automated git push with enterprise-grade commit intelligence, smart conventional commit generation based on deep code analysis, and interactive release workflow management."
99
readme = "README.md"
1010
license = "Apache-2.0"

0 commit comments

Comments
 (0)