mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
Merge pull request #926 from xingzihai/feature/pre-commit-quality-hook
feat(hooks): add pre-commit quality check hook
This commit is contained in:
446
agents/performance-optimizer.md
Normal file
446
agents/performance-optimizer.md
Normal 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.
|
||||||
@@ -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) |
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
405
scripts/hooks/pre-bash-commit-quality.js
Normal file
405
scripts/hooks/pre-bash-commit-quality.js
Normal 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 };
|
||||||
716
skills/git-workflow/SKILL.md
Normal file
716
skills/git-workflow/SKILL.md
Normal 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` |
|
||||||
81
tests/hooks/pre-bash-commit-quality.test.js
Normal file
81
tests/hooks/pre-bash-commit-quality.test.js
Normal 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);
|
||||||
Reference in New Issue
Block a user