0
# Version Control Hooks
1
2
Pre-commit and post-commit hooks for Git and Mercurial integration. Pylama provides automated code quality checking as part of your version control workflow to catch issues before they enter the repository.
3
4
## Capabilities
5
6
### Git Integration
7
8
Git pre-commit hook implementation that checks staged files before allowing commits.
9
10
```python { .api }
11
def git_hook(error: bool = True):
12
"""
13
Git pre-commit hook implementation.
14
15
Args:
16
error: Whether to exit with error code if issues are found
17
18
This function:
19
- Gets list of staged files using 'git diff-index --cached --name-only HEAD'
20
- Filters for Python files (.py extension)
21
- Runs pylama checks on staged files only
22
- Displays any errors found
23
- Exits with error code to prevent commit if issues found and error=True
24
25
Usage in pre-commit hook:
26
#!/usr/bin/env python
27
import sys
28
from pylama.hook import git_hook
29
30
if __name__ == '__main__':
31
sys.exit(git_hook())
32
"""
33
```
34
35
### Mercurial Integration
36
37
Mercurial commit hook implementation for post-commit quality checking.
38
39
```python { .api }
40
def hg_hook(_, repo, node=None, **kwargs):
41
"""
42
Mercurial commit hook implementation.
43
44
Args:
45
_: Unused first argument (Mercurial convention)
46
repo: Mercurial repository object
47
node: Starting revision node for the commit range
48
**kwargs: Additional keyword arguments from Mercurial
49
50
This function:
51
- Iterates through committed revisions from node to tip
52
- Collects all modified files in the commit range
53
- Filters for existing Python files
54
- Runs pylama checks on modified files
55
- Displays errors and exits with error code if issues found
56
57
Configured in .hg/hgrc:
58
[hooks]
59
commit = python:pylama.hook.hg_hook
60
qrefresh = python:pylama.hook.hg_hook
61
"""
62
```
63
64
### Hook Installation
65
66
Automatic hook installation with VCS detection and setup.
67
68
```python { .api }
69
def install_hook(path: str):
70
"""
71
Auto-detect VCS and install appropriate hook.
72
73
Args:
74
path: Repository root path to install hooks in
75
76
Detection logic:
77
- Checks for .git/hooks directory -> installs Git hook
78
- Checks for .hg directory -> installs Mercurial hook
79
- Exits with error if no supported VCS found
80
81
This function provides a convenient way to set up hooks without
82
manual VCS-specific configuration.
83
"""
84
85
def install_git(path: str):
86
"""
87
Install Git pre-commit hook.
88
89
Args:
90
path: Path to .git/hooks directory
91
92
Creates executable pre-commit hook script that:
93
- Imports and calls git_hook() function
94
- Has proper shebang and permissions
95
- Prevents commits when code quality issues are found
96
"""
97
98
def install_hg(path: str):
99
"""
100
Install Mercurial commit hook.
101
102
Args:
103
path: Path to .hg directory
104
105
Modifies .hg/hgrc configuration file to:
106
- Add [hooks] section if not present
107
- Configure commit and qrefresh hooks
108
- Point to pylama.hook.hg_hook function
109
"""
110
```
111
112
### Shell Command Execution
113
114
Utility function for running shell commands from hooks.
115
116
```python { .api }
117
def run(command: str) -> Tuple[int, List[bytes], List[bytes]]:
118
"""
119
Run shell command and capture output.
120
121
Args:
122
command: Shell command string to execute
123
124
Returns:
125
tuple: (return_code, stdout_lines, stderr_lines)
126
- return_code: Exit code from command
127
- stdout_lines: List of stdout lines as bytes
128
- stderr_lines: List of stderr lines as bytes
129
130
Used internally by hooks to execute Git/Mercurial commands
131
for file discovery and status checking.
132
"""
133
```
134
135
## Installation and Setup
136
137
### Command Line Installation
138
139
```python
140
# Install hooks via command line
141
import subprocess
142
143
# For Git repositories
144
subprocess.run(['pylama', '--hook', '/path/to/git/repo'])
145
146
# For Mercurial repositories
147
subprocess.run(['pylama', '--hook', '/path/to/hg/repo'])
148
```
149
150
### Programmatic Installation
151
152
```python
153
from pylama.hook import install_hook, install_git, install_hg
154
155
# Auto-detect and install
156
install_hook('/path/to/repository')
157
158
# Install specific VCS hooks
159
install_git('/path/to/repo/.git/hooks')
160
install_hg('/path/to/repo/.hg')
161
```
162
163
### Manual Git Hook Setup
164
165
```bash
166
#!/usr/bin/env python
167
# .git/hooks/pre-commit
168
169
import sys
170
from pylama.hook import git_hook
171
172
if __name__ == '__main__':
173
sys.exit(git_hook())
174
```
175
176
```bash
177
# Make hook executable
178
chmod +x .git/hooks/pre-commit
179
```
180
181
### Manual Mercurial Hook Setup
182
183
```ini
184
# .hg/hgrc
185
[hooks]
186
commit = python:pylama.hook.hg_hook
187
qrefresh = python:pylama.hook.hg_hook
188
```
189
190
## Usage Examples
191
192
### Basic Hook Usage
193
194
```python
195
from pylama.hook import git_hook
196
197
# Run git hook manually (for testing)
198
try:
199
git_hook(error=False) # Don't exit on errors
200
print("All staged files pass quality checks")
201
except SystemExit as e:
202
if e.code != 0:
203
print("Quality issues found in staged files")
204
```
205
206
### Custom Hook Configuration
207
208
```python
209
import os
210
from pylama.hook import git_hook
211
from pylama.config import parse_options
212
213
def custom_git_hook():
214
"""Custom git hook with specific configuration."""
215
216
# Set custom pylama configuration
217
os.environ['PYLAMA_CONFIG'] = 'hooks/pylama.ini'
218
219
# Run hook with custom settings
220
git_hook()
221
222
# Custom configuration file (hooks/pylama.ini)
223
hook_config = """
224
[pylama]
225
linters = pycodestyle,pyflakes
226
ignore = E501,W503
227
format = parsable
228
"""
229
```
230
231
### Conditional Hook Execution
232
233
```python
234
import os
235
from pylama.hook import git_hook
236
237
def conditional_git_hook():
238
"""Run hook only in certain conditions."""
239
240
# Skip hook in CI environment
241
if os.getenv('CI') == 'true':
242
print("Skipping pylama hook in CI environment")
243
return 0
244
245
# Skip hook for merge commits
246
if os.path.exists('.git/MERGE_HEAD'):
247
print("Skipping pylama hook for merge commit")
248
return 0
249
250
# Run normal hook
251
return git_hook()
252
```
253
254
### Hook with Custom File Filtering
255
256
```python
257
import subprocess
258
from pylama.hook import run
259
from pylama.main import check_paths
260
from pylama.config import parse_options
261
262
def selective_git_hook():
263
"""Git hook that only checks certain file patterns."""
264
265
# Get staged files
266
_, files_modified, _ = run("git diff-index --cached --name-only HEAD")
267
268
# Filter for specific patterns
269
python_files = []
270
for file_bytes in files_modified:
271
filename = file_bytes.decode('utf-8')
272
if filename.endswith('.py') and not filename.startswith('tests/'):
273
python_files.append(filename)
274
275
if not python_files:
276
return 0
277
278
# Check filtered files
279
options = parse_options(['--linters=pycodestyle,pyflakes'])
280
errors = check_paths(python_files, options)
281
282
if errors:
283
for error in errors:
284
print(f"{error.filename}:{error.lnum} - {error.message}")
285
return 1
286
287
return 0
288
```
289
290
### Hook Integration with CI/CD
291
292
```python
293
import json
294
from pylama.hook import git_hook
295
from pylama.main import check_paths
296
from pylama.config import parse_options
297
298
def ci_hook():
299
"""Hook that generates CI-friendly output."""
300
301
try:
302
# Configure for JSON output
303
options = parse_options(['--format=json'])
304
305
# Get modified files (in CI, check all files)
306
errors = check_paths(['.'], options)
307
308
if errors:
309
# Output for CI system
310
error_data = [error.to_dict() for error in errors]
311
with open('pylama-results.json', 'w') as f:
312
json.dump(error_data, f, indent=2)
313
314
print(f"Found {len(errors)} code quality issues")
315
return 1
316
317
print("All files pass code quality checks")
318
return 0
319
320
except Exception as e:
321
print(f"Hook execution failed: {e}")
322
return 1
323
```
324
325
### Pre-push Hook
326
327
```python
328
import sys
329
from pylama.main import check_paths
330
from pylama.config import parse_options
331
332
def pre_push_hook():
333
"""Check all files before pushing to remote."""
334
335
print("Running code quality checks before push...")
336
337
# Check entire codebase
338
options = parse_options([
339
'--linters=pycodestyle,pyflakes,mccabe',
340
'--ignore=E501',
341
'.'
342
])
343
344
errors = check_paths(['.'], options)
345
346
if errors:
347
print(f"\nFound {len(errors)} code quality issues:")
348
for error in errors[:10]: # Show first 10 errors
349
print(f" {error.filename}:{error.lnum} - {error.message}")
350
351
if len(errors) > 10:
352
print(f" ... and {len(errors) - 10} more issues")
353
354
print("\nPush aborted. Fix issues before pushing.")
355
return 1
356
357
print("All files pass code quality checks. Push allowed.")
358
return 0
359
360
if __name__ == '__main__':
361
sys.exit(pre_push_hook())
362
```
363
364
### Hook Bypass Mechanism
365
366
```python
367
import os
368
import sys
369
from pylama.hook import git_hook
370
371
def bypassable_git_hook():
372
"""Git hook that can be bypassed with environment variable."""
373
374
# Check for bypass flag
375
if os.getenv('PYLAMA_SKIP_HOOK') == '1':
376
print("Pylama hook bypassed via PYLAMA_SKIP_HOOK")
377
return 0
378
379
# Check for --no-verify in git commit command
380
if '--no-verify' in ' '.join(sys.argv):
381
print("Pylama hook bypassed via --no-verify")
382
return 0
383
384
# Run normal hook
385
return git_hook()
386
387
# Usage: PYLAMA_SKIP_HOOK=1 git commit -m "emergency fix"
388
# Usage: git commit --no-verify -m "bypass hook"
389
```
390
391
### Hook Performance Optimization
392
393
```python
394
import time
395
from pylama.hook import run, git_hook
396
from pylama.main import check_paths
397
from pylama.config import parse_options
398
399
def fast_git_hook():
400
"""Optimized git hook for large repositories."""
401
402
start_time = time.time()
403
404
# Get only staged Python files
405
_, files_modified, _ = run("git diff-index --cached --name-only HEAD")
406
python_files = [
407
f.decode('utf-8') for f in files_modified
408
if f.decode('utf-8').endswith('.py')
409
]
410
411
if not python_files:
412
return 0
413
414
# Use fast linters only
415
options = parse_options([
416
'--linters=pycodestyle,pyflakes', # Skip slow linters
417
'--async', # Use parallel processing
418
'--ignore=E501,W503' # Ignore non-critical issues
419
])
420
421
errors = check_paths(python_files, options)
422
423
elapsed = time.time() - start_time
424
print(f"Hook completed in {elapsed:.2f}s")
425
426
if errors:
427
print(f"Found {len(errors)} issues in staged files")
428
for error in errors:
429
print(f" {error.filename}:{error.lnum} - {error.message}")
430
return 1
431
432
return 0
433
```
434
435
## Hook Configuration
436
437
### Repository-specific Settings
438
439
```ini
440
# .pylama.ini (in repository root)
441
[pylama]
442
# Hook-specific configuration
443
linters = pycodestyle,pyflakes
444
ignore = E501,W503,E203
445
skip = migrations/*,venv/*
446
format = parsable
447
448
# Different settings for hooks vs manual runs
449
[pylama:hook]
450
linters = pycodestyle,pyflakes # Faster linters for hooks
451
max_line_length = 100
452
```
453
454
### Global Hook Settings
455
456
```ini
457
# ~/.pylama.ini (global configuration)
458
[pylama]
459
# Global defaults for all projects
460
ignore = E501,W503
461
format = parsable
462
463
[pylama:hook]
464
# Specific settings when running from hooks
465
async = 1
466
concurrent = 1
467
```
468
469
### Environment Variables
470
471
```bash
472
# Hook-specific environment variables
473
export PYLAMA_CONFIG="/path/to/hook-config.ini"
474
export PYLAMA_SKIP_HOOK="0"
475
export PYLAMA_HOOK_TIMEOUT="30"
476
```