Skip to content

Commit b18a099

Browse files
committed
feat: add Django smart test detection and configuration options for pre-commit and pre-push hooks
1 parent aab417d commit b18a099

7 files changed

Lines changed: 295 additions & 24 deletions

File tree

.dev-hooks.example.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ pre-commit:
1212
# Supports multiple patterns: "*.py, *.js"
1313
# only_for_files: "*.py"
1414

15+
# Django smart test detection (default: true)
16+
# Warns if modified Python files don't have corresponding tests
17+
# This is just a warning, it won't block the commit
18+
django_check_tests: true
19+
1520
# Commands to run before commit
1621
commands:
1722
# Example: Run linter
@@ -41,9 +46,18 @@ pre-push:
4146
# Supports multiple patterns separated by comma: "*.py, *.js"
4247
only_for_files: "*.py"
4348

49+
# Django smart tests (default: true)
50+
# When enabled, runs tests only for modified Django apps instead of all tests
51+
# Detects which apps were modified and runs: pytest <app1> <app2> ...
52+
django_smart_tests: true
53+
54+
# Test command for Django smart tests (default: "pytest")
55+
django_test_command: "pytest"
56+
4457
# Commands to run before push
4558
commands:
4659
# Example: Run tests (only if .py files changed)
60+
# Note: If django_smart_tests is enabled, this will be replaced with smart test execution
4761
- name: "Run Tests"
4862
run: "pytest"
4963

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.0] - 2025-12-19
9+
10+
### Added
11+
- **Django Smart Tests**: Automatically run tests only for modified Django apps/modules instead of the entire test suite
12+
- `pre-push.django_smart_tests`: Enable/disable smart test detection (default: true)
13+
- `pre-push.django_test_command`: Configure test command (default: "pytest")
14+
- **Django Test Detection**: Warn about modified Python files without corresponding tests
15+
- `pre-commit.django_check_tests`: Enable/disable test detection warnings (default: true)
16+
- Shows warning message but doesn't block the commit
17+
- **Tag Push Support**: Allow pushing git tags without branch validation in pre-push hook
18+
- **Detached HEAD Support**: Allow push operations when in detached HEAD state
19+
20+
### Changed
21+
- ClickUp ID pattern in commit-msg is now disabled by default (kept commented for future use)
22+
- Simplified commit message format examples (removed ClickUp references from error messages)
23+
824
## [1.0.0] - 2025-12-11
925

1026
### Added

README.md

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ Git hooks for development workflow automation.
44

55
## Features
66

7-
- **commit-msg**: Validates commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) format (with optional ClickUp ID)
7+
- **commit-msg**: Validates commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) format
88
- **pre-commit**: Runs custom commands (lint, type check, format) from config file
99
- **pre-push**: Validates branch naming + runs custom commands (tests) from config file
10+
- **Django Smart Tests**: Run tests only for modified apps/modules instead of all tests
11+
- **Test Detection**: Warns about modified files without corresponding tests
1012
- **File filtering**: Only run commands when specific file types are changed
13+
- **Tag Support**: Allows pushing git tags without validation
1114

1215
## Installation
1316

@@ -63,32 +66,35 @@ Validates that commit messages follow Conventional Commits format:
6366

6467
```
6568
<type>(<optional-scope>): <description>
66-
CU-xxxxxxxxx - <type>(<optional-scope>): <description>
6769
```
6870

6971
Valid types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`
7072

7173
Examples:
7274
- `feat: add user authentication`
7375
- `fix(api): resolve timeout issue`
74-
- `CU-86b7kybxx - feat: add git hooks`
75-
- `CU-86b7kybxx - fix(auth): resolve login bug`
76+
- `docs: update README`
7677

7778
### pre-commit
7879

7980
Runs custom commands defined in `.dev-hooks.yml` before each commit:
8081
- Lint checks
8182
- Type checking
8283
- Format validation
84+
- **Django test detection**: Warns if modified Python files don't have corresponding tests (configurable)
8385

8486
### pre-push
8587

86-
1. **Branch validation** - Validates branch names follow one of these formats:
88+
1. **Tag support** - Allows pushing git tags without any validation
89+
90+
2. **Branch validation** - Validates branch names follow one of these formats:
8791
- ClickUp ID: `CU-xxxxxxxxx`
8892
- Conventional: `<type>/<description>` (e.g., `feat/user-login`, `fix/header-bug`)
8993
- Special branches: `master`, `main`, `develop`, `staging`, `production`
9094

91-
2. **Custom commands** - Runs commands defined in `.dev-hooks.yml` (tests, build, etc.)
95+
3. **Django Smart Tests** - Runs tests only for modified apps/modules (configurable)
96+
97+
4. **Custom commands** - Runs commands defined in `.dev-hooks.yml` (tests, build, etc.)
9298

9399
## Configuration File
94100

@@ -147,6 +153,45 @@ If no matching files are found, commands are skipped with a message:
147153
Skipping pre-push commands (no matching files: *.py)
148154
```
149155

156+
### Django Smart Tests
157+
158+
For Django/Python projects, enable smart test features:
159+
160+
```yaml
161+
pre-commit:
162+
# Warn about modified files without tests (default: true)
163+
django_check_tests: true
164+
165+
pre-push:
166+
# Run tests only for modified apps (default: true)
167+
django_smart_tests: true
168+
# Test command (default: "pytest")
169+
django_test_command: "pytest"
170+
```
171+
172+
**How it works:**
173+
174+
1. **pre-commit**: Analyzes staged Python files and warns if they don't have corresponding test files (e.g., `test_<filename>.py`). This is just a warning and won't block the commit.
175+
176+
2. **pre-push**: Detects which Django apps/modules were modified and runs tests only for those apps instead of the entire test suite:
177+
```bash
178+
# Instead of: pytest
179+
# Runs: pytest app1 app2
180+
```
181+
182+
Example output:
183+
```
184+
┌──────────────────────────────────────────────────────────────┐
185+
│ Running Django Smart Tests... │
186+
└──────────────────────────────────────────────────────────────┘
187+
188+
Modified apps: users api payments
189+
190+
▶ Smart Tests
191+
pytest users api payments
192+
✔ Passed
193+
```
194+
150195
### Docker Support
151196

152197
For dockerized projects, enable docker execution:

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 = "dev-tools-hooks"
7-
version = "1.0.0"
7+
version = "1.1.0"
88
description = "Git hooks for development workflow - Conventional Commits, branch naming, and custom commands"
99
readme = "README.md"
1010
license = {text = "MIT"}

src/dev_tools_hooks/hooks/commit-msg

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
# Conventional Commits validation hook
44
# Valid prefixes: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
5-
# Supports ClickUp ID prefix: CU-xxxxxxxxx - type: description
65

76
commit_msg_file=$1
87
commit_msg=$(cat "$commit_msg_file")
@@ -14,9 +13,9 @@ types="feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert"
1413
# Format: type(optional-scope): description
1514
pattern="^($types)(\(.+\))?: .+"
1615

17-
# Pattern with ClickUp ID prefix
16+
# Pattern with ClickUp ID prefix (disabled - kept for future use if needed)
1817
# Format: CU-xxxxxxxxx - type(optional-scope): description
19-
clickup_pattern="^CU-[a-zA-Z0-9]+ - ($types)(\(.+\))?: .+"
18+
# clickup_pattern="^CU-[a-zA-Z0-9]+ - ($types)(\(.+\))?: .+"
2019

2120
# Allow merge commits
2221
merge_pattern="^Merge (branch|pull request|remote-tracking branch) .+"
@@ -29,9 +28,10 @@ if echo "$commit_msg" | grep -qE "$pattern"; then
2928
exit 0
3029
fi
3130

32-
if echo "$commit_msg" | grep -qE "$clickup_pattern"; then
33-
exit 0
34-
fi
31+
# ClickUp pattern validation disabled
32+
# if echo "$commit_msg" | grep -qE "$clickup_pattern"; then
33+
# exit 0
34+
# fi
3535

3636
# Colors optimized for macOS Terminal
3737
RED='\x1b[31m'
@@ -53,15 +53,10 @@ echo ""
5353
echo -e "${WHITE}${BOLD}Your message:${NC}"
5454
echo -e " ${DIM}\"${commit_msg}\"${NC}"
5555
echo ""
56-
echo -e "${CYAN}${BOLD}Expected formats:${NC}"
57-
echo ""
58-
echo -e " ${WHITE}1. Conventional Commit:${NC}"
59-
echo -e " ${GREEN}<type>${NC}${DIM}:${NC} ${WHITE}<description>${NC}"
60-
echo -e " ${GREEN}<type>${NC}${DIM}(${NC}${YELLOW}scope${NC}${DIM}):${NC} ${WHITE}<description>${NC}"
56+
echo -e "${CYAN}${BOLD}Expected format:${NC}"
6157
echo ""
62-
echo -e " ${WHITE}2. With ClickUp ID:${NC}"
63-
echo -e " ${MAGENTA}CU-xxxxxxxxx${NC} ${DIM}-${NC} ${GREEN}<type>${NC}${DIM}:${NC} ${WHITE}<description>${NC}"
64-
echo -e " ${MAGENTA}CU-xxxxxxxxx${NC} ${DIM}-${NC} ${GREEN}<type>${NC}${DIM}(${NC}${YELLOW}scope${NC}${DIM}):${NC} ${WHITE}<description>${NC}"
58+
echo -e " ${GREEN}<type>${NC}${DIM}:${NC} ${WHITE}<description>${NC}"
59+
echo -e " ${GREEN}<type>${NC}${DIM}(${NC}${YELLOW}scope${NC}${DIM}):${NC} ${WHITE}<description>${NC}"
6560
echo ""
6661
echo -e "${CYAN}${BOLD}Valid types:${NC}"
6762
echo -e " ${GREEN}feat${NC} ${DIM}A new feature${NC}"
@@ -79,7 +74,5 @@ echo ""
7974
echo -e "${CYAN}${BOLD}Examples:${NC}"
8075
echo -e " ${DIM}\$${NC} git commit -m \"${GREEN}feat${NC}: add user authentication\""
8176
echo -e " ${DIM}\$${NC} git commit -m \"${GREEN}fix${NC}(${YELLOW}api${NC}): resolve timeout issue\""
82-
echo -e " ${DIM}\$${NC} git commit -m \"${MAGENTA}CU-86b7kybxx${NC} - ${GREEN}feat${NC}: add git hooks\""
83-
echo -e " ${DIM}\$${NC} git commit -m \"${MAGENTA}CU-86b7kybxx${NC} - ${GREEN}fix${NC}(${YELLOW}auth${NC}): resolve login bug\""
8477
echo ""
8578
exit 1

src/dev_tools_hooks/hooks/pre-commit

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,71 @@ has_matching_staged_files() {
124124
return 1
125125
}
126126

127+
# ============================================================================
128+
# Django Test Detection
129+
# ============================================================================
130+
131+
check_django_tests() {
132+
local check_enabled=$(yaml_get "pre-commit.django_check_tests" "$CONFIG_FILE")
133+
134+
# Default to true if not specified
135+
if [ "$check_enabled" = "false" ]; then
136+
return 0
137+
fi
138+
139+
# Get staged Python files (excluding tests, migrations, __init__.py)
140+
local staged_py_files=$(git diff --cached --name-only --diff-filter=ACMR | grep '\.py$' | grep -v 'test' | grep -v 'migrations' | grep -v '__init__.py' | grep -v 'conftest.py' | grep -v 'setup.py' | grep -v 'manage.py')
141+
142+
if [ -z "$staged_py_files" ]; then
143+
return 0
144+
fi
145+
146+
local missing_tests=()
147+
148+
while IFS= read -r file; do
149+
[ -z "$file" ] && continue
150+
151+
# Get the directory and filename
152+
local dir=$(dirname "$file")
153+
local filename=$(basename "$file" .py)
154+
155+
# Skip if it's already a test file
156+
if [[ "$filename" == test_* ]] || [[ "$filename" == *_test ]]; then
157+
continue
158+
fi
159+
160+
# Look for corresponding test file
161+
local test_file_1="${dir}/test_${filename}.py"
162+
local test_file_2="${dir}/tests/test_${filename}.py"
163+
local test_file_3="${dir}/${filename}_test.py"
164+
local test_file_4="${dir}/tests.py"
165+
166+
# Check if any test file exists
167+
if [ ! -f "$PROJECT_ROOT/$test_file_1" ] && \
168+
[ ! -f "$PROJECT_ROOT/$test_file_2" ] && \
169+
[ ! -f "$PROJECT_ROOT/$test_file_3" ] && \
170+
[ ! -f "$PROJECT_ROOT/$test_file_4" ]; then
171+
missing_tests+=("$file")
172+
fi
173+
done <<< "$staged_py_files"
174+
175+
if [ ${#missing_tests[@]} -gt 0 ]; then
176+
echo ""
177+
echo -e "${YELLOW}${BOLD}┌──────────────────────────────────────────────────────────────┐${NC}"
178+
echo -e "${YELLOW}${BOLD}│ ⚠ WARNING: Modified files without corresponding tests │${NC}"
179+
echo -e "${YELLOW}${BOLD}└──────────────────────────────────────────────────────────────┘${NC}"
180+
echo ""
181+
for file in "${missing_tests[@]}"; do
182+
echo -e " ${DIM}${NC} ${WHITE}${file}${NC}"
183+
done
184+
echo ""
185+
echo -e "${DIM}Consider adding tests for these files.${NC}"
186+
echo ""
187+
fi
188+
189+
return 0
190+
}
191+
127192
# ============================================================================
128193
# Run Commands from .dev-hooks.yml
129194
# ============================================================================
@@ -218,5 +283,8 @@ run_commands() {
218283
# Main
219284
# ============================================================================
220285

286+
# Check for missing tests (warning only)
287+
check_django_tests
288+
221289
run_commands
222290
exit 0

0 commit comments

Comments
 (0)