Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal 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
73
plugin.lock.json
Normal 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
147
skills/git-commit/README.md
Normal 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
213
skills/git-commit/SKILL.md
Normal 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
|
||||||
161
skills/git-commit/references/examples.md
Normal file
161
skills/git-commit/references/examples.md
Normal 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
|
||||||
|
```
|
||||||
451
skills/git-commit/references/slash-commands.md
Normal file
451
skills/git-commit/references/slash-commands.md
Normal 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
|
||||||
402
skills/git-commit/scripts/analyze-diff.py
Executable file
402
skills/git-commit/scripts/analyze-diff.py
Executable 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()
|
||||||
251
skills/git-commit/scripts/changelog.py
Executable file
251
skills/git-commit/scripts/changelog.py
Executable 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()
|
||||||
84
skills/git-commit/scripts/validate.py
Executable file
84
skills/git-commit/scripts/validate.py
Executable 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()
|
||||||
217
skills/git-commit/scripts/version.py
Executable file
217
skills/git-commit/scripts/version.py
Executable 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()
|
||||||
Reference in New Issue
Block a user