Merge pull request #926 from xingzihai/feature/pre-commit-quality-hook

feat(hooks): add pre-commit quality check hook
This commit is contained in:
Affaan Mustafa
2026-03-29 00:07:28 -04:00
committed by GitHub
6 changed files with 1659 additions and 0 deletions

View File

@@ -0,0 +1,446 @@
---
name: performance-optimizer
description: Performance analysis and optimization specialist. Use PROACTIVELY for identifying bottlenecks, optimizing slow code, reducing bundle sizes, and improving runtime performance. Profiling, memory leaks, render optimization, and algorithmic improvements.
tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
model: sonnet
---
# Performance Optimizer
You are an expert performance specialist focused on identifying bottlenecks and optimizing application speed, memory usage, and efficiency. Your mission is to make code faster, lighter, and more responsive.
## Core Responsibilities
1. **Performance Profiling** — Identify slow code paths, memory leaks, and bottlenecks
2. **Bundle Optimization** — Reduce JavaScript bundle sizes, lazy loading, code splitting
3. **Runtime Optimization** — Improve algorithmic efficiency, reduce unnecessary computations
4. **React/Rendering Optimization** — Prevent unnecessary re-renders, optimize component trees
5. **Database & Network** — Optimize queries, reduce API calls, implement caching
6. **Memory Management** — Detect leaks, optimize memory usage, cleanup resources
## Analysis Commands
```bash
# Bundle analysis
npx bundle-analyzer
npx source-map-explorer build/static/js/*.js
# Lighthouse performance audit
npx lighthouse https://your-app.com --view
# Node.js profiling
node --prof your-app.js
node --prof-process isolate-*.log
# Memory analysis
node --inspect your-app.js # Then use Chrome DevTools
# React profiling (in browser)
# React DevTools > Profiler tab
# Network analysis
npx webpack-bundle-analyzer
```
## Performance Review Workflow
### 1. Identify Performance Issues
**Critical Performance Indicators:**
| Metric | Target | Action if Exceeded |
|--------|--------|-------------------|
| First Contentful Paint | < 1.8s | Optimize critical path, inline critical CSS |
| Largest Contentful Paint | < 2.5s | Lazy load images, optimize server response |
| Time to Interactive | < 3.8s | Code splitting, reduce JavaScript |
| Cumulative Layout Shift | < 0.1 | Reserve space for images, avoid layout thrashing |
| Total Blocking Time | < 200ms | Break up long tasks, use web workers |
| Bundle Size (gzipped) | < 200KB | Tree shaking, lazy loading, code splitting |
### 2. Algorithmic Analysis
Check for inefficient algorithms:
| Pattern | Complexity | Better Alternative |
|---------|------------|-------------------|
| Nested loops on same data | O(n²) | Use Map/Set for O(1) lookups |
| Repeated array searches | O(n) per search | Convert to Map for O(1) |
| Sorting inside loop | O(n² log n) | Sort once outside loop |
| String concatenation in loop | O(n²) | Use array.join() |
| Deep cloning large objects | O(n) each time | Use shallow copy or immer |
| Recursion without memoization | O(2^n) | Add memoization |
```typescript
// BAD: O(n²) - searching array in loop
for (const user of users) {
const posts = allPosts.filter(p => p.userId === user.id); // O(n) per user
}
// GOOD: O(n) - group once with Map
const postsByUser = new Map<number, Post[]>();
for (const post of allPosts) {
const userPosts = postsByUser.get(post.userId) || [];
userPosts.push(post);
postsByUser.set(post.userId, userPosts);
}
// Now O(1) lookup per user
```
### 3. React Performance Optimization
**Common React Anti-patterns:**
```tsx
// BAD: Inline function creation in render
<Button onClick={() => handleClick(id)}>Submit</Button>
// GOOD: Stable callback with useCallback
const handleButtonClick = useCallback(() => handleClick(id), [handleClick, id]);
<Button onClick={handleButtonClick}>Submit</Button>
// BAD: Object creation in render
<Child style={{ color: 'red' }} />
// GOOD: Stable object reference
const style = useMemo(() => ({ color: 'red' }), []);
<Child style={style} />
// BAD: Expensive computation on every render
const sortedItems = items.sort((a, b) => a.name.localeCompare(b.name));
// GOOD: Memoize expensive computations
const sortedItems = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// BAD: List without keys or with index
{items.map((item, index) => <Item key={index} />)}
// GOOD: Stable unique keys
{items.map(item => <Item key={item.id} item={item} />)}
```
**React Performance Checklist:**
- [ ] `useMemo` for expensive computations
- [ ] `useCallback` for functions passed to children
- [ ] `React.memo` for frequently re-rendered components
- [ ] Proper dependency arrays in hooks
- [ ] Virtualization for long lists (react-window, react-virtualized)
- [ ] Lazy loading for heavy components (`React.lazy`)
- [ ] Code splitting at route level
### 4. Bundle Size Optimization
**Bundle Analysis Checklist:**
```bash
# Analyze bundle composition
npx webpack-bundle-analyzer build/static/js/*.js
# Check for duplicate dependencies
npx duplicate-package-checker-analyzer
# Find largest files
du -sh node_modules/* | sort -hr | head -20
```
**Optimization Strategies:**
| Issue | Solution |
|-------|----------|
| Large vendor bundle | Tree shaking, smaller alternatives |
| Duplicate code | Extract to shared module |
| Unused exports | Remove dead code with knip |
| Moment.js | Use date-fns or dayjs (smaller) |
| Lodash | Use lodash-es or native methods |
| Large icons library | Import only needed icons |
```javascript
// BAD: Import entire library
import _ from 'lodash';
import moment from 'moment';
// GOOD: Import only what you need
import debounce from 'lodash/debounce';
import { format, addDays } from 'date-fns';
// Or use lodash-es with tree shaking
import { debounce, throttle } from 'lodash-es';
```
### 5. Database & Query Optimization
**Query Optimization Patterns:**
```sql
-- BAD: Select all columns
SELECT * FROM users WHERE active = true;
-- GOOD: Select only needed columns
SELECT id, name, email FROM users WHERE active = true;
-- BAD: N+1 queries (in application loop)
-- 1 query for users, then N queries for each user's orders
-- GOOD: Single query with JOIN or batch fetch
SELECT u.*, o.id as order_id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.active = true;
-- Add index for frequently queried columns
CREATE INDEX idx_users_active ON users(active);
CREATE INDEX idx_orders_user_id ON orders(user_id);
```
**Database Performance Checklist:**
- [ ] Indexes on frequently queried columns
- [ ] Composite indexes for multi-column queries
- [ ] Avoid SELECT * in production code
- [ ] Use connection pooling
- [ ] Implement query result caching
- [ ] Use pagination for large result sets
- [ ] Monitor slow query logs
### 6. Network & API Optimization
**Network Optimization Strategies:**
```typescript
// BAD: Multiple sequential requests
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
// GOOD: Parallel requests when independent
const [user, posts] = await Promise.all([
fetchUser(id),
fetchPosts(id)
]);
// GOOD: Batch requests when possible
const results = await batchFetch(['user1', 'user2', 'user3']);
// Implement request caching
const fetchWithCache = async (url: string, ttl = 300000) => {
const cached = cache.get(url);
if (cached) return cached;
const data = await fetch(url).then(r => r.json());
cache.set(url, data, ttl);
return data;
};
// Debounce rapid API calls
const debouncedSearch = debounce(async (query: string) => {
const results = await searchAPI(query);
setResults(results);
}, 300);
```
**Network Optimization Checklist:**
- [ ] Parallel independent requests with `Promise.all`
- [ ] Implement request caching
- [ ] Debounce rapid-fire requests
- [ ] Use streaming for large responses
- [ ] Implement pagination for large datasets
- [ ] Use GraphQL or API batching to reduce requests
- [ ] Enable compression (gzip/brotli) on server
### 7. Memory Leak Detection
**Common Memory Leak Patterns:**
```typescript
// BAD: Event listener without cleanup
useEffect(() => {
window.addEventListener('resize', handleResize);
// Missing cleanup!
}, []);
// GOOD: Clean up event listeners
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// BAD: Timer without cleanup
useEffect(() => {
setInterval(() => pollData(), 1000);
// Missing cleanup!
}, []);
// GOOD: Clean up timers
useEffect(() => {
const interval = setInterval(() => pollData(), 1000);
return () => clearInterval(interval);
}, []);
// BAD: Holding references in closures
const Component = () => {
const largeData = useLargeData();
useEffect(() => {
eventEmitter.on('update', () => {
console.log(largeData); // Closure keeps reference
});
}, [largeData]);
};
// GOOD: Use refs or proper dependencies
const largeDataRef = useRef(largeData);
useEffect(() => {
largeDataRef.current = largeData;
}, [largeData]);
useEffect(() => {
const handleUpdate = () => {
console.log(largeDataRef.current);
};
eventEmitter.on('update', handleUpdate);
return () => eventEmitter.off('update', handleUpdate);
}, []);
```
**Memory Leak Detection:**
```bash
# Chrome DevTools Memory tab:
# 1. Take heap snapshot
# 2. Perform action
# 3. Take another snapshot
# 4. Compare to find objects that shouldn't exist
# 5. Look for detached DOM nodes, event listeners, closures
# Node.js memory debugging
node --inspect app.js
# Open chrome://inspect
# Take heap snapshots and compare
```
## Performance Testing
### Lighthouse Audits
```bash
# Run full lighthouse audit
npx lighthouse https://your-app.com --view --preset=desktop
# CI mode for automated checks
npx lighthouse https://your-app.com --output=json --output-path=./lighthouse.json
# Check specific metrics
npx lighthouse https://your-app.com --only-categories=performance
```
### Performance Budgets
```json
// package.json
{
"bundlesize": [
{
"path": "./build/static/js/*.js",
"maxSize": "200 kB"
}
]
}
```
### Web Vitals Monitoring
```typescript
// Track Core Web Vitals
import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals';
getCLS(console.log); // Cumulative Layout Shift
getFID(console.log); // First Input Delay
getLCP(console.log); // Largest Contentful Paint
getFCP(console.log); // First Contentful Paint
getTTFB(console.log); // Time to First Byte
```
## Performance Report Template
````markdown
# Performance Audit Report
## Executive Summary
- **Overall Score**: X/100
- **Critical Issues**: X
- **Recommendations**: X
## Bundle Analysis
| Metric | Current | Target | Status |
|--------|---------|--------|--------|
| Total Size (gzip) | XXX KB | < 200 KB | ⚠️ |
| Main Bundle | XXX KB | < 100 KB | ✅ |
| Vendor Bundle | XXX KB | < 150 KB | ⚠️ |
## Web Vitals
| Metric | Current | Target | Status |
|--------|---------|--------|--------|
| LCP | X.Xs | < 2.5s | ✅ |
| FID | XXms | < 100ms | ✅ |
| CLS | X.XX | < 0.1 | ⚠️ |
## Critical Issues
### 1. [Issue Title]
**File**: path/to/file.ts:42
**Impact**: High - Causes XXXms delay
**Fix**: [Description of fix]
```typescript
// Before (slow)
const slowCode = ...;
// After (optimized)
const fastCode = ...;
```
### 2. [Issue Title]
...
## Recommendations
1. [Priority recommendation]
2. [Priority recommendation]
3. [Priority recommendation]
## Estimated Impact
- Bundle size reduction: XX KB (XX%)
- LCP improvement: XXms
- Time to Interactive improvement: XXms
````
## When to Run
**ALWAYS:** Before major releases, after adding new features, when users report slowness, during performance regression testing.
**IMMEDIATELY:** Lighthouse score drops, bundle size increases >10%, memory usage grows, slow page loads.
## Red Flags - Act Immediately
| Issue | Action |
|-------|--------|
| Bundle > 500KB gzip | Code split, lazy load, tree shake |
| LCP > 4s | Optimize critical path, preload resources |
| Memory usage growing | Check for leaks, review useEffect cleanup |
| CPU spikes | Profile with Chrome DevTools |
| Database query > 1s | Add index, optimize query, cache results |
## Success Metrics
- Lighthouse performance score > 90
- All Core Web Vitals in "good" range
- Bundle size under budget
- No memory leaks detected
- Test suite still passing
- No performance regressions
---
**Remember**: Performance is a feature. Users notice speed. Every 100ms of improvement matters. Optimize for the 90th percentile, not the average.

View File

@@ -23,6 +23,7 @@ User request → Claude picks a tool → PreToolUse hook runs → Tool executes
| **Dev server blocker** | `Bash` | Blocks `npm run dev` etc. outside tmux — ensures log access | 2 (blocks) | | **Dev server blocker** | `Bash` | Blocks `npm run dev` etc. outside tmux — ensures log access | 2 (blocks) |
| **Tmux reminder** | `Bash` | Suggests tmux for long-running commands (npm test, cargo build, docker) | 0 (warns) | | **Tmux reminder** | `Bash` | Suggests tmux for long-running commands (npm test, cargo build, docker) | 0 (warns) |
| **Git push reminder** | `Bash` | Reminds to review changes before `git push` | 0 (warns) | | **Git push reminder** | `Bash` | Reminds to review changes before `git push` | 0 (warns) |
| **Pre-commit quality check** | `Bash` | Runs quality checks before `git commit`: lints staged files, validates commit message format when provided via `-m/--message`, detects console.log/debugger/secrets | 2 (blocks critical) / 0 (warns) |
| **Doc file warning** | `Write` | Warns about non-standard `.md`/`.txt` files (allows README, CLAUDE, CONTRIBUTING, CHANGELOG, LICENSE, SKILL, docs/, skills/); cross-platform path handling | 0 (warns) | | **Doc file warning** | `Write` | Warns about non-standard `.md`/`.txt` files (allows README, CLAUDE, CONTRIBUTING, CHANGELOG, LICENSE, SKILL, docs/, skills/); cross-platform path handling | 0 (warns) |
| **Strategic compact** | `Edit\|Write` | Suggests manual `/compact` at logical intervals (every ~50 tool calls) | 0 (warns) | | **Strategic compact** | `Edit\|Write` | Suggests manual `/compact` at logical intervals (every ~50 tool calls) | 0 (warns) |
| **InsAIts security monitor (opt-in)** | `Bash\|Write\|Edit\|MultiEdit` | Optional security scan for high-signal tool inputs. Disabled unless `ECC_ENABLE_INSAITS=1`. Blocks on critical findings, warns on non-critical, and writes audit log to `.insaits_audit_session.jsonl`. Requires `pip install insa-its`. [Details](../scripts/hooks/insaits-security-monitor.py) | 2 (blocks critical) / 0 (warns) | | **InsAIts security monitor (opt-in)** | `Bash\|Write\|Edit\|MultiEdit` | Optional security scan for high-signal tool inputs. Disabled unless `ECC_ENABLE_INSAITS=1`. Blocks on critical findings, warns on non-critical, and writes audit log to `.insaits_audit_session.jsonl`. Requires `pip install insa-its`. [Details](../scripts/hooks/insaits-security-monitor.py) | 2 (blocks critical) / 0 (warns) |

View File

@@ -42,6 +42,16 @@
], ],
"description": "Reminder before git push to review changes" "description": "Reminder before git push to review changes"
}, },
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/run-with-flags.js\" \"pre:bash:commit-quality\" \"scripts/hooks/pre-bash-commit-quality.js\" \"strict\""
}
],
"description": "Pre-commit quality check: lint staged files, validate commit message format, detect console.log/debugger/secrets before committing"
},
{ {
"matcher": "Write", "matcher": "Write",
"hooks": [ "hooks": [

View File

@@ -0,0 +1,405 @@
#!/usr/bin/env node
/**
* PreToolUse Hook: Pre-commit Quality Check
*
* Runs quality checks before git commit commands:
* - Detects staged files
* - Runs linter on staged files (if available)
* - Checks for common issues (console.log, TODO, etc.)
* - Validates commit message format (if provided)
*
* Cross-platform (Windows, macOS, Linux)
*
* Exit codes:
* 0 - Success (allow commit)
* 2 - Block commit (quality issues found)
*/
const { spawnSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const MAX_STDIN = 1024 * 1024; // 1MB limit
/**
* Detect staged files for commit
* @returns {string[]} Array of staged file paths
*/
function getStagedFiles() {
const result = spawnSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACMR'], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
});
if (result.status !== 0) {
return [];
}
return result.stdout.trim().split('\n').filter(f => f.length > 0);
}
function getStagedFileContent(filePath) {
const result = spawnSync('git', ['show', `:${filePath}`], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
});
if (result.status !== 0) {
return null;
}
return result.stdout;
}
/**
* Check if a file should be quality-checked
* @param {string} filePath
* @returns {boolean}
*/
function shouldCheckFile(filePath) {
const checkableExtensions = ['.js', '.jsx', '.ts', '.tsx', '.py', '.go', '.rs'];
return checkableExtensions.some(ext => filePath.endsWith(ext));
}
/**
* Find issues in file content
* @param {string} filePath
* @returns {object[]} Array of issues found
*/
function findFileIssues(filePath) {
const issues = [];
try {
const content = getStagedFileContent(filePath);
if (content == null) {
return issues;
}
const lines = content.split('\n');
lines.forEach((line, index) => {
const lineNum = index + 1;
// Check for console.log
if (line.includes('console.log') && !line.trim().startsWith('//') && !line.trim().startsWith('*')) {
issues.push({
type: 'console.log',
message: `console.log found at line ${lineNum}`,
line: lineNum,
severity: 'warning'
});
}
// Check for debugger statements
if (/\bdebugger\b/.test(line) && !line.trim().startsWith('//')) {
issues.push({
type: 'debugger',
message: `debugger statement at line ${lineNum}`,
line: lineNum,
severity: 'error'
});
}
// Check for TODO/FIXME without issue reference
const todoMatch = line.match(/\/\/\s*(TODO|FIXME):?\s*(.+)/);
if (todoMatch && !todoMatch[2].match(/#\d+|issue/i)) {
issues.push({
type: 'todo',
message: `TODO/FIXME without issue reference at line ${lineNum}: "${todoMatch[2].trim()}"`,
line: lineNum,
severity: 'info'
});
}
// Check for hardcoded secrets (basic patterns)
const secretPatterns = [
{ pattern: /sk-[a-zA-Z0-9]{20,}/, name: 'OpenAI API key' },
{ pattern: /ghp_[a-zA-Z0-9]{36}/, name: 'GitHub PAT' },
{ pattern: /AKIA[A-Z0-9]{16}/, name: 'AWS Access Key' },
{ pattern: /api[_-]?key\s*[=:]\s*['"][^'"]+['"]/i, name: 'API key' }
];
for (const { pattern, name } of secretPatterns) {
if (pattern.test(line)) {
issues.push({
type: 'secret',
message: `Potential ${name} exposed at line ${lineNum}`,
line: lineNum,
severity: 'error'
});
}
}
});
} catch {
// File not readable, skip
}
return issues;
}
/**
* Validate commit message format
* @param {string} command
* @returns {object|null} Validation result or null if no message to validate
*/
function validateCommitMessage(command) {
// Extract commit message from command
const messageMatch = command.match(/(?:-m|--message)[=\s]+["']?([^"']+)["']?/);
if (!messageMatch) return null;
const message = messageMatch[1];
const issues = [];
// Check conventional commit format
const conventionalCommit = /^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?:\s*.+/;
if (!conventionalCommit.test(message)) {
issues.push({
type: 'format',
message: 'Commit message does not follow conventional commit format',
suggestion: 'Use format: type(scope): description (e.g., "feat(auth): add login flow")'
});
}
// Check message length
if (message.length > 72) {
issues.push({
type: 'length',
message: `Commit message too long (${message.length} chars, max 72)`,
suggestion: 'Keep the first line under 72 characters'
});
}
// Check for lowercase first letter (conventional)
if (conventionalCommit.test(message)) {
const afterColon = message.split(':')[1];
if (afterColon && /^[A-Z]/.test(afterColon.trim())) {
issues.push({
type: 'capitalization',
message: 'Subject should start with lowercase after type',
suggestion: 'Use lowercase for the first letter of the subject'
});
}
}
// Check for trailing period
if (message.endsWith('.')) {
issues.push({
type: 'punctuation',
message: 'Commit message should not end with a period',
suggestion: 'Remove the trailing period'
});
}
return { message, issues };
}
/**
* Run linter on staged files
* @param {string[]} files
* @returns {object} Lint results
*/
function runLinter(files) {
const jsFiles = files.filter(f => /\.(js|jsx|ts|tsx)$/.test(f));
const pyFiles = files.filter(f => f.endsWith('.py'));
const goFiles = files.filter(f => f.endsWith('.go'));
const results = {
eslint: null,
pylint: null,
golint: null
};
// Run ESLint if available
if (jsFiles.length > 0) {
const eslintBin = process.platform === 'win32' ? 'eslint.cmd' : 'eslint';
const eslintPath = path.join(process.cwd(), 'node_modules', '.bin', eslintBin);
if (fs.existsSync(eslintPath)) {
const result = spawnSync(eslintPath, ['--format', 'compact', ...jsFiles], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 30000
});
results.eslint = {
success: result.status === 0,
output: result.stdout || result.stderr
};
}
}
// Run Pylint if available
if (pyFiles.length > 0) {
try {
const result = spawnSync('pylint', ['--output-format=text', ...pyFiles], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 30000
});
if (result.error && result.error.code === 'ENOENT') {
results.pylint = null;
} else {
results.pylint = {
success: result.status === 0,
output: result.stdout || result.stderr
};
}
} catch {
// Pylint not available
}
}
// Run golint if available
if (goFiles.length > 0) {
try {
const result = spawnSync('golint', goFiles, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 30000
});
if (result.error && result.error.code === 'ENOENT') {
results.golint = null;
} else {
results.golint = {
success: !result.stdout || result.stdout.trim() === '',
output: result.stdout
};
}
} catch {
// golint not available
}
}
return results;
}
/**
* Core logic — exported for direct invocation
* @param {string} rawInput - Raw JSON string from stdin
* @returns {{output:string, exitCode:number}} Pass-through output and exit code
*/
function evaluate(rawInput) {
try {
const input = JSON.parse(rawInput);
const command = input.tool_input?.command || '';
// Only run for git commit commands
if (!command.includes('git commit')) {
return { output: rawInput, exitCode: 0 };
}
// Check if this is an amend (skip checks for amends to avoid blocking)
if (command.includes('--amend')) {
return { output: rawInput, exitCode: 0 };
}
// Get staged files
const stagedFiles = getStagedFiles();
if (stagedFiles.length === 0) {
console.error('[Hook] No staged files found. Use "git add" to stage files first.');
return { output: rawInput, exitCode: 0 };
}
console.error(`[Hook] Checking ${stagedFiles.length} staged file(s)...`);
// Check each staged file
const filesToCheck = stagedFiles.filter(shouldCheckFile);
let totalIssues = 0;
let errorCount = 0;
let warningCount = 0;
let infoCount = 0;
for (const file of filesToCheck) {
const fileIssues = findFileIssues(file);
if (fileIssues.length > 0) {
console.error(`\n📁 ${file}`);
for (const issue of fileIssues) {
const icon = issue.severity === 'error' ? '❌' : issue.severity === 'warning' ? '⚠️' : '';
console.error(` ${icon} Line ${issue.line}: ${issue.message}`);
totalIssues++;
if (issue.severity === 'error') errorCount++;
if (issue.severity === 'warning') warningCount++;
if (issue.severity === 'info') infoCount++;
}
}
}
// Validate commit message if provided
const messageValidation = validateCommitMessage(command);
if (messageValidation && messageValidation.issues.length > 0) {
console.error('\n📝 Commit Message Issues:');
for (const issue of messageValidation.issues) {
console.error(` ⚠️ ${issue.message}`);
if (issue.suggestion) {
console.error(` 💡 ${issue.suggestion}`);
}
totalIssues++;
warningCount++;
}
}
// Run linter
const lintResults = runLinter(filesToCheck);
if (lintResults.eslint && !lintResults.eslint.success) {
console.error('\n🔍 ESLint Issues:');
console.error(lintResults.eslint.output);
totalIssues++;
errorCount++;
}
if (lintResults.pylint && !lintResults.pylint.success) {
console.error('\n🔍 Pylint Issues:');
console.error(lintResults.pylint.output);
totalIssues++;
errorCount++;
}
if (lintResults.golint && !lintResults.golint.success) {
console.error('\n🔍 golint Issues:');
console.error(lintResults.golint.output);
totalIssues++;
errorCount++;
}
// Summary
if (totalIssues > 0) {
console.error(`\n📊 Summary: ${totalIssues} issue(s) found (${errorCount} error(s), ${warningCount} warning(s), ${infoCount} info)`);
if (errorCount > 0) {
console.error('\n[Hook] ❌ Commit blocked due to critical issues. Fix them before committing.');
return { output: rawInput, exitCode: 2 };
} else {
console.error('\n[Hook] ⚠️ Warnings found. Consider fixing them, but commit is allowed.');
console.error('[Hook] To bypass these checks, use: git commit --no-verify');
}
} else {
console.error('\n[Hook] ✅ All checks passed!');
}
} catch (error) {
console.error(`[Hook] Error: ${error.message}`);
// Non-blocking on error
}
return { output: rawInput, exitCode: 0 };
}
function run(rawInput) {
return evaluate(rawInput).output;
}
// ── stdin entry point ────────────────────────────────────────────
if (require.main === module) {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => {
if (data.length < MAX_STDIN) {
const remaining = MAX_STDIN - data.length;
data += chunk.substring(0, remaining);
}
});
process.stdin.on('end', () => {
const result = evaluate(data);
process.stdout.write(result.output);
process.exit(result.exitCode);
});
}
module.exports = { run, evaluate };

View File

@@ -0,0 +1,716 @@
---
name: git-workflow
description: Git workflow patterns including branching strategies, commit conventions, merge vs rebase, conflict resolution, and collaborative development best practices for teams of all sizes.
origin: ECC
---
# Git Workflow Patterns
Best practices for Git version control, branching strategies, and collaborative development.
## When to Activate
- Setting up Git workflow for a new project
- Deciding on branching strategy (GitFlow, trunk-based, GitHub flow)
- Writing commit messages and PR descriptions
- Resolving merge conflicts
- Managing releases and version tags
- Onboarding new team members to Git practices
## Branching Strategies
### GitHub Flow (Simple, Recommended for Most)
Best for continuous deployment and small-to-medium teams.
```
main (protected, always deployable)
├── feature/user-auth → PR → merge to main
├── feature/payment-flow → PR → merge to main
└── fix/login-bug → PR → merge to main
```
**Rules:**
- `main` is always deployable
- Create feature branches from `main`
- Open Pull Request when ready for review
- After approval and CI passes, merge to `main`
- Deploy immediately after merge
### Trunk-Based Development (High-Velocity Teams)
Best for teams with strong CI/CD and feature flags.
```
main (trunk)
├── short-lived feature (1-2 days max)
├── short-lived feature
└── short-lived feature
```
**Rules:**
- Everyone commits to `main` or very short-lived branches
- Feature flags hide incomplete work
- CI must pass before merge
- Deploy multiple times per day
### GitFlow (Complex, Release-Cycle Driven)
Best for scheduled releases and enterprise projects.
```
main (production releases)
└── develop (integration branch)
├── feature/user-auth
├── feature/payment
├── release/1.0.0 → merge to main and develop
└── hotfix/critical → merge to main and develop
```
**Rules:**
- `main` contains production-ready code only
- `develop` is the integration branch
- Feature branches from `develop`, merge back to `develop`
- Release branches from `develop`, merge to `main` and `develop`
- Hotfix branches from `main`, merge to both `main` and `develop`
### When to Use Which
| Strategy | Team Size | Release Cadence | Best For |
|----------|-----------|-----------------|----------|
| GitHub Flow | Any | Continuous | SaaS, web apps, startups |
| Trunk-Based | 5+ experienced | Multiple/day | High-velocity teams, feature flags |
| GitFlow | 10+ | Scheduled | Enterprise, regulated industries |
## Commit Messages
### Conventional Commits Format
```
<type>(<scope>): <subject>
[optional body]
[optional footer(s)]
```
### Types
| Type | Use For | Example |
|------|---------|---------|
| `feat` | New feature | `feat(auth): add OAuth2 login` |
| `fix` | Bug fix | `fix(api): handle null response in user endpoint` |
| `docs` | Documentation | `docs(readme): update installation instructions` |
| `style` | Formatting, no code change | `style: fix indentation in login component` |
| `refactor` | Code refactoring | `refactor(db): extract connection pool to module` |
| `test` | Adding/updating tests | `test(auth): add unit tests for token validation` |
| `chore` | Maintenance tasks | `chore(deps): update dependencies` |
| `perf` | Performance improvement | `perf(query): add index to users table` |
| `ci` | CI/CD changes | `ci: add PostgreSQL service to test workflow` |
| `revert` | Revert previous commit | `revert: revert "feat(auth): add OAuth2 login"` |
### Good vs Bad Examples
```
# BAD: Vague, no context
git commit -m "fixed stuff"
git commit -m "updates"
git commit -m "WIP"
# GOOD: Clear, specific, explains why
git commit -m "fix(api): retry requests on 503 Service Unavailable
The external API occasionally returns 503 errors during peak hours.
Added exponential backoff retry logic with max 3 attempts.
Closes #123"
```
### Commit Message Template
Create `.gitmessage` in repo root:
```
# <type>(<scope>): <subject>
#
# Types: feat, fix, docs, style, refactor, test, chore, perf, ci, revert
# Scope: api, ui, db, auth, etc.
# Subject: imperative mood, no period, max 50 chars
#
# [optional body] - explain why, not what
# [optional footer] - Breaking changes, closes #issue
```
Enable with: `git config commit.template .gitmessage`
## Merge vs Rebase
### Merge (Preserves History)
```bash
# Creates a merge commit
git checkout main
git merge feature/user-auth
# Result:
# * merge commit
# |\
# | * feature commits
# |/
# * main commits
```
**Use when:**
- Merging feature branches into `main`
- You want to preserve exact history
- Multiple people worked on the branch
- The branch has been pushed and others may have based work on it
### Rebase (Linear History)
```bash
# Rewrites feature commits onto target branch
git checkout feature/user-auth
git rebase main
# Result:
# * feature commits (rewritten)
# * main commits
```
**Use when:**
- Updating your local feature branch with latest `main`
- You want a linear, clean history
- The branch is local-only (not pushed)
- You're the only one working on the branch
### Rebase Workflow
```bash
# Update feature branch with latest main (before PR)
git checkout feature/user-auth
git fetch origin
git rebase origin/main
# Fix any conflicts
# Tests should still pass
# Force push (only if you're the only contributor)
git push --force-with-lease origin feature/user-auth
```
### When NOT to Rebase
```
# NEVER rebase branches that:
- Have been pushed to a shared repository
- Other people have based work on
- Are protected branches (main, develop)
- Are already merged
# Why: Rebase rewrites history, breaking others' work
```
## Pull Request Workflow
### PR Title Format
```
<type>(<scope>): <description>
Examples:
feat(auth): add SSO support for enterprise users
fix(api): resolve race condition in order processing
docs(api): add OpenAPI specification for v2 endpoints
```
### PR Description Template
```markdown
## What
Brief description of what this PR does.
## Why
Explain the motivation and context.
## How
Key implementation details worth highlighting.
## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [ ] Manual testing performed
## Screenshots (if applicable)
Before/after screenshots for UI changes.
## Checklist
- [ ] Code follows project style guidelines
- [ ] Self-review completed
- [ ] Comments added for complex logic
- [ ] Documentation updated
- [ ] No new warnings introduced
- [ ] Tests pass locally
- [ ] Related issues linked
Closes #123
```
### Code Review Checklist
**For Reviewers:**
- [ ] Does the code solve the stated problem?
- [ ] Are there any edge cases not handled?
- [ ] Is the code readable and maintainable?
- [ ] Are there sufficient tests?
- [ ] Are there security concerns?
- [ ] Is the commit history clean (squashed if needed)?
**For Authors:**
- [ ] Self-review completed before requesting review
- [ ] CI passes (tests, lint, typecheck)
- [ ] PR size is reasonable (<500 lines ideal)
- [ ] Related to a single feature/fix
- [ ] Description clearly explains the change
## Conflict Resolution
### Identify Conflicts
```bash
# Check for conflicts before merge
git checkout main
git merge feature/user-auth --no-commit --no-ff
# If conflicts, Git will show:
# CONFLICT (content): Merge conflict in src/auth/login.ts
# Automatic merge failed; fix conflicts and then commit the result.
```
### Resolve Conflicts
```bash
# See conflicted files
git status
# View conflict markers in file
# <<<<<<< HEAD
# content from main
# =======
# content from feature branch
# >>>>>>> feature/user-auth
# Option 1: Manual resolution
# Edit file, remove markers, keep correct content
# Option 2: Use merge tool
git mergetool
# Option 3: Accept one side
git checkout --ours src/auth/login.ts # Keep main version
git checkout --theirs src/auth/login.ts # Keep feature version
# After resolving, stage and commit
git add src/auth/login.ts
git commit
```
### Conflict Prevention Strategies
```bash
# 1. Keep feature branches small and short-lived
# 2. Rebase frequently onto main
git checkout feature/user-auth
git fetch origin
git rebase origin/main
# 3. Communicate with team about touching shared files
# 4. Use feature flags instead of long-lived branches
# 5. Review and merge PRs promptly
```
## Branch Management
### Naming Conventions
```
# Feature branches
feature/user-authentication
feature/JIRA-123-payment-integration
# Bug fixes
fix/login-redirect-loop
fix/456-null-pointer-exception
# Hotfixes (production issues)
hotfix/critical-security-patch
hotfix/database-connection-leak
# Releases
release/1.2.0
release/2024-01-hotfix
# Experiments/POCs
experiment/new-caching-strategy
poc/graphql-migration
```
### Branch Cleanup
```bash
# Delete local branches that are merged
git branch --merged main | grep -v "^\*\|main" | xargs -n 1 git branch -d
# Delete remote-tracking references for deleted remote branches
git fetch -p
# Delete local branch
git branch -d feature/user-auth # Safe delete (only if merged)
git branch -D feature/user-auth # Force delete
# Delete remote branch
git push origin --delete feature/user-auth
```
### Stash Workflow
```bash
# Save work in progress
git stash push -m "WIP: user authentication"
# List stashes
git stash list
# Apply most recent stash
git stash pop
# Apply specific stash
git stash apply stash@{2}
# Drop stash
git stash drop stash@{0}
```
## Release Management
### Semantic Versioning
```
MAJOR.MINOR.PATCH
MAJOR: Breaking changes
MINOR: New features, backward compatible
PATCH: Bug fixes, backward compatible
Examples:
1.0.0 → 1.0.1 (patch: bug fix)
1.0.1 → 1.1.0 (minor: new feature)
1.1.0 → 2.0.0 (major: breaking change)
```
### Creating Releases
```bash
# Create annotated tag
git tag -a v1.2.0 -m "Release v1.2.0
Features:
- Add user authentication
- Implement password reset
Fixes:
- Resolve login redirect issue
Breaking Changes:
- None"
# Push tag to remote
git push origin v1.2.0
# List tags
git tag -l
# Delete tag
git tag -d v1.2.0
git push origin --delete v1.2.0
```
### Changelog Generation
```bash
# Generate changelog from commits
git log v1.1.0..v1.2.0 --oneline --no-merges
# Or use conventional-changelog
npx conventional-changelog -i CHANGELOG.md -s
```
## Git Configuration
### Essential Configs
```bash
# User identity
git config --global user.name "Your Name"
git config --global user.email "your@email.com"
# Default branch name
git config --global init.defaultBranch main
# Pull behavior (rebase instead of merge)
git config --global pull.rebase true
# Push behavior (push current branch only)
git config --global push.default current
# Auto-correct typos
git config --global help.autocorrect 1
# Better diff algorithm
git config --global diff.algorithm histogram
# Color output
git config --global color.ui auto
```
### Useful Aliases
```bash
# Add to ~/.gitconfig
[alias]
co = checkout
br = branch
ci = commit
st = status
unstage = reset HEAD --
last = log -1 HEAD
visual = log --oneline --graph --all
amend = commit --amend --no-edit
wip = commit -m "WIP"
undo = reset --soft HEAD~1
contributors = shortlog -sn
```
### Gitignore Patterns
```gitignore
# Dependencies
node_modules/
vendor/
# Build outputs
dist/
build/
*.o
*.exe
# Environment files
.env
.env.local
.env.*.local
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS files
.DS_Store
Thumbs.db
# Logs
*.log
logs/
# Test coverage
coverage/
# Cache
.cache/
*.tsbuildinfo
```
## Common Workflows
### Starting a New Feature
```bash
# 1. Update main branch
git checkout main
git pull origin main
# 2. Create feature branch
git checkout -b feature/user-auth
# 3. Make changes and commit
git add .
git commit -m "feat(auth): implement OAuth2 login"
# 4. Push to remote
git push -u origin feature/user-auth
# 5. Create Pull Request on GitHub/GitLab
```
### Updating a PR with New Changes
```bash
# 1. Make additional changes
git add .
git commit -m "feat(auth): add error handling"
# 2. Push updates
git push origin feature/user-auth
```
### Syncing Fork with Upstream
```bash
# 1. Add upstream remote (once)
git remote add upstream https://github.com/original/repo.git
# 2. Fetch upstream
git fetch upstream
# 3. Merge upstream/main into your main
git checkout main
git merge upstream/main
# 4. Push to your fork
git push origin main
```
### Undoing Mistakes
```bash
# Undo last commit (keep changes)
git reset --soft HEAD~1
# Undo last commit (discard changes)
git reset --hard HEAD~1
# Undo last commit pushed to remote
git revert HEAD
git push origin main
# Undo specific file changes
git checkout HEAD -- path/to/file
# Fix last commit message
git commit --amend -m "New message"
# Add forgotten file to last commit
git add forgotten-file
git commit --amend --no-edit
```
## Git Hooks
### Pre-Commit Hook
```bash
#!/bin/bash
# .git/hooks/pre-commit
# Run linting
npm run lint || exit 1
# Run tests
npm test || exit 1
# Check for secrets
if git diff --cached | grep -E '(password|api_key|secret)'; then
echo "Possible secret detected. Commit aborted."
exit 1
fi
```
### Pre-Push Hook
```bash
#!/bin/bash
# .git/hooks/pre-push
# Run full test suite
npm run test:all || exit 1
# Check for console.log statements
if git diff origin/main | grep -E 'console\.log'; then
echo "Remove console.log statements before pushing."
exit 1
fi
```
## Anti-Patterns
```
# BAD: Committing directly to main
git checkout main
git commit -m "fix bug"
# GOOD: Use feature branches and PRs
# BAD: Committing secrets
git add .env # Contains API keys
# GOOD: Add to .gitignore, use environment variables
# BAD: Giant PRs (1000+ lines)
# GOOD: Break into smaller, focused PRs
# BAD: "Update" commit messages
git commit -m "update"
git commit -m "fix"
# GOOD: Descriptive messages
git commit -m "fix(auth): resolve redirect loop after login"
# BAD: Rewriting public history
git push --force origin main
# GOOD: Use revert for public branches
git revert HEAD
# BAD: Long-lived feature branches (weeks/months)
# GOOD: Keep branches short (days), rebase frequently
# BAD: Committing generated files
git add dist/
git add node_modules/
# GOOD: Add to .gitignore
```
## Quick Reference
| Task | Command |
|------|---------|
| Create branch | `git checkout -b feature/name` |
| Switch branch | `git checkout branch-name` |
| Delete branch | `git branch -d branch-name` |
| Merge branch | `git merge branch-name` |
| Rebase branch | `git rebase main` |
| View history | `git log --oneline --graph` |
| View changes | `git diff` |
| Stage changes | `git add .` or `git add -p` |
| Commit | `git commit -m "message"` |
| Push | `git push origin branch-name` |
| Pull | `git pull origin branch-name` |
| Stash | `git stash push -m "message"` |
| Undo last commit | `git reset --soft HEAD~1` |
| Revert commit | `git revert HEAD` |

View File

@@ -0,0 +1,81 @@
/**
* Tests for scripts/hooks/pre-bash-commit-quality.js
*
* Run with: node tests/hooks/pre-bash-commit-quality.test.js
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const hook = require('../../scripts/hooks/pre-bash-commit-quality');
function test(name, fn) {
try {
fn();
console.log(`${name}`);
return true;
} catch (err) {
console.log(`${name}`);
console.log(` Error: ${err.message}`);
return false;
}
}
function inTempRepo(fn) {
const prevCwd = process.cwd();
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pre-bash-commit-quality-'));
try {
spawnSync('git', ['init'], { cwd: repoDir, stdio: 'pipe', encoding: 'utf8' });
spawnSync('git', ['config', 'user.name', 'ECC Test'], { cwd: repoDir, stdio: 'pipe', encoding: 'utf8' });
spawnSync('git', ['config', 'user.email', 'ecc@example.com'], { cwd: repoDir, stdio: 'pipe', encoding: 'utf8' });
process.chdir(repoDir);
return fn(repoDir);
} finally {
process.chdir(prevCwd);
fs.rmSync(repoDir, { recursive: true, force: true });
}
}
let passed = 0;
let failed = 0;
console.log('\nPre-Bash Commit Quality Hook Tests');
console.log('==================================\n');
if (test('evaluate blocks commits when staged snapshot contains debugger', () => {
inTempRepo(repoDir => {
const filePath = path.join(repoDir, 'index.js');
fs.writeFileSync(filePath, 'function main() {\n debugger;\n}\n', 'utf8');
spawnSync('git', ['add', 'index.js'], { cwd: repoDir, stdio: 'pipe', encoding: 'utf8' });
const input = JSON.stringify({ tool_input: { command: 'git commit -m "fix: test debugger hook"' } });
const result = hook.evaluate(input);
assert.strictEqual(result.output, input, 'should preserve stdin payload');
assert.strictEqual(result.exitCode, 2, 'should block commit when staged snapshot has debugger');
});
})) passed++; else failed++;
if (test('evaluate inspects staged snapshot instead of newer working tree content', () => {
inTempRepo(repoDir => {
const filePath = path.join(repoDir, 'index.js');
fs.writeFileSync(filePath, 'function main() {\n return 1;\n}\n', 'utf8');
spawnSync('git', ['add', 'index.js'], { cwd: repoDir, stdio: 'pipe', encoding: 'utf8' });
// Working tree diverges after staging; hook should still inspect staged content.
fs.writeFileSync(filePath, 'function main() {\n debugger;\n return 1;\n}\n', 'utf8');
const input = JSON.stringify({ tool_input: { command: 'git commit -m "fix: staged snapshot only"' } });
const result = hook.evaluate(input);
assert.strictEqual(result.output, input, 'should preserve stdin payload');
assert.strictEqual(result.exitCode, 0, 'should ignore unstaged debugger in working tree');
});
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);