Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:55:13 +08:00
commit 02de918865
11 changed files with 2014 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "git-commit",
"description": "Professional git commit message generation following Conventional Commits specification with automatic diff analysis",
"version": "2.0.0",
"author": {
"name": "AnthemFlynn",
"email": "AnthemFlynn@users.noreply.github.com"
},
"skills": [
"./skills"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# git-commit
Professional git commit message generation following Conventional Commits specification with automatic diff analysis

73
plugin.lock.json Normal file
View File

@@ -0,0 +1,73 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:AnthemFlynn/ccmp:plugins/git-commit",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "e75285328b429e6c01ab67f622d92ad894b91dc8",
"treeHash": "07548aa19bbe5949aa3cb642bbd39babf944d19c5243af92a806a3c76f0b645a",
"generatedAt": "2025-11-28T10:24:52.617149Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "git-commit",
"description": "Professional git commit message generation following Conventional Commits specification with automatic diff analysis",
"version": "2.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "f092531a502f0a597c9a4b18089f6bf6374bbde5af93aadb7e4a2048f8223b4f"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "1da7a4b3f99daf08068a269393ed267fcaa1d2636fa9a79225e1a74c692e4d86"
},
{
"path": "skills/git-commit/README.md",
"sha256": "89da246224599144eacacaf7ce9d1cb99b2212ff2fce7608d8197e79583a58bb"
},
{
"path": "skills/git-commit/SKILL.md",
"sha256": "58f51b3671896d7c5dc63c7500f21c2bda3994bd29d43439aa8bc2bde4052fb9"
},
{
"path": "skills/git-commit/references/slash-commands.md",
"sha256": "2b0955e50ac3c2d9fc69dd7dc4470ef9764f355646ed50fdf7b9836f2a0ccd0a"
},
{
"path": "skills/git-commit/references/examples.md",
"sha256": "dd177ce89dc2451dc9b91ffd8cde6a4dcda5025b8efa4fe15c04673e3fdb3eb1"
},
{
"path": "skills/git-commit/scripts/version.py",
"sha256": "8f33f318c6c093de8a043ec5ec488e702421de5737b9df933fb6e6868e027a6d"
},
{
"path": "skills/git-commit/scripts/validate.py",
"sha256": "688a9b42d920fe84dac2ceb9a8ce52cdfd8ef0c32150927c9f7363b13c7b0e45"
},
{
"path": "skills/git-commit/scripts/changelog.py",
"sha256": "07c40536622cb7e5e855c2dac1c9f1863c67175169ba90651403b4ddc43b13a5"
},
{
"path": "skills/git-commit/scripts/analyze-diff.py",
"sha256": "0e712385957b8b0046c01dcb989a0f3637044d75868fcb5dc9ba6fcd03b6b472"
}
],
"dirSha256": "07548aa19bbe5949aa3cb642bbd39babf944d19c5243af92a806a3c76f0b645a"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

147
skills/git-commit/README.md Normal file
View File

@@ -0,0 +1,147 @@
# Git Commit Skill
Claude will help you write professional commit messages following industry standards (Conventional Commits).
## Installation
1. Install `git-commit.skill` in Claude
2. That's it
## Usage
### Smart Analysis (NEW!)
Stage your changes and ask Claude to help:
```
You: "Help me commit"
Claude: [runs analyze-diff.py to examine your code]
Based on your changes to auth/oauth.py:
- Added OAuth2 authentication functions
- Modified 15 lines in auth module
Suggested commit:
git commit -m"feat(auth): add OAuth2 authentication"
```
The analyzer examines:
- **File paths** → suggests scope (e.g., auth, api, ui)
- **Added code** → suggests type (feat, fix, refactor)
- **Function names** → generates description
- **Removed APIs** → detects breaking changes
You can also run it standalone:
```bash
git add .
python scripts/analyze-diff.py # Get suggestion
python scripts/analyze-diff.py --commit # Auto-commit with suggestion
```
### Manual Description
Or just describe what you changed:
```
You: "Help me write a commit - I added OAuth login"
Claude: git commit -m"feat(auth): add OAuth2 login support"
```
Claude will:
- Ask clarifying questions if needed
- Suggest the right commit type
- Format everything correctly
- Give you a ready-to-use git command
## Slash Commands
Use these commands for quick access to specific features:
- **`/commit`** - Smart commit helper (analyzes code if staged, otherwise interactive)
- **`/validate <message>`** - Check if a commit message is valid
- **`/types`** - Show all commit types with examples
- **`/scopes`** - Learn about scopes with project-specific suggestions
- **`/breaking`** - Guide for creating breaking change commits
- **`/changelog`** - Generate formatted changelog from commits
- **`/version`** - Calculate next semantic version number
- **`/examples`** - Show real-world commit examples
- **`/fix`** - Help amend or fix recent commits
## How /commit Works
**Smart and Adaptive:**
1. **Has staged changes?** → Analyzes your code automatically
2. **No staged changes?** → Asks what you changed, builds interactively
3. **You described it already?** → Uses your description
**Example with staged changes:**
```bash
git add auth/oauth.py
```
```
You: /commit
Claude: 📊 Analyzed your changes...
Suggested: git commit -m"feat(auth): add OAuth2 authentication"
Does this look good?
```
**Example without staged changes:**
```
You: /commit
Claude: No staged changes found. What did you change?
You: I added OAuth login
Claude: git commit -m"feat(auth): add OAuth login"
```
One command, smart behavior.
## Examples of What to Ask
- "Help me commit this change: [describe what you did]"
- "How should I write a commit for fixing the login bug?"
- "Is this commit message okay? fix: bug"
- "I made a breaking change to the API, help me write the commit"
## Commit Format
Claude follows this format:
```
type(scope): description
optional body
optional footer
```
**Types:** feat, fix, refactor, perf, style, test, docs, build, ops, chore
You don't need to memorize this - just describe what you did and Claude will format it correctly.
## Optional: Git Hook
If you want automatic validation, copy the included script:
```bash
cp scripts/validate.py .git/hooks/commit-msg
chmod +x .git/hooks/commit-msg
```
Now all commits are validated before they're created.
## That's It
No documentation to read. No commands to memorize. Just ask Claude for help.
---
**What it does:** Helps you write good commits
**How to use it:** Ask Claude naturally
**Learning curve:** Zero

213
skills/git-commit/SKILL.md Normal file
View File

@@ -0,0 +1,213 @@
---
name: git-commit
description: Help users write professional git commit messages following Conventional Commits. Use when users ask about commits, need help writing commit messages, want to validate commit format, ask about git message conventions, or use slash commands like /commit, /validate, /changelog, /version.
---
# Git Commit Assistant
Help users write clear, professional commit messages following the Conventional Commits specification.
## Slash Commands
Recognize and respond to these slash commands:
- `/commit` - Smart commit helper (auto-analyzes code if staged, otherwise interactive)
- `/validate <message>` - Validate a commit message format
- `/types` - Show all commit types with descriptions
- `/scopes` - Explain scopes and show examples
- `/breaking` - Guide for creating breaking change commits
- `/changelog` - Generate changelog from recent commits
- `/version` - Determine next semantic version from commits
- `/examples` - Show comprehensive commit examples
- `/fix` - Help amend/fix the last commit
When user types a slash command, execute that specific workflow.
## User Intent Recognition
When users ask questions like:
- "Help me write a commit for..." → Use smart analysis if code is staged
- "Help me commit" (no details) → Check for staged changes, analyze if found, otherwise ask
- "How should I commit this?" → Smart analysis mode
- "Is this commit message good?" → Validation mode
- "What's the right format for..." → Show format and examples
Guide them naturally through creating a proper commit.
## Commit Format
Standard format:
```
<type>(<scope>): <description>
<body>
<footer>
```
**Types:**
- `feat` - New feature
- `fix` - Bug fix
- `refactor` - Code change without behavior change
- `perf` - Performance improvement
- `style` - Formatting, whitespace
- `test` - Test changes
- `docs` - Documentation
- `build` - Build/dependencies
- `ops` - Infrastructure/deployment
- `chore` - Maintenance
**Scope:** Optional context (e.g., `api`, `auth`, `database`)
**Description:** Short summary, lowercase, imperative mood, no period, under 100 chars
**Body:** Optional explanation of what and why
**Footer:** Optional issue references (`Closes #123`) or breaking changes
## Breaking Changes
Add `!` before colon: `feat(api)!: remove endpoint`
Include in footer:
```
BREAKING CHANGE: explanation of what broke and how to migrate
```
## Workflow Modes
### Smart Commit Mode (/commit or "help me commit")
When user requests help with a commit, follow this adaptive workflow:
**Step 1: Check for staged changes**
- Run `git diff --staged --name-only` to check for staged files
- If error (not a git repo), explain and exit
**Step 2: Choose path based on context**
**Path A: Staged changes exist (Smart Analysis)**
1. Run diff analyzer: `scripts/analyze-diff.py --json`
2. Parse results: type, scope, description, confidence, breaking
3. Present analysis:
```
📊 I analyzed your staged changes:
Files: auth/oauth.py (+45 lines)
Changes: New OAuth authentication functions
Suggested commit:
git commit -m"feat(auth): add OAuth2 authentication"
Does this look good? (y/n/help)
```
4. Handle response:
- `y` or positive → Provide final command
- `n` or concerns → Ask what's wrong, offer to rebuild
- Low confidence → Warn and offer interactive mode
- `help` → Explain the suggestion
**Path B: No staged changes (Interactive Builder)**
1. Inform: "No staged changes found. Let's build the commit message."
2. Ask: "What did you change?" (get description)
3. Suggest type based on description
4. Build interactively:
- Type selection
- Optional scope
- Breaking change check
- Description refinement
- Optional body
- Optional footer
5. Present final formatted message
**Path C: User provided description (Manual Mode)**
If user said "help me commit - I added OAuth", skip analysis:
1. Extract what they did from their message
2. Suggest commit type
3. Build message from their description
4. Present formatted result
**Key principle:** Be adaptive. Use automation when possible, fall back to interactive when needed.
### Validation Mode (/validate)
Check user's commit message:
1. Parse the message
2. Check format, type, description rules
3. Give specific feedback on issues
4. Suggest corrections
### Changelog Mode (/changelog)
Generate formatted changelog:
1. Run `git log` to get commits since last tag/version
2. Group by type (features, fixes, breaking changes)
3. Format as markdown with headers
4. Present organized changelog
### Version Mode (/version)
Calculate next semantic version:
1. Analyze commits since last release
2. Check for breaking changes (major bump)
3. Check for features/fixes (minor bump)
4. Default to patch bump
5. Present: "Next version: 2.0.0 (major bump due to breaking change)"
### Fix Mode (/fix)
Help amend last commit:
1. Show last commit message
2. Ask what needs fixing
3. Suggest `git commit --amend` with corrected message
4. Or suggest interactive rebase for older commits
## Examples to Reference
See references/examples.md for comprehensive examples when:
- User asks for examples
- Situation is complex or ambiguous
- Breaking changes are involved
## Validation
When validating messages, check:
- Valid type from approved list
- Lowercase description (unless proper noun)
- No period at end
- Under 100 chars
- Breaking change indicator matches footer
- Imperative mood (heuristic: avoid past tense words)
Give friendly, actionable feedback.
## Script Integration
The skill includes Python scripts for automation:
- `scripts/analyze-diff.py` - Analyzes staged changes, suggests commits
- `scripts/validate.py` - Validates commit format (can be git hook)
Use these when appropriate for the workflow.
## Tone
- **Be conversational** - Not academic or overly formal
- **Be helpful** - Guide don't lecture
- **Be concise** - Get to the commit message quickly
- **Be practical** - Focus on their actual change
- **Be smart** - Use automation when possible
## Anti-patterns
Don't:
- Overwhelm with options or theory upfront
- Ask too many questions when you can analyze the diff
- Make users read documentation
- Reference the skill system itself
Do:
- Listen to what they did OR analyze their code
- Suggest a good commit immediately
- Explain briefly why if asked
- Make it easy and fast

View File

@@ -0,0 +1,161 @@
# Commit Examples Reference
Quick examples for Claude to reference when helping users.
## Common Scenarios
### New Feature
```
feat(auth): add OAuth2 login support
Integrate Google and GitHub OAuth providers with
automatic account linking for existing users.
Closes #234
```
### Bug Fix
```
fix(api): prevent null pointer in user lookup
Add null check before accessing user.profile to avoid
crashes when profile hasn't been initialized yet.
```
### Breaking Change
```
feat(api)!: change response format to include metadata
BREAKING CHANGE:
All API responses now return {data, meta} instead of
raw data. Update clients to access response.data.
Migration: https://docs.example.com/v2-migration
```
### Performance
```
perf(database): add composite index on user queries
Reduces average query time from 2.1s to 45ms for
user search endpoint.
```
### Refactor
```
refactor(auth): extract validation to separate module
Move authentication validation logic from controllers
to dedicated service for better testability.
```
### Documentation
```
docs(api): add OpenAPI specification
Add complete API documentation with request/response
examples for all endpoints.
```
### Simple Commits
```
feat: add email notifications
fix: correct timezone handling
docs: update README
style: format with prettier
test: add unit tests for auth
build: update dependencies
chore: update .gitignore
```
## Edge Cases
### Multiple Related Issues
```
fix(payment): resolve duplicate charge issues
Fixes #123, #456, #789
```
### Revert
```
revert: "feat(api): add caching layer"
This reverts commit a1b2c3d. Caching caused data
staleness issues in production.
```
### Initial Commit
```
chore: init
```
## Breaking Change Patterns
### API Change
```
feat(api)!: remove deprecated v1 endpoints
BREAKING CHANGE:
Removed endpoints:
- GET /v1/users
- POST /v1/users
Use /v2/users instead with updated auth headers.
```
### Database Schema
```
refactor(db)!: normalize user table structure
BREAKING CHANGE:
Split user.name into user.firstName and user.lastName.
Run migration: npm run migrate:user-schema
```
### Config Change
```
build!: require Node.js 18+
BREAKING CHANGE:
Node.js 16 is no longer supported. Upgrade to Node 18
before deploying this version.
```
## What NOT to Do
### Too Vague
```
❌ fix: bug fix
✅ fix(auth): prevent session timeout on refresh
```
### Past Tense
```
❌ feat: added new feature
✅ feat: add new feature
```
### Capitalized
```
❌ feat: Add new feature
✅ feat: add new feature
```
### With Period
```
❌ feat: add new feature.
✅ feat: add new feature
```
### Issue as Scope
```
❌ feat(#123): add feature
✅ feat(api): add feature
Closes #123
```

View File

@@ -0,0 +1,451 @@
# Slash Commands Reference
Detailed workflows for each slash command.
## /commit - Smart Commit Helper
**Trigger:** User types `/commit` or says "help me commit"
**Smart Workflow (Adaptive):**
### Stage 1: Detection
Check for staged changes:
```bash
git diff --staged --name-only
```
### Stage 2: Path Selection
**If staged changes exist → Smart Analysis Path**
1. **Run analysis:**
```bash
python scripts/analyze-diff.py --json
```
2. **Present suggestion:**
```
📊 Analyzed your staged changes:
Files changed: 3
auth/oauth.py | 45 +++++++++++++++
auth/tokens.py | 23 ++++++++
tests/auth.py | 67 ++++++++++++++++++++++
Detected:
• Type: feat (new functions found)
• Scope: auth (from file paths)
• Description: add OAuth2 authentication
• Confidence: High (85%)
Suggested commit:
git commit -m"feat(auth): add OAuth2 authentication"
Does this look good? (y/n/edit/help)
```
3. **Handle response:**
- **`y` or "yes" or "looks good"** → Provide final command or execute
- **`n` or "no"** → Ask "What should I change?" then rebuild
- **`edit`** → Let them modify specific parts (type, scope, description)
- **`help`** → Explain why each part was suggested
- **Low confidence (<50%)** → Add warning: "⚠️ Low confidence. Would you like to build it step-by-step instead?"
**If no staged changes → Interactive Builder Path**
1. **Inform user:**
```
No staged changes found.
Tip: Stage your changes first with:
git add <files>
Or I can help you build the message. What did you change?
```
2. **Get description:**
- Wait for user to describe their change
- If they want to stage first, guide them
3. **Build interactively:**
- Suggest type based on description
- Ask for scope (optional)
- Check breaking change
- Refine description
- Add body if needed
- Add footer if needed
4. **Present final:**
```
Your commit message:
feat(auth): add OAuth2 login
Git command:
git commit -m"feat(auth): add OAuth2 login"
```
**If user provided description in request → Manual Path**
Example: "help me commit - I added OAuth login"
1. **Extract information:**
- What they did: "added OAuth login"
- Infer type: feat (adding something new)
- Suggest scope: auth (OAuth is authentication)
2. **Build message:**
```
Based on your description:
git commit -m"feat(auth): add OAuth login"
Want me to add more details? (y/n)
```
3. **Offer refinement:**
- If yes: ask about body, footer
- If no: done
### Key Principles
1. **Be smart:** Use automation when possible
2. **Be flexible:** Fall back to interactive when needed
3. **Be clear:** Always show what was detected and why
4. **Be helpful:** Offer next steps at each stage
## /analyze - REMOVED
**Note:** This command has been merged into `/commit`. The smart analysis is now the default first step when using `/commit` with staged changes.
Users can still trigger it by:
- Typing `/commit`
- Saying "help me commit"
- Saying "analyze my changes"
## /validate - Message Validation
**Trigger:** User types `/validate <message>` or "Is this valid: ..."
**Workflow:**
1. Extract message (after `/validate` or from context)
2. Run validation checks:
- Format: matches `type(scope): description` pattern
- Type: in approved list
- Scope: valid format if present
- Description: lowercase, no period, under 100 chars
- Breaking change: indicator matches footer
3. Present results:
**If valid:**
```
✅ Valid commit message!
Type: feat
Scope: auth
Description: add OAuth login
```
**If invalid:**
```
❌ Invalid commit message
Issues:
• Description starts with uppercase (should be lowercase)
• Description ends with period (should not)
Suggested fix:
feat(auth): add OAuth login
Original:
feat(auth): Add OAuth login.
```
## /types - Show Commit Types
**Trigger:** User types `/types` or asks "what commit types..."
**Response:**
```
📝 Conventional Commit Types:
🆕 feat - New feature for users/API
Example: feat(auth): add OAuth login
🐛 fix - Bug fix for users/API
Example: fix(api): prevent null pointer
♻️ refactor - Code change without behavior change
Example: refactor(auth): extract validation logic
⚡ perf - Performance improvement
Example: perf(db): add index to user queries
💄 style - Code formatting only
Example: style: apply prettier formatting
✅ test - Add or fix tests
Example: test(auth): add OAuth flow tests
📚 docs - Documentation changes
Example: docs: update API reference
📦 build - Build system or dependencies
Example: build: upgrade to React 18
🔧 ops - Infrastructure or deployment
Example: ops(k8s): increase replica count
🏗️ chore - Miscellaneous maintenance
Example: chore: update .gitignore
Need help choosing? Describe your change and I'll suggest the right type.
```
## /scopes - Scope Guide
**Trigger:** User types `/scopes` or asks about scopes
**Response:**
```
📍 Scopes in Conventional Commits
A scope provides context about what part of the codebase changed.
**Format:**
- Optional (can omit)
- Lowercase
- Alphanumeric + hyphens
- 1-20 characters
- Based on your project structure
**Common patterns:**
By module/component:
feat(auth): ...
fix(api): ...
refactor(database): ...
By feature area:
feat(payments): ...
fix(checkout): ...
By technical layer:
refactor(ui): ...
perf(backend): ...
**Examples:**
✅ feat(auth): add login
✅ fix(shopping-cart): prevent empty checkout
✅ refactor(api-v2): restructure endpoints
❌ feat(#123): add feature (don't use issue numbers)
❌ feat(AUTHENTICATION): ... (not uppercase)
❌ feat(this-is-way-too-long-scope): ... (max 20 chars)
**Your project:**
Based on your file structure, consider scopes like:
[analyze their repo structure and suggest relevant scopes]
```
## /breaking - Breaking Change Guide
**Trigger:** User types `/breaking` or mentions breaking changes
**Response:**
```
⚠️ Breaking Changes Guide
A breaking change is any modification that requires users to change their code.
**When to mark as breaking:**
• Removed public API endpoints
• Changed function signatures
• Modified response formats
• Removed configuration options
• Changed database schema
• Updated minimum version requirements
**How to mark breaking changes:**
1. Add `!` before the colon:
feat(api)!: remove deprecated endpoints
2. Add BREAKING CHANGE in footer:
feat(api)!: remove deprecated v1 endpoints
BREAKING CHANGE:
All v1 API endpoints have been removed. Clients must
migrate to v2 API with updated authentication.
Migration guide: docs/v1-to-v2-migration.md
**Template:**
```
<type>(<scope>)!: <description>
<optional body explaining the change>
BREAKING CHANGE:
What broke: <explain what no longer works>
Why: <reason for the breaking change>
Migration: <how to update code>
```
**Example:**
```
refactor(auth)!: change token format to JWT
Replace custom token format with industry-standard JWT
for better security and third-party integration.
BREAKING CHANGE:
Auth tokens are now JWT format instead of custom base64.
Migration steps:
1. Update token parsing: use jwt.decode() instead of base64
2. Update token validation: verify JWT signature
3. Existing tokens will be invalidated - users must re-login
See: docs/jwt-migration.md
```
**Version impact:**
Breaking changes trigger a MAJOR version bump (1.x.x → 2.0.0)
```
## /changelog - Generate Changelog
**Trigger:** User types `/changelog` or asks to generate changelog
**Workflow:**
1. Get version info: "From which version/tag? (default: last tag)"
2. Run git log: `git log <version>..HEAD --format=%s`
3. Parse conventional commits
4. Group by type:
- Breaking Changes (top priority)
- Features
- Bug Fixes
- Other (refactor, perf, docs, etc.)
5. Format as markdown:
```markdown
## [2.0.0] - 2025-11-15
### ⚠️ BREAKING CHANGES
- **auth**: change token format to JWT (#234)
Migration required. See docs/jwt-migration.md
### ✨ Features
- **auth**: add OAuth2 login (#123)
- **api**: add user search endpoint (#145)
- **ui**: add dark mode toggle (#167)
### 🐛 Bug Fixes
- **api**: prevent null pointer in user lookup (#156)
- **ui**: fix date picker timezone issue (#178)
### ⚡ Performance
- **database**: add index to user queries (#189)
### 📚 Documentation
- update API reference (#190)
- add authentication guide (#191)
```
## /version - Semantic Versioning
**Trigger:** User types `/version` or asks "what should the next version be"
**Workflow:**
1. Get current version: `git describe --tags --abbrev=0` or ask user
2. Analyze commits since that version
3. Apply semantic versioning rules:
- **Major** (X.0.0): Has breaking changes
- **Minor** (0.X.0): Has features or fixes, no breaking changes
- **Patch** (0.0.X): Only other changes
4. Present analysis:
```
📊 Version Analysis
Current version: 1.2.3
Analyzing commits since v1.2.3...
Found:
• 1 breaking change
• 5 features
• 3 bug fixes
• 7 other commits
Recommended: 2.0.0 (Major)
Reason: Breaking change detected
Breaking commits:
• feat(api)!: remove deprecated v1 endpoints
Create this version?
git tag -a v2.0.0 -m "Release 2.0.0"
git push origin v2.0.0
```
## /examples - Show Examples
**Trigger:** User types `/examples` or asks for examples
**Response:**
Load and display relevant examples from references/examples.md based on context:
- If discussing features → show feature examples
- If discussing fixes → show fix examples
- If discussing breaking changes → show breaking change examples
- Default → show variety of examples
## /fix - Amend Last Commit
**Trigger:** User types `/fix` or "I need to fix my last commit"
**Workflow:**
1. Get last commit: `git log -1 --format=%s`
2. Show current message: "Your last commit: `feat: add login`"
3. Ask: "What needs fixing?"
4. Common scenarios:
**Wrong message:**
```
Current: feat: add login
New message: feat(auth): add OAuth login
Command:
git commit --amend -m"feat(auth): add OAuth login"
```
**Forgot files:**
```
Stage missing files:
git add forgotten-file.py
git commit --amend --no-edit
```
**Older commit:**
```
To fix an older commit, use interactive rebase:
git rebase -i HEAD~3
Then mark the commit as 'edit' or 'reword'
```
## General Response Pattern
For all slash commands:
1. Acknowledge the command
2. Execute the workflow
3. Present clear, actionable output
4. Offer next steps
5. Be concise but complete

View File

@@ -0,0 +1,402 @@
#!/usr/bin/env python3
"""
Analyze staged git changes and suggest commit messages.
Usage:
python analyze-diff.py # Analyze staged changes
python analyze-diff.py --commit HEAD # Analyze specific commit
python analyze-diff.py --file path.py # Analyze specific file
"""
import subprocess
import sys
import re
from pathlib import Path
from typing import Dict, List, Tuple, Optional
from collections import defaultdict
class DiffAnalyzer:
"""Analyze git diffs to suggest commit messages."""
# Map file patterns to scopes
SCOPE_PATTERNS = {
r'.*/(auth|login|oauth)': 'auth',
r'.*/(api|endpoints|routes)': 'api',
r'.*/database|migrations': 'database',
r'.*/tests?/': 'test',
r'.*/(ui|components|views)': 'ui',
r'.*/docs?/': 'docs',
r'.*/(config|settings)': 'config',
r'.*\.github/': 'ci',
r'Dockerfile|docker-compose': 'docker',
r'.*/(deploy|infra|terraform)': 'ops',
}
# Keywords in diff that suggest commit types
TYPE_KEYWORDS = {
'feat': [
'add', 'create', 'implement', 'introduce', 'new',
'class', 'function', 'feature', 'endpoint', 'component'
],
'fix': [
'fix', 'bug', 'issue', 'error', 'crash', 'correct',
'resolve', 'patch', 'repair'
],
'refactor': [
'refactor', 'restructure', 'reorganize', 'extract',
'rename', 'move', 'cleanup', 'simplify'
],
'perf': [
'optimize', 'performance', 'faster', 'speed', 'cache',
'index', 'query', 'efficient'
],
'style': [
'format', 'lint', 'prettier', 'whitespace', 'indent'
],
'test': [
'test', 'spec', 'coverage', 'mock', 'fixture'
],
'docs': [
'readme', 'documentation', 'comment', 'docstring'
],
'build': [
'package.json', 'requirements.txt', 'dependencies',
'dependency', 'upgrade', 'bump'
],
}
def __init__(self):
self.git_root = self._get_git_root()
def _get_git_root(self) -> Optional[Path]:
"""Get git repository root."""
try:
result = subprocess.run(
['git', 'rev-parse', '--show-toplevel'],
capture_output=True,
text=True,
check=True
)
return Path(result.stdout.strip())
except subprocess.CalledProcessError:
return None
def _run_git(self, args: List[str]) -> str:
"""Run git command and return output."""
try:
result = subprocess.run(
['git'] + args,
capture_output=True,
text=True,
check=True
)
return result.stdout
except subprocess.CalledProcessError as e:
return ""
def get_staged_changes(self) -> Dict[str, any]:
"""Get information about staged changes."""
# Get list of changed files
files_output = self._run_git(['diff', '--staged', '--name-status'])
if not files_output:
return None
files = []
for line in files_output.strip().split('\n'):
if not line:
continue
parts = line.split('\t', 1)
status = parts[0]
filepath = parts[1] if len(parts) > 1 else ''
files.append({
'path': filepath,
'status': status, # A=added, M=modified, D=deleted
})
# Get the actual diff
diff = self._run_git(['diff', '--staged'])
# Get stats
stats_output = self._run_git(['diff', '--staged', '--stat'])
return {
'files': files,
'diff': diff,
'stats': stats_output,
}
def infer_scope(self, files: List[Dict]) -> Optional[str]:
"""Infer scope from changed file paths."""
scopes = []
for file in files:
path = file['path'].lower()
for pattern, scope in self.SCOPE_PATTERNS.items():
if re.search(pattern, path):
scopes.append(scope)
break
if not scopes:
# Try to extract from path
for file in files:
path = Path(file['path'])
if len(path.parts) > 1:
# Use first directory as scope
potential_scope = path.parts[0]
if potential_scope not in ['src', 'lib', 'app']:
return potential_scope[:20] # Truncate to 20 chars
# Return most common scope
if scopes:
return max(set(scopes), key=scopes.count)
return None
def infer_type(self, files: List[Dict], diff: str) -> Tuple[str, float]:
"""
Infer commit type from changes.
Returns (type, confidence) where confidence is 0-1.
"""
# Check file status first
has_new_files = any(f['status'] == 'A' for f in files)
has_deletions = any(f['status'] == 'D' for f in files)
only_tests = all('test' in f['path'].lower() for f in files)
only_docs = all(
f['path'].lower().endswith(('.md', '.txt', '.rst'))
for f in files
)
if only_tests:
return 'test', 0.9
if only_docs:
return 'docs', 0.9
# Analyze diff content
diff_lower = diff.lower()
type_scores = defaultdict(int)
for commit_type, keywords in self.TYPE_KEYWORDS.items():
for keyword in keywords:
# Count occurrences in added lines
added_lines = [
line for line in diff.split('\n')
if line.startswith('+') and not line.startswith('+++')
]
for line in added_lines:
if keyword in line.lower():
type_scores[commit_type] += 1
# Adjust scores based on file status
if has_new_files:
type_scores['feat'] += 5
if has_deletions and not has_new_files:
type_scores['refactor'] += 2
# Get best guess
if not type_scores:
return 'chore', 0.3
best_type = max(type_scores.items(), key=lambda x: x[1])
confidence = min(best_type[1] / 10, 1.0) # Normalize to 0-1
return best_type[0], confidence
def generate_description(self,
files: List[Dict],
diff: str,
commit_type: str) -> str:
"""Generate a description based on changes."""
# Extract function/class names from diff
added_patterns = [
r'\+.*def (\w+)', # Python functions
r'\+.*function (\w+)', # JS functions
r'\+.*class (\w+)', # Classes
r'\+.*const (\w+)', # Constants
]
entities = []
for pattern in added_patterns:
matches = re.findall(pattern, diff)
entities.extend(matches[:3]) # Limit to first 3
# Generate description based on type
if commit_type == 'feat' and entities:
entity = entities[0]
return f"add {entity}"
elif commit_type == 'fix':
# Look for bug-related keywords in diff
if 'null' in diff.lower() and 'check' in diff.lower():
return "prevent null pointer exception"
elif 'error' in diff.lower():
return "fix error handling"
else:
return "fix bug"
elif commit_type == 'refactor':
if entities:
return f"extract {entities[0]} logic"
return "restructure code"
elif commit_type == 'perf':
if 'cache' in diff.lower():
return "add caching"
elif 'index' in diff.lower():
return "optimize database queries"
return "improve performance"
elif commit_type == 'docs':
return "update documentation"
elif commit_type == 'test':
if entities:
return f"add tests for {entities[0]}"
return "add tests"
# Default descriptions
action = 'add' if any(f['status'] == 'A' for f in files) else 'update'
if len(files) == 1:
filename = Path(files[0]['path']).stem
return f"{action} {filename}"
else:
return f"{action} {len(files)} files"
def is_breaking_change(self, diff: str) -> Tuple[bool, Optional[str]]:
"""Detect if this might be a breaking change."""
breaking_indicators = [
(r'\-.*public ', 'removed public API'),
(r'\-.*export ', 'removed exports'),
(r'BREAKING CHANGE', 'explicitly marked'),
(r'\-.*@deprecated', 'removed deprecated feature'),
]
for pattern, reason in breaking_indicators:
if re.search(pattern, diff, re.IGNORECASE):
return True, reason
return False, None
def analyze(self) -> Optional[Dict]:
"""Analyze staged changes and return suggestions."""
if not self.git_root:
return {
'error': 'Not in a git repository'
}
changes = self.get_staged_changes()
if not changes:
return {
'error': 'No staged changes found. Use: git add <files>'
}
files = changes['files']
diff = changes['diff']
# Infer commit components
scope = self.infer_scope(files)
commit_type, confidence = self.infer_type(files, diff)
description = self.generate_description(files, diff, commit_type)
is_breaking, breaking_reason = self.is_breaking_change(diff)
return {
'type': commit_type,
'scope': scope,
'description': description,
'confidence': confidence,
'breaking': is_breaking,
'breaking_reason': breaking_reason,
'files_changed': len(files),
'stats': changes['stats'],
}
def main():
"""Main entry point."""
import argparse
import json
parser = argparse.ArgumentParser(
description='Analyze git changes and suggest commits'
)
parser.add_argument(
'--json',
action='store_true',
help='Output as JSON'
)
parser.add_argument(
'--commit',
action='store_true',
help='Generate and execute git commit (interactive)'
)
args = parser.parse_args()
analyzer = DiffAnalyzer()
result = analyzer.analyze()
if not result:
print("No changes to analyze")
sys.exit(1)
if 'error' in result:
print(f"Error: {result['error']}")
sys.exit(1)
if args.json:
print(json.dumps(result, indent=2))
sys.exit(0)
# Pretty output
print("📊 Analyzed your changes:\n")
print(f"Files changed: {result['files_changed']}")
print(result['stats'])
print()
# Build commit message
commit_msg = result['type']
if result['scope']:
commit_msg += f"({result['scope']})"
if result['breaking']:
commit_msg += "!"
commit_msg += f": {result['description']}"
print("💡 Suggested commit:\n")
print(f" {commit_msg}")
print()
if result['confidence'] < 0.5:
print("⚠️ Low confidence - please review and adjust")
print()
if result['breaking']:
print(f"⚠️ Possible breaking change detected: {result['breaking_reason']}")
print(" Consider adding BREAKING CHANGE: in commit body")
print()
if args.commit:
response = input("Execute this commit? [y/N]: ")
if response.lower() == 'y':
subprocess.run(['git', 'commit', '-m', commit_msg])
print("✓ Committed!")
else:
print("Copy and adjust as needed:")
print(f" git commit -m\"{commit_msg}\"")
else:
print("To commit:")
print(f" git commit -m\"{commit_msg}\"")
print()
print("To auto-commit next time:")
print(" python analyze-diff.py --commit")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,251 @@
#!/usr/bin/env python3
"""
Generate changelog from conventional commits.
Usage:
python changelog.py # Since last tag
python changelog.py --from v1.0.0 # Since specific version
python changelog.py --version 2.0.0 # Add version header
"""
import subprocess
import sys
import re
from typing import List, Dict, Optional
from collections import defaultdict
from datetime import datetime
class Commit:
"""Parsed commit."""
def __init__(self, hash: str, message: str):
self.hash = hash
self.message = message
self.type = None
self.scope = None
self.breaking = False
self.description = None
self._parse()
def _parse(self):
"""Parse conventional commit message."""
header = self.message.split('\n')[0]
# Match: type(scope)!: description
pattern = r'^(?P<type>\w+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s*(?P<desc>.+)$'
match = re.match(pattern, header)
if match:
self.type = match.group('type')
self.scope = match.group('scope')
self.breaking = bool(match.group('breaking'))
self.description = match.group('desc')
# Check for BREAKING CHANGE in body
if 'BREAKING CHANGE:' in self.message:
self.breaking = True
@property
def is_valid(self):
"""Check if this is a valid conventional commit."""
return self.type is not None
class ChangelogGenerator:
"""Generate formatted changelog."""
TYPE_HEADERS = {
'feat': '### ✨ Features',
'fix': '### 🐛 Bug Fixes',
'perf': '### ⚡ Performance',
'refactor': '### ♻️ Refactoring',
'docs': '### 📚 Documentation',
'style': '### 💄 Styling',
'test': '### ✅ Tests',
'build': '### 📦 Build',
'ops': '### 🔧 Operations',
'chore': '### 🏗️ Chores',
}
TYPE_ORDER = [
'feat', 'fix', 'perf', 'refactor',
'docs', 'style', 'test', 'build', 'ops', 'chore'
]
def __init__(self, include_hash: bool = False):
self.include_hash = include_hash
def get_commits(self, from_ref: Optional[str] = None) -> List[Commit]:
"""Get commits from git log."""
cmd = ['git', 'log', '--format=%H%n%B%n---END---']
if from_ref:
cmd.insert(2, f'{from_ref}..HEAD')
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
except subprocess.CalledProcessError:
return []
commits = []
lines = result.stdout.split('\n')
current_hash = None
current_message = []
for line in lines:
if not current_hash:
current_hash = line.strip()
elif line == '---END---':
if current_hash and current_message:
message = '\n'.join(current_message)
commits.append(Commit(current_hash, message))
current_hash = None
current_message = []
else:
current_message.append(line)
return commits
def group_commits(self, commits: List[Commit]) -> Dict[str, List[Commit]]:
"""Group commits by type."""
breaking = []
by_type = defaultdict(list)
for commit in commits:
if not commit.is_valid:
continue
if commit.breaking:
breaking.append(commit)
by_type[commit.type].append(commit)
return {
'breaking': breaking,
'by_type': by_type
}
def format_commit(self, commit: Commit) -> str:
"""Format a single commit line."""
parts = []
if commit.scope:
parts.append(f"**{commit.scope}**:")
parts.append(commit.description)
if self.include_hash:
parts.append(f"([`{commit.hash[:7]}`])")
return '- ' + ' '.join(parts)
def generate(
self,
from_ref: Optional[str] = None,
version: Optional[str] = None,
date: Optional[str] = None
) -> str:
"""Generate complete changelog."""
commits = self.get_commits(from_ref)
if not commits:
return "No commits found."
grouped = self.group_commits(commits)
lines = []
# Version header
if version:
header = f"## [{version}]"
if date:
header += f" - {date}"
lines.append(header)
lines.append("")
# Breaking changes first
if grouped['breaking']:
lines.append("### ⚠️ BREAKING CHANGES")
lines.append("")
for commit in grouped['breaking']:
lines.append(self.format_commit(commit))
# Add BREAKING CHANGE description if available
for line in commit.message.split('\n'):
if line.startswith('BREAKING CHANGE:'):
detail = line.replace('BREAKING CHANGE:', '').strip()
if detail:
lines.append(f" - {detail}")
lines.append("")
# Group by type
for commit_type in self.TYPE_ORDER:
if commit_type not in grouped['by_type']:
continue
type_commits = grouped['by_type'][commit_type]
if not type_commits:
continue
lines.append(self.TYPE_HEADERS[commit_type])
lines.append("")
for commit in type_commits:
lines.append(self.format_commit(commit))
lines.append("")
return '\n'.join(lines)
def get_latest_tag() -> Optional[str]:
"""Get latest git tag."""
try:
result = subprocess.run(
['git', 'describe', '--tags', '--abbrev=0'],
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError:
return None
def main():
"""Main entry point."""
import argparse
parser = argparse.ArgumentParser(description='Generate changelog')
parser.add_argument('--from', dest='from_ref', help='Start from this ref')
parser.add_argument('--version', help='Version for header')
parser.add_argument('--date', help='Date for header (default: today)')
parser.add_argument('--include-hash', action='store_true', help='Include commit hashes')
parser.add_argument('--output', help='Output file (default: stdout)')
args = parser.parse_args()
# Get from ref
from_ref = args.from_ref
if not from_ref:
from_ref = get_latest_tag()
if from_ref:
print(f"# Generating changelog since {from_ref}", file=sys.stderr)
# Generate
generator = ChangelogGenerator(include_hash=args.include_hash)
date = args.date or datetime.now().strftime('%Y-%m-%d')
changelog = generator.generate(from_ref, args.version, date)
# Output
if args.output:
with open(args.output, 'w') as f:
f.write(changelog)
print(f"Changelog written to {args.output}", file=sys.stderr)
else:
print(changelog)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""
Simple commit message validator for git hooks.
Usage:
As git hook: cp validate.py .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg
Standalone: python validate.py --message "feat: add feature"
From file: python validate.py commit-msg-file
"""
import sys
import re
def validate_commit(message):
"""Validate commit message, return (is_valid, error_message)."""
header = message.split('\n')[0]
# Special formats are always valid
if (header.startswith('Merge branch') or
header.startswith('Revert') or
header == 'chore: init'):
return True, None
# Standard format
pattern = (
r'^(feat|fix|refactor|perf|style|test|docs|build|ops|chore)'
r'(\([a-z0-9-]+\))?'
r'!?'
r': '
r'.{1,100}$'
)
if not re.match(pattern, header):
return False, (
f"Invalid format: {header}\n\n"
f"Expected: <type>(<scope>): <description>\n"
f"Example: feat(auth): add login\n\n"
f"Valid types: feat, fix, refactor, perf, style, test, docs, build, ops, chore"
)
# Additional checks
desc = header.split(': ', 1)[1]
if desc[0].isupper():
return False, "Description should start with lowercase"
if desc.endswith('.'):
return False, "Description should not end with period"
return True, None
def main():
import argparse
parser = argparse.ArgumentParser(description='Validate conventional commits')
parser.add_argument('file', nargs='?', help='Commit message file')
parser.add_argument('--message', help='Validate message directly')
args = parser.parse_args()
# Get message
if args.message:
message = args.message
elif args.file:
with open(args.file) as f:
message = f.read()
else:
message = sys.stdin.read()
# Validate
valid, error = validate_commit(message.strip())
if valid:
print("✓ Valid commit message")
sys.exit(0)
else:
print(f"{error}")
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""
Calculate next semantic version from commits.
Usage:
python version.py # Auto-detect current, suggest next
python version.py --current 1.2.3 # Start from specific version
python version.py --verbose # Show detailed analysis
"""
import subprocess
import sys
import re
from typing import Optional, Tuple
class Version:
"""Semantic version."""
def __init__(self, major: int, minor: int, patch: int):
self.major = major
self.minor = minor
self.patch = patch
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
@classmethod
def parse(cls, version_str: str) -> 'Version':
"""Parse version string."""
version_str = version_str.lstrip('v')
parts = version_str.split('.')
if len(parts) != 3:
raise ValueError(f"Invalid version: {version_str}")
return cls(int(parts[0]), int(parts[1]), int(parts[2]))
def bump_major(self) -> 'Version':
"""Bump major version."""
return Version(self.major + 1, 0, 0)
def bump_minor(self) -> 'Version':
"""Bump minor version."""
return Version(self.major, self.minor + 1, 0)
def bump_patch(self) -> 'Version':
"""Bump patch version."""
return Version(self.major, self.minor, self.patch + 1)
class CommitAnalyzer:
"""Analyze commits for versioning."""
def __init__(self):
self.breaking_commits = []
self.feature_commits = []
self.fix_commits = []
self.other_commits = []
def analyze(self, from_ref: Optional[str] = None):
"""Analyze commits since ref."""
commits = self._get_commits(from_ref)
for commit in commits:
self._classify_commit(commit)
def _get_commits(self, from_ref: Optional[str]) -> list:
"""Get commit messages."""
cmd = ['git', 'log', '--format=%s']
if from_ref:
cmd.insert(2, f'{from_ref}..HEAD')
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return [line.strip() for line in result.stdout.split('\n') if line.strip()]
except subprocess.CalledProcessError:
return []
def _classify_commit(self, message: str):
"""Classify a commit message."""
# Check for breaking change
if '!' in message and ':' in message:
if message.index('!') < message.index(':'):
self.breaking_commits.append(message)
return
# Parse type
pattern = r'^(?P<type>\w+)(?:\([^)]+\))?:\s*'
match = re.match(pattern, message)
if not match:
self.other_commits.append(message)
return
commit_type = match.group('type')
if commit_type == 'feat':
self.feature_commits.append(message)
elif commit_type == 'fix':
self.fix_commits.append(message)
else:
self.other_commits.append(message)
def get_bump_type(self) -> Tuple[str, str]:
"""
Get version bump type and reason.
Returns:
(bump_type, reason) where bump_type is 'major', 'minor', or 'patch'
"""
if self.breaking_commits:
return 'major', f'{len(self.breaking_commits)} breaking change(s)'
if self.feature_commits or self.fix_commits:
feat_count = len(self.feature_commits)
fix_count = len(self.fix_commits)
parts = []
if feat_count:
parts.append(f'{feat_count} feature(s)')
if fix_count:
parts.append(f'{fix_count} fix(es)')
return 'minor', ', '.join(parts)
return 'patch', f'{len(self.other_commits)} other change(s)'
def get_next_version(self, current: Version) -> Tuple[Version, str, str]:
"""
Get next version.
Returns:
(next_version, bump_type, reason)
"""
bump_type, reason = self.get_bump_type()
if bump_type == 'major':
next_version = current.bump_major()
elif bump_type == 'minor':
next_version = current.bump_minor()
else:
next_version = current.bump_patch()
return next_version, bump_type, reason
def get_latest_tag() -> Optional[str]:
"""Get latest git tag."""
try:
result = subprocess.run(
['git', 'describe', '--tags', '--abbrev=0'],
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError:
return None
def main():
"""Main entry point."""
import argparse
parser = argparse.ArgumentParser(description='Calculate next version')
parser.add_argument('--current', help='Current version (default: latest tag)')
parser.add_argument('--from', dest='from_ref', help='Analyze from this ref')
parser.add_argument('--verbose', action='store_true', help='Show detailed analysis')
args = parser.parse_args()
# Get current version
if args.current:
current = Version.parse(args.current)
from_ref = args.from_ref or f'v{current}'
else:
tag = get_latest_tag()
if tag:
current = Version.parse(tag)
from_ref = args.from_ref or tag
else:
current = Version(0, 0, 0)
from_ref = args.from_ref
# Analyze commits
analyzer = CommitAnalyzer()
analyzer.analyze(from_ref)
next_version, bump_type, reason = analyzer.get_next_version(current)
if args.verbose:
print(f"📊 Version Analysis\n")
print(f"Current version: {current}")
if from_ref:
print(f"Analyzing since: {from_ref}")
print()
print("Commits found:")
print(f"{len(analyzer.breaking_commits)} breaking change(s)")
print(f"{len(analyzer.feature_commits)} feature(s)")
print(f"{len(analyzer.fix_commits)} fix(es)")
print(f"{len(analyzer.other_commits)} other change(s)")
print()
print(f"Bump type: {bump_type.upper()}")
print(f"Reason: {reason}")
print()
print(f"Next version: {next_version}")
if analyzer.breaking_commits:
print()
print("Breaking commits:")
for commit in analyzer.breaking_commits[:5]:
print(f"{commit}")
else:
print(next_version)
if __name__ == '__main__':
main()