diffron 0.1.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.
- diffron/__init__.py +26 -0
- diffron/__init__.pyi +28 -0
- diffron/cli.py +347 -0
- diffron/cli.pyi +30 -0
- diffron/client.py +175 -0
- diffron/client.pyi +80 -0
- diffron/commit_gen.py +151 -0
- diffron/commit_gen.pyi +41 -0
- diffron/git_hooks.py +292 -0
- diffron/git_hooks.pyi +52 -0
- diffron/lemonade.py +192 -0
- diffron/lemonade.pyi +66 -0
- diffron/pr_gen.py +224 -0
- diffron/pr_gen.pyi +56 -0
- diffron/py.typed +0 -0
- diffron/utils.py +236 -0
- diffron/utils.pyi +58 -0
- diffron-0.1.0.dist-info/METADATA +373 -0
- diffron-0.1.0.dist-info/RECORD +28 -0
- diffron-0.1.0.dist-info/WHEEL +5 -0
- diffron-0.1.0.dist-info/entry_points.txt +5 -0
- diffron-0.1.0.dist-info/licenses/LICENSE +21 -0
- diffron-0.1.0.dist-info/top_level.txt +2 -0
- docs/HOOKS.md +442 -0
- docs/PLAN.md +378 -0
- docs/PYPI_RELEASE.md +487 -0
- docs/SETUP.md +524 -0
- docs/USAGE.md +515 -0
diffron/commit_gen.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Commit message generation for Diffron.
|
|
3
|
+
|
|
4
|
+
Generates Conventional Commits format messages from git diffs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from .lemonade import LemonadeClient
|
|
11
|
+
from .utils import get_staged_diff
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Conventional Commits types
|
|
15
|
+
COMMIT_TYPES = [
|
|
16
|
+
"feat", # New feature
|
|
17
|
+
"fix", # Bug fix
|
|
18
|
+
"docs", # Documentation changes
|
|
19
|
+
"style", # Code style changes (formatting, etc.)
|
|
20
|
+
"refactor", # Code refactoring
|
|
21
|
+
"perf", # Performance improvements
|
|
22
|
+
"test", # Test additions/modifications
|
|
23
|
+
"build", # Build system/external dependencies
|
|
24
|
+
"ci", # CI configuration
|
|
25
|
+
"chore", # Other changes
|
|
26
|
+
"revert", # Revert previous commit
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
DEFAULT_MAX_CHARS = 4000
|
|
30
|
+
DEFAULT_MAX_TOKENS = 100
|
|
31
|
+
DEFAULT_TEMPERATURE = 0.2
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def generate_commit_message(
|
|
35
|
+
diff: Optional[str] = None,
|
|
36
|
+
max_chars: Optional[int] = None,
|
|
37
|
+
max_tokens: Optional[int] = None,
|
|
38
|
+
temperature: Optional[float] = None,
|
|
39
|
+
client: Optional[LemonadeClient] = None,
|
|
40
|
+
) -> str:
|
|
41
|
+
"""
|
|
42
|
+
Generate a commit message from a git diff.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
diff: Git diff string. If None, gets staged diff automatically.
|
|
46
|
+
max_chars: Maximum characters of diff to send. Defaults to 4000.
|
|
47
|
+
max_tokens: Maximum tokens to generate. Defaults to 100.
|
|
48
|
+
temperature: Sampling temperature. Defaults to 0.2.
|
|
49
|
+
client: LemonadeClient instance. Creates new one if not provided.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Generated commit message in Conventional Commits format.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If diff is empty.
|
|
56
|
+
ConnectionError: If Lemonade is not running.
|
|
57
|
+
"""
|
|
58
|
+
max_chars = max_chars or int(os.environ.get("DIFFRON_MAX_DIFF_CHARS", DEFAULT_MAX_CHARS))
|
|
59
|
+
max_tokens = max_tokens or DEFAULT_MAX_TOKENS
|
|
60
|
+
temperature = temperature if temperature is not None else DEFAULT_TEMPERATURE
|
|
61
|
+
|
|
62
|
+
# Get diff if not provided
|
|
63
|
+
if diff is None:
|
|
64
|
+
diff = get_staged_diff(max_chars=max_chars)
|
|
65
|
+
|
|
66
|
+
if not diff.strip():
|
|
67
|
+
raise ValueError("No staged changes to generate commit message from.")
|
|
68
|
+
|
|
69
|
+
# Create client if not provided
|
|
70
|
+
if client is None:
|
|
71
|
+
client = LemonadeClient()
|
|
72
|
+
|
|
73
|
+
# Build prompt
|
|
74
|
+
commit_types = ", ".join(COMMIT_TYPES)
|
|
75
|
+
prompt = (
|
|
76
|
+
f"Write a concise Git commit message in Conventional Commits format. "
|
|
77
|
+
f"Use one of these types: {commit_types}. "
|
|
78
|
+
f"Format: 'type: description' (e.g., 'feat: add user authentication'). "
|
|
79
|
+
f"Output ONLY the commit message, nothing else. No quotes, no explanations.\n\n"
|
|
80
|
+
f"Diff:\n{diff}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
messages = [{"role": "user", "content": prompt}]
|
|
84
|
+
|
|
85
|
+
# Generate completion
|
|
86
|
+
response = client.chat_completion(
|
|
87
|
+
messages=messages,
|
|
88
|
+
max_tokens=max_tokens,
|
|
89
|
+
temperature=temperature,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Clean up response
|
|
93
|
+
commit_message = response.strip()
|
|
94
|
+
|
|
95
|
+
# Remove any surrounding quotes
|
|
96
|
+
if commit_message.startswith('"') and commit_message.endswith('"'):
|
|
97
|
+
commit_message = commit_message[1:-1]
|
|
98
|
+
if commit_message.startswith("'") and commit_message.endswith("'"):
|
|
99
|
+
commit_message = commit_message[1:-1]
|
|
100
|
+
|
|
101
|
+
# Remove markdown code blocks if present
|
|
102
|
+
if commit_message.startswith("```"):
|
|
103
|
+
lines = commit_message.split("\n")
|
|
104
|
+
commit_message = "\n".join(
|
|
105
|
+
line for line in lines
|
|
106
|
+
if not line.startswith("```")
|
|
107
|
+
).strip()
|
|
108
|
+
|
|
109
|
+
return commit_message
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def validate_commit_type(message: str) -> bool:
|
|
113
|
+
"""
|
|
114
|
+
Validate that a commit message starts with a valid Conventional Commits type.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
message: Commit message to validate.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True if valid, False otherwise.
|
|
121
|
+
"""
|
|
122
|
+
for commit_type in COMMIT_TYPES:
|
|
123
|
+
if message.lower().startswith(f"{commit_type}:"):
|
|
124
|
+
return True
|
|
125
|
+
if message.lower().startswith(f"{commit_type}("): # With scope
|
|
126
|
+
return True
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def format_commit_message(
|
|
131
|
+
commit_type: str,
|
|
132
|
+
description: str,
|
|
133
|
+
scope: Optional[str] = None,
|
|
134
|
+
breaking: bool = False,
|
|
135
|
+
) -> str:
|
|
136
|
+
"""
|
|
137
|
+
Format a commit message with proper Conventional Commits structure.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
commit_type: Type of commit (feat, fix, etc.).
|
|
141
|
+
description: Short description of the change.
|
|
142
|
+
scope: Optional scope (e.g., 'api', 'ui').
|
|
143
|
+
breaking: Whether this is a breaking change.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Formatted commit message.
|
|
147
|
+
"""
|
|
148
|
+
scope_part = f"({scope})" if scope else ""
|
|
149
|
+
breaking_part = "!" if breaking else ""
|
|
150
|
+
|
|
151
|
+
return f"{commit_type}{scope_part}{breaking_part}: {description}"
|
diffron/commit_gen.pyi
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Commit message generation for Diffron.
|
|
3
|
+
|
|
4
|
+
Generates Conventional Commits format messages from git diffs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from .lemonade import LemonadeClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
COMMIT_TYPES: list[str]
|
|
12
|
+
|
|
13
|
+
DEFAULT_MAX_CHARS: int
|
|
14
|
+
DEFAULT_MAX_TOKENS: int
|
|
15
|
+
DEFAULT_TEMPERATURE: float
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def generate_commit_message(
|
|
19
|
+
diff: Optional[str] = None,
|
|
20
|
+
max_chars: Optional[int] = None,
|
|
21
|
+
max_tokens: Optional[int] = None,
|
|
22
|
+
temperature: Optional[float] = None,
|
|
23
|
+
client: Optional[LemonadeClient] = None,
|
|
24
|
+
) -> str:
|
|
25
|
+
"""Generate a commit message from a git diff."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_commit_type(message: str) -> bool:
|
|
30
|
+
"""Validate that a commit message starts with a valid Conventional Commits type."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def format_commit_message(
|
|
35
|
+
commit_type: str,
|
|
36
|
+
description: str,
|
|
37
|
+
scope: Optional[str] = None,
|
|
38
|
+
breaking: bool = False,
|
|
39
|
+
) -> str:
|
|
40
|
+
"""Format a commit message with proper Conventional Commits structure."""
|
|
41
|
+
...
|
diffron/git_hooks.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git hooks installation and management for Diffron.
|
|
3
|
+
|
|
4
|
+
Provides functions to install, uninstall, and verify Diffron Git hooks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import stat
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from .utils import get_git_dir, is_git_repo
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Hook file names
|
|
17
|
+
WRAPPER_NAME = "prepare-commit-msg"
|
|
18
|
+
PYTHON_HOOK_NAME = "prepare-commit-msg.py"
|
|
19
|
+
|
|
20
|
+
# Get the hooks directory (where the template hooks are stored)
|
|
21
|
+
DIFFRON_PACKAGE_DIR = Path(__file__).parent
|
|
22
|
+
HOOKS_TEMPLATE_DIR = DIFFRON_PACKAGE_DIR.parent / "hooks"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def install_hooks(
|
|
26
|
+
repo_path: str = ".",
|
|
27
|
+
global_install: bool = False,
|
|
28
|
+
) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Install Diffron Git hooks to a repository.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
repo_path: Path to the git repository. Defaults to current directory.
|
|
34
|
+
global_install: If True, install hooks globally for all repositories.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if installation was successful, False otherwise.
|
|
38
|
+
"""
|
|
39
|
+
if global_install:
|
|
40
|
+
return _install_global_hooks()
|
|
41
|
+
|
|
42
|
+
if not is_git_repo(repo_path):
|
|
43
|
+
raise ValueError(f"Not a git repository: {repo_path}")
|
|
44
|
+
|
|
45
|
+
git_dir = get_git_dir(repo_path)
|
|
46
|
+
if git_dir is None:
|
|
47
|
+
raise ValueError(f"Could not find .git directory in: {repo_path}")
|
|
48
|
+
|
|
49
|
+
hooks_dir = Path(git_dir) / "hooks"
|
|
50
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
# Copy wrapper script
|
|
53
|
+
wrapper_src = HOOKS_TEMPLATE_DIR / WRAPPER_NAME
|
|
54
|
+
wrapper_dst = hooks_dir / WRAPPER_NAME
|
|
55
|
+
|
|
56
|
+
if wrapper_src.exists():
|
|
57
|
+
shutil.copy2(wrapper_src, wrapper_dst)
|
|
58
|
+
_make_executable(wrapper_dst)
|
|
59
|
+
|
|
60
|
+
# Copy Python hook script
|
|
61
|
+
python_hook_src = HOOKS_TEMPLATE_DIR / PYTHON_HOOK_NAME
|
|
62
|
+
python_hook_dst = hooks_dir / PYTHON_HOOK_NAME
|
|
63
|
+
|
|
64
|
+
if python_hook_src.exists():
|
|
65
|
+
shutil.copy2(python_hook_src, python_hook_dst)
|
|
66
|
+
_make_executable(python_hook_dst)
|
|
67
|
+
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _install_global_hooks() -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Install hooks globally using git config core.hooksPath.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if installation was successful, False otherwise.
|
|
77
|
+
"""
|
|
78
|
+
import subprocess
|
|
79
|
+
|
|
80
|
+
# Create global hooks directory in user's home
|
|
81
|
+
home_dir = Path.home()
|
|
82
|
+
global_hooks_dir = home_dir / ".diffron-hooks"
|
|
83
|
+
global_hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
|
|
85
|
+
# Copy hook files
|
|
86
|
+
wrapper_src = HOOKS_TEMPLATE_DIR / WRAPPER_NAME
|
|
87
|
+
|
|
88
|
+
# For global install, use the simplified hook that imports from installed package
|
|
89
|
+
python_hook_src = HOOKS_TEMPLATE_DIR / "prepare-commit-msg-global.py"
|
|
90
|
+
if not python_hook_src.exists():
|
|
91
|
+
# Fallback to regular hook if global version doesn't exist
|
|
92
|
+
python_hook_src = HOOKS_TEMPLATE_DIR / PYTHON_HOOK_NAME
|
|
93
|
+
|
|
94
|
+
wrapper_dst = global_hooks_dir / WRAPPER_NAME
|
|
95
|
+
python_hook_dst = global_hooks_dir / PYTHON_HOOK_NAME
|
|
96
|
+
|
|
97
|
+
if wrapper_src.exists():
|
|
98
|
+
shutil.copy2(wrapper_src, wrapper_dst)
|
|
99
|
+
_make_executable(wrapper_dst)
|
|
100
|
+
|
|
101
|
+
if python_hook_src.exists():
|
|
102
|
+
shutil.copy2(python_hook_src, python_hook_dst)
|
|
103
|
+
_make_executable(python_hook_dst)
|
|
104
|
+
|
|
105
|
+
# Configure git to use global hooks path
|
|
106
|
+
hooks_path_str = str(global_hooks_dir).replace("\\", "/")
|
|
107
|
+
try:
|
|
108
|
+
subprocess.run(
|
|
109
|
+
["git", "config", "--global", "core.hooksPath", hooks_path_str],
|
|
110
|
+
check=True,
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True,
|
|
113
|
+
timeout=10,
|
|
114
|
+
)
|
|
115
|
+
return True
|
|
116
|
+
except subprocess.SubprocessError:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def uninstall_hooks(
|
|
121
|
+
repo_path: str = ".",
|
|
122
|
+
global_install: bool = False,
|
|
123
|
+
) -> bool:
|
|
124
|
+
"""
|
|
125
|
+
Remove Diffron Git hooks from a repository.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
repo_path: Path to the git repository. Defaults to current directory.
|
|
129
|
+
global_install: If True, remove global hooks configuration.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
True if uninstallation was successful, False otherwise.
|
|
133
|
+
"""
|
|
134
|
+
import subprocess
|
|
135
|
+
|
|
136
|
+
if global_install:
|
|
137
|
+
# Remove global hooks configuration
|
|
138
|
+
try:
|
|
139
|
+
subprocess.run(
|
|
140
|
+
["git", "config", "--global", "--unset", "core.hooksPath"],
|
|
141
|
+
check=True,
|
|
142
|
+
capture_output=True,
|
|
143
|
+
text=True,
|
|
144
|
+
timeout=10,
|
|
145
|
+
)
|
|
146
|
+
except subprocess.SubprocessError:
|
|
147
|
+
pass # Config might not be set
|
|
148
|
+
|
|
149
|
+
# Remove global hooks directory
|
|
150
|
+
home_dir = Path.home()
|
|
151
|
+
global_hooks_dir = home_dir / ".diffron-hooks"
|
|
152
|
+
if global_hooks_dir.exists():
|
|
153
|
+
try:
|
|
154
|
+
shutil.rmtree(global_hooks_dir)
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
if not is_git_repo(repo_path):
|
|
160
|
+
raise ValueError(f"Not a git repository: {repo_path}")
|
|
161
|
+
|
|
162
|
+
git_dir = get_git_dir(repo_path)
|
|
163
|
+
if git_dir is None:
|
|
164
|
+
raise ValueError(f"Could not find .git directory in: {repo_path}")
|
|
165
|
+
|
|
166
|
+
hooks_dir = Path(git_dir) / "hooks"
|
|
167
|
+
|
|
168
|
+
# Remove hook files
|
|
169
|
+
wrapper_path = hooks_dir / WRAPPER_NAME
|
|
170
|
+
python_hook_path = hooks_dir / PYTHON_HOOK_NAME
|
|
171
|
+
|
|
172
|
+
removed = False
|
|
173
|
+
|
|
174
|
+
if wrapper_path.exists():
|
|
175
|
+
wrapper_path.unlink()
|
|
176
|
+
removed = True
|
|
177
|
+
|
|
178
|
+
if python_hook_path.exists():
|
|
179
|
+
python_hook_path.unlink()
|
|
180
|
+
removed = True
|
|
181
|
+
|
|
182
|
+
return removed
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def is_hooks_installed(
|
|
186
|
+
repo_path: str = ".",
|
|
187
|
+
check_global: bool = False,
|
|
188
|
+
) -> bool:
|
|
189
|
+
"""
|
|
190
|
+
Check if Diffron hooks are installed.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
repo_path: Path to the git repository. Defaults to current directory.
|
|
194
|
+
check_global: If True, check for global hooks configuration.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
True if hooks are installed, False otherwise.
|
|
198
|
+
"""
|
|
199
|
+
import subprocess
|
|
200
|
+
|
|
201
|
+
if check_global:
|
|
202
|
+
# Check global hooks configuration
|
|
203
|
+
try:
|
|
204
|
+
result = subprocess.run(
|
|
205
|
+
["git", "config", "--global", "core.hooksPath"],
|
|
206
|
+
capture_output=True,
|
|
207
|
+
text=True,
|
|
208
|
+
timeout=10,
|
|
209
|
+
)
|
|
210
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
211
|
+
hooks_path = result.stdout.strip()
|
|
212
|
+
# Check if it's the diffron hooks directory
|
|
213
|
+
if ".diffron-hooks" in hooks_path:
|
|
214
|
+
return True
|
|
215
|
+
except subprocess.SubprocessError:
|
|
216
|
+
pass
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
if not is_git_repo(repo_path):
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
git_dir = get_git_dir(repo_path)
|
|
223
|
+
if git_dir is None:
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
hooks_dir = Path(git_dir) / "hooks"
|
|
227
|
+
|
|
228
|
+
# Check if both hook files exist
|
|
229
|
+
wrapper_path = hooks_dir / WRAPPER_NAME
|
|
230
|
+
python_hook_path = hooks_dir / PYTHON_HOOK_NAME
|
|
231
|
+
|
|
232
|
+
return wrapper_path.exists() and python_hook_path.exists()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _make_executable(path: Path) -> None:
|
|
236
|
+
"""
|
|
237
|
+
Make a file executable.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
path: Path to the file.
|
|
241
|
+
"""
|
|
242
|
+
if os.name == "nt":
|
|
243
|
+
# Windows doesn't use Unix permissions, but we can still set the flag
|
|
244
|
+
pass
|
|
245
|
+
else:
|
|
246
|
+
# Unix-like systems
|
|
247
|
+
current_mode = path.stat().st_mode
|
|
248
|
+
path.chmod(current_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def get_hooks_status(repo_path: str = ".") -> dict:
|
|
252
|
+
"""
|
|
253
|
+
Get detailed status of hooks installation.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
repo_path: Path to the git repository.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Dict with status information.
|
|
260
|
+
"""
|
|
261
|
+
status = {
|
|
262
|
+
"is_git_repo": is_git_repo(repo_path),
|
|
263
|
+
"local_hooks_installed": False,
|
|
264
|
+
"global_hooks_configured": False,
|
|
265
|
+
"wrapper_exists": False,
|
|
266
|
+
"python_hook_exists": False,
|
|
267
|
+
"git_dir": None,
|
|
268
|
+
"hooks_dir": None,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if status["is_git_repo"]:
|
|
272
|
+
git_dir = get_git_dir(repo_path)
|
|
273
|
+
status["git_dir"] = str(git_dir) if git_dir else None
|
|
274
|
+
|
|
275
|
+
if git_dir:
|
|
276
|
+
hooks_dir = Path(git_dir) / "hooks"
|
|
277
|
+
status["hooks_dir"] = str(hooks_dir)
|
|
278
|
+
|
|
279
|
+
wrapper_path = hooks_dir / WRAPPER_NAME
|
|
280
|
+
python_hook_path = hooks_dir / PYTHON_HOOK_NAME
|
|
281
|
+
|
|
282
|
+
status["wrapper_exists"] = wrapper_path.exists()
|
|
283
|
+
status["python_hook_exists"] = python_hook_path.exists()
|
|
284
|
+
status["local_hooks_installed"] = (
|
|
285
|
+
status["wrapper_exists"] and status["python_hook_exists"]
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
status["global_hooks_configured"] = is_hooks_installed(
|
|
289
|
+
repo_path, check_global=True
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
return status
|
diffron/git_hooks.pyi
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git hooks installation and management for Diffron.
|
|
3
|
+
|
|
4
|
+
Provides functions to install, uninstall, and verify Diffron Git hooks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
WRAPPER_NAME: str
|
|
12
|
+
PYTHON_HOOK_NAME: str
|
|
13
|
+
HOOKS_TEMPLATE_DIR: Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def install_hooks(
|
|
17
|
+
repo_path: str = ".",
|
|
18
|
+
global_install: bool = False,
|
|
19
|
+
) -> bool:
|
|
20
|
+
"""Install Diffron Git hooks to a repository."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _install_global_hooks() -> bool:
|
|
25
|
+
"""Install hooks globally using git config core.hooksPath."""
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def uninstall_hooks(
|
|
30
|
+
repo_path: str = ".",
|
|
31
|
+
global_install: bool = False,
|
|
32
|
+
) -> bool:
|
|
33
|
+
"""Remove Diffron Git hooks from a repository."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_hooks_installed(
|
|
38
|
+
repo_path: str = ".",
|
|
39
|
+
check_global: bool = False,
|
|
40
|
+
) -> bool:
|
|
41
|
+
"""Check if Diffron hooks are installed."""
|
|
42
|
+
...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _make_executable(path: Path) -> None:
|
|
46
|
+
"""Make a file executable."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_hooks_status(repo_path: str = ".") -> dict:
|
|
51
|
+
"""Get detailed status of hooks installation."""
|
|
52
|
+
...
|