Skip to content

Commit e05130e

Browse files
authored
Merge pull request #3 from swappsco/feat/smart-tests
feat: add Django smart test detection and configuration options for p…
2 parents aab417d + e30a04d commit e05130e

7 files changed

Lines changed: 296 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: 52 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

@@ -23,6 +26,7 @@ pip install -e .
2326

2427
## Usage
2528

29+
2630
### Install hooks in your repository
2731

2832
```bash
@@ -63,32 +67,35 @@ Validates that commit messages follow Conventional Commits format:
6367

6468
```
6569
<type>(<optional-scope>): <description>
66-
CU-xxxxxxxxx - <type>(<optional-scope>): <description>
6770
```
6871

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

7174
Examples:
7275
- `feat: add user authentication`
7376
- `fix(api): resolve timeout issue`
74-
- `CU-86b7kybxx - feat: add git hooks`
75-
- `CU-86b7kybxx - fix(auth): resolve login bug`
77+
- `docs: update README`
7678

7779
### pre-commit
7880

7981
Runs custom commands defined in `.dev-hooks.yml` before each commit:
8082
- Lint checks
8183
- Type checking
8284
- Format validation
85+
- **Django test detection**: Warns if modified Python files don't have corresponding tests (configurable)
8386

8487
### pre-push
8588

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

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

93100
## Configuration File
94101

@@ -147,6 +154,45 @@ If no matching files are found, commands are skipped with a message:
147154
Skipping pre-push commands (no matching files: *.py)
148155
```
149156

157+
### Django Smart Tests
158+
159+
For Django/Python projects, enable smart test features:
160+
161+
```yaml
162+
pre-commit:
163+
# Warn about modified files without tests (default: true)
164+
django_check_tests: true
165+
166+
pre-push:
167+
# Run tests only for modified apps (default: true)
168+
django_smart_tests: true
169+
# Test command (default: "pytest")
170+
django_test_command: "pytest"
171+
```
172+
173+
**How it works:**
174+
175+
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.
176+
177+
2. **pre-push**: Detects which Django apps/modules were modified and runs tests only for those apps instead of the entire test suite:
178+
```bash
179+
# Instead of: pytest
180+
# Runs: pytest app1 app2
181+
```
182+
183+
Example output:
184+
```
185+
┌──────────────────────────────────────────────────────────────┐
186+
│ Running Django Smart Tests... │
187+
└──────────────────────────────────────────────────────────────┘
188+
189+
Modified apps: users api payments
190+
191+
▶ Smart Tests
192+
pytest users api payments
193+
✔ Passed
194+
```
195+
150196
### Docker Support
151197

152198
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)