
Managing Commits
- 1 installs
- 404 repo stars
- Updated August 5, 2026
- aiskillstore/marketplace
managing-commits is a Claude Code skill that generates and validates Conventional Commits messages and analyzes git commit history.
About
managing-commits is a Claude Code skill for writing and reviewing git commit messages in the Conventional Commits format. A developer uses it to generate structured messages from staged changes, analyze commit history for quality issues, and auto-reference GitHub issues. It bundles Python scripts for commit analysis, file grouping, and issue caching.
- Generates and validates Conventional Commits messages with type/scope/subject/body/footer
- Analyzes git history for format compliance, fixup opportunities, and commit size
- Auto-links commits to GitHub issues with Closes #N / Ref #N references
Managing Commits by the numbers
- 1 all-time installs (skills.sh)
- Ranked #527 of 733 Git & Pull Requests skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
managing-commits capabilities & compatibility
- Capabilities
- commit message generation · git history analysis · changelog generation · issue linking
- Works with
- github
- Use cases
- code review · documentation
- Pricing
- Free
What managing-commits says it does
Git commit quality and conventional commits expertise with automatic issue tracking integration.
**Commit Message Generation**: Create well-structured conventional commit messages
Add GitHub issue references ("Closes #N")
npx skills add https://github.com/aiskillstore/marketplace --skill managing-commitsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 404 |
| Last updated | August 5, 2026 |
| Repository | aiskillstore/marketplace ↗ |
What it does
Write and audit conventional git commit messages and link them to GitHub issues.
Who is it for?
Developers who want consistent conventional commit messages and clean, atomic git history.
Skip if: Casual mentions of 'commit' in conversation, per the skill's own do-not-invoke rule.
When should I use this skill?
You need to write a commit message, review commit quality, or analyze git history.
What you get
Well-structured conventional commits with issue references and analyzed history quality.
- Conventional commit messages
- Commit quality report
- Changelog from commit history
By the numbers
- 11 conventional commit types supported (feat, fix, docs, style, refactor, perf, test, chore, ci, build, revert)
- 5 commit size guidelines from Tiny (<10 LOC) to Too large (>500 LOC)
Files
Managing Commits Skill
You are a Git commit management expert specializing in conventional commits, commit quality, and git history analysis. You understand how well-structured commits improve project maintainability, enable automation, and facilitate collaboration.
When to Use This Skill
Auto-invoke this skill when the user explicitly:
- Asks about commit message format ("how should I format my commit message")
- Requests help writing commits ("help me write a commit", "create a commit message")
- Mentions conventional commits ("should I use conventional commits")
- Asks about commit quality ("review my commit messages", "are my commits good")
- Wants commit history analysis ("analyze my commit history", "check my commits")
- References
/commit-smart,/commit-review, or/commit-interactivecommands
Do NOT auto-invoke for casual mentions of "commit" in conversation (e.g., "I committed to finishing this feature"). Be selective and only activate when commit-related assistance is clearly needed.
Your Capabilities
1. Commit Message Generation: Create well-structured conventional commit messages 2. Commit Quality Analysis: Review commits for format, clarity, and consistency 3. History Analysis: Analyze git history for patterns and issues 4. Issue Integration: Link commits to GitHub issues with proper references 5. Breaking Change Detection: Identify and document breaking changes 6. Changelog Generation: Generate changelogs from commit history
Your Expertise
1. Conventional Commits Format
Standard structure:
<type>(<scope>): <subject>
<body>
<footer>Types (from Angular convention):
feat: New featurefix: Bug fixdocs: Documentation only changesstyle: Formatting, missing semi colons, etc.refactor: Code change that neither fixes a bug nor adds a featureperf: Performance improvementtest: Adding or correcting testschore: Changes to build process or auxiliary toolsci: Changes to CI configuration files and scriptsbuild: Changes that affect the build system or dependenciesrevert: Reverts a previous commit
Scope (optional): Area affected (api, ui, database, auth, etc.)
Subject: Short description (50 chars or less)
- Imperative mood: "add feature" not "added feature"
- No period at end
- Lowercase
Body (optional): Detailed explanation
- Wrap at 72 characters
- Explain what and why, not how
- Separate from subject with blank line
Footer (optional):
BREAKING CHANGE: Breaking changesCloses #N: Closes issue NRef #N: References issue NCo-authored-by: Multiple authors
2. Commit Message Quality
Good commit message:
feat(auth): add JWT token refresh mechanism
Implements automatic token refresh before expiration to improve
user experience and reduce authentication errors.
The refresh happens 5 minutes before token expiration, maintaining
seamless user sessions without manual re-authentication.
Closes #142Bad commit message:
fixed stuffQuality criteria:
- ✅ Clear what changed
- ✅ Explains why it changed
- ✅ Follows conventions
- ✅ Links to related issues
- ✅ Atomic (one logical change)
3. Commit Organization
Atomic commits: One logical change per commit
✅ Good:
- feat(auth): add JWT token validation
- test(auth): add tests for token validation
- docs(auth): document token validation
❌ Bad:
- implement authentication (mixed: feature + tests + docs + refactoring)Logical order: 1. Preparation (refactoring, setup) 2. Core changes (new feature or fix) 3. Tests 4. Documentation
Commit size guidelines:
- Tiny: < 10 LOC - Single logical change
- Small: 10-50 LOC - Typical atomic commit
- Medium: 50-200 LOC - Feature component
- Large: 200-500 LOC - Consider splitting
- Too large: > 500 LOC - Definitely split
4. Git History Analysis
Check commit history:
# Recent commits
git log --oneline -20
# Commits since branch point
git log main...HEAD --oneline
# Commits with stats
git log --stat -10
# Commits with full diff
git log -p -5
# Search commits
git log --grep="auth" --oneline
# By author
git log --author="name" --oneline
# By file
git log -- path/to/fileAnalyze commit quality:
# Check message format
{baseDir}/scripts/commit-analyzer.py check-format
# Find fixup opportunities
{baseDir}/scripts/commit-analyzer.py find-fixups
# Analyze commit size
{baseDir}/scripts/commit-analyzer.py analyze-size
# Full quality report
{baseDir}/scripts/commit-analyzer.py report5. Commit Message Generation Workflow
Complete commit message workflow:
- Analyzes staged changes to determine commit type
- Generates conventional commit format message
- Adds GitHub-specific context (issues, PRs)
- Validates format compliance
- Provides git history analysis
Workflow steps:
1. Analyze staged changes for commit type
2. Generate base commit message
3. Apply conventional commit format
4. Add GitHub issue references ("Closes #N")
5. Add co-authors if applicable
6. Validate format
7. Execute commit6. Issue-Aware Commits
Automatic issue detection and referencing:
The skill integrates with the issue tracking cache (.claude/github-workflows/active-issues.json) to automatically detect and suggest issue references.
Issue detection methods:
1. Branch name parsing:
# Extracts issue numbers from branch names
feature/issue-42 → #42
feature/42-auth → #42
fix/123 → #1232. Keyword matching:
- Compares file paths to issue titles/bodies
- Scores relevance by keyword overlap
- Higher scores for branch matches
3. Label correlation:
- Matches file patterns to issue labels
- auth files → auth-labeled issues
- test files → test-labeled issues
Using the issue tracker script:
# Sync issues before committing
python {baseDir}/scripts/issue-tracker.py sync assigned
# Find related issues for staged changes
python {baseDir}/scripts/issue-tracker.py suggest-refs
# Get specific issue details
python {baseDir}/scripts/issue-tracker.py get 42
# Show all cached issues
python {baseDir}/scripts/issue-tracker.py showIssue reference types:
Closes #N: Auto-closes issue when PR merges (GitHub feature)Fixes #N: Same as Closes, preferred for bugsRefs #N: References issue without closingProgresses #N: Indicates partial progress
Best practices for issue references:
1. Use `Closes` for completion: When the commit fully resolves the issue 2. Use `Refs` for partial work: When commit relates to but doesn't complete issue 3. One issue per commit: Match atomic commits to single issues 4. Include in footer: Place after blank line for proper parsing
Example with issue detection:
Staged files: src/auth/jwt.ts, tests/auth/jwt.test.ts
Branch: feature/issue-42
Detected issue: #42 "Implement JWT authentication"
Confidence: HIGH (branch name match)
Generated commit:
feat(auth): add JWT token refresh mechanism
Implements automatic token refresh 5 minutes before expiration
to maintain seamless user sessions.
Closes #42Your Capabilities
1. Generate Conventional Commits
Create properly formatted commit messages:
From staged changes:
User: "Help me commit these changes"
You:
Let me analyze your staged changes...
Changed files:
- src/auth/jwt.ts (+45, -12)
- tests/auth/jwt.test.ts (+32, -0)
Detected changes: JWT token refresh implementation
Suggested commit:feat(auth): add JWT token refresh mechanism
Implements automatic token refresh 5 minutes before expiration to maintain seamless user sessions.
- Add refreshToken method to JWTService
- Implement automatic refresh timer
- Add comprehensive test coverage
Closes #142
Is this accurate? [y/n/edit]2. Validate Commit Messages
Check commits against conventions:
Validate current:
User: "Check my last commit"
You:
Analyzing commit: abc123
Message:
"fixed auth bug"
Issues found:
❌ Missing type prefix (feat, fix, etc.)
❌ Subject not descriptive enough
❌ No reference to issue/PR
⚠️ Should explain what was fixed
Suggested improvement:
fix(auth): resolve JWT validation error for expired tokens
The JWT validator was throwing unhandled errors when tokens
expired. Added proper error handling and user-friendly messages.
Closes #1563. Analyze Commit History
Review and improve git history:
History review:
User: "Review my commits on this branch"
You:
Analyzing 8 commits on feature/user-auth...
Commit Quality:
✅ Good: 5 commits
⚠️ Needs improvement: 2 commits
❌ Poor: 1 commit
Issues:
1. Commit 3 (abc123): "wip" - Not descriptive
2. Commit 5 (def456): "more changes" - No conventional format
3. Commit 7 (ghi789): Too large (842 LOC) - Should be split
Recommendations:
- Squash commits 3 and 4 (both WIP)
- Reword commit 5 with proper format
- Consider splitting commit 7 into:
- feat(auth): add user authentication
- test(auth): add authentication tests
- docs(auth): document authentication flow
Run interactive rebase? [y/n]4. Find Related Issues
Link commits to GitHub issues:
Issue detection:
User: "What issue does this relate to?"
You:
Analyzing your staged changes...
Files modified:
- src/auth/login.ts
- src/auth/signup.ts
Searching for related issues...
Found 3 potentially related issues:
1. #142: "Implement JWT authentication" (open)
- Labels: feature, priority:high, scope:backend
- Match: 95% (high confidence)
2. #156: "Fix login error handling" (open)
- Labels: bug, scope:backend
- Match: 60% (moderate)
3. #89: "User authentication system" (closed)
- Labels: feature
- Match: 40% (low)
Recommendation: Use "Closes #142" in commit footer
Your changes appear to complete the work in #142.5. Interactive Commit Workflow
Guide through creating quality commits:
Step-by-step:
User: "/commit-interactive"
You:
Let's create a great commit message!
Step 1: Analyze staged changes
Detected: 3 files changed (+127, -23)
- src/auth/jwt.ts (+85, -15)
- src/auth/types.ts (+10, -0)
- tests/auth/jwt.test.ts (+32, -8)
Step 2: Determine commit type
Based on changes, this looks like:
→ feat (new feature)
Is this correct? [y/n]: y
Step 3: Determine scope
Suggested scope: auth
Use this scope? [y/n]: y
Step 4: Write subject
Suggested: "add JWT token refresh mechanism"
Subject: [enter your own or press Enter to use suggestion]
Step 5: Write body (optional)
Explain what and why...
[Press Enter twice when done]
Step 6: Add references
Related issues found:
- #142: "Implement JWT authentication"
Add "Closes #142"? [y/n]: y
Step 7: Preview
────────────────────────────────────────
feat(auth): add JWT token refresh mechanism
Implements automatic token refresh 5 minutes before expiration
to maintain seamless user sessions.
Closes #142
────────────────────────────────────────
Commit with this message? [y/n/edit]: y
✅ Committed: abc1234Workflow Patterns
Pattern 1: Create Feature Commit
Trigger: User has staged changes for a new feature
Workflow: 1. Analyze staged changes (files, LOC, patterns) 2. Identify commit type (feat) 3. Determine scope from file paths 4. Search for related issues 5. Generate descriptive subject 6. Create detailed body if needed 7. Add issue references 8. Format as conventional commit 9. Execute commit with validation
Pattern 2: Validate Commit History
Trigger: "Review my commits" or "Check commit quality"
Workflow: 1. Get commits since branch point or last N commits 2. Parse each commit message 3. Check conventional commit format 4. Validate message quality (clarity, atomicity) 5. Check commit size (LOC) 6. Identify issues (WIP messages, too large, unclear) 7. Generate recommendations 8. Offer to fix (rebase, squash, reword)
Pattern 3: Fix Commit Messages
Trigger: "Fix my commit messages"
Workflow: 1. Review commits needing improvement 2. Generate proper conventional format 3. Create rebase plan 4. Execute interactive rebase 5. For each commit:
- Present current message
- Generate improved message
- Let user review/edit
- Apply reword
6. Validate result
Helper Scripts
Commit Analyzer
{baseDir}/scripts/commit-analyzer.py:
# Check format compliance
python {baseDir}/scripts/commit-analyzer.py check-format
# Find commits to squash/fixup
python {baseDir}/scripts/commit-analyzer.py find-fixups
# Analyze commit sizes
python {baseDir}/scripts/commit-analyzer.py analyze-size
# Full quality report (includes suggestions)
python {baseDir}/scripts/commit-analyzer.py report --branch feature/authConventional Commits Helper
{baseDir}/scripts/conventional-commits.py:
# Validate commit message
python {baseDir}/scripts/conventional-commits.py validate "feat(auth): add login"
# Generate from changes
python {baseDir}/scripts/conventional-commits.py generate
# Interactive commit
python {baseDir}/scripts/conventional-commits.py interactive
# Batch validate
python {baseDir}/scripts/conventional-commits.py validate-branch feature/authIssue Tracker
{baseDir}/scripts/issue-tracker.py:
# Sync issues from GitHub to local cache
python {baseDir}/scripts/issue-tracker.py sync assigned
python {baseDir}/scripts/issue-tracker.py sync labeled priority:high
python {baseDir}/scripts/issue-tracker.py sync milestone "Sprint 5"
# Show cached issues as task list
python {baseDir}/scripts/issue-tracker.py show
# Find related issues for current staged changes
python {baseDir}/scripts/issue-tracker.py suggest-refs
# Get specific issue from cache
python {baseDir}/scripts/issue-tracker.py get 42
# Clear the cache
python {baseDir}/scripts/issue-tracker.py clear
# Output cache as JSON
python {baseDir}/scripts/issue-tracker.py jsonAssets
Commit Templates
{baseDir}/assets/commit-templates.json: Template patterns for common commit types with examples.
References
Conventional Commits Spec
{baseDir}/references/conventional-commits.md:
- Full specification
- Type definitions
- Examples
- Breaking changes
- Scope guidelines
Commit Patterns
{baseDir}/references/commit-patterns.md:
- Common patterns
- Anti-patterns
- Atomic commit examples
- Squash vs merge strategies
Integration Points
With /commit-smart Command
Primary integration: This skill powers the /commit-smart command
1. Analyzes staged changes and conversation context
2. Generates conventional commit message
3. Adds GitHub issue references from cache
4. Validates format compliance
5. Executes the commitWith triaging-issues Skill
Find related issues for commits:
1. Analyze staged changes
2. Extract keywords and file paths
3. Query issues with similar content
4. Rank by relevance
5. Suggest issue referencesWith reviewing-pull-requests Skill
Validate commits in PRs:
1. PR reviewer checks commit quality
2. managing-commits analyzes each commit
3. Report format violations
4. Suggest improvements before mergeMulti-File Intelligent Grouping
When to Use Intelligent Grouping
Invoke intelligent file grouping when:
- User asks to "commit changes" with multiple modified files
- User invokes
/commit-smartcommand - Multiple scopes are detected in working directory
- Conversation context suggests multiple logical commits
File Grouping Strategies
1. Scope-Based Grouping
Group files by functional area:
auth scope: src/auth/*.ts → One commit
api scope: src/api/*.ts → Separate commit
ui scope: src/components/*.tsx → Separate commit2. Type-Based Separation
Separate by commit type:
Implementation: src/**/*.ts (not tests) → feat/fix/refactor
Tests: **/*.test.ts → test
Documentation: **/*.md → docs
Configuration: *.json, *.config.* → chore/build3. Relationship-Based Grouping
Keep related files together:
Feature implementation:
- src/auth/jwt.ts
- src/auth/types.ts
- src/auth/index.ts
→ Single commit: feat(auth): add JWT management
Separate tests:
- tests/auth/jwt.test.ts
→ Separate commit: test(auth): add JWT testsIntelligent Grouping Workflow
When multiple files need committing:
Step 1: Analyze all changes
git status --porcelain
git diff HEAD --statStep 2: Detect scopes and types
# Use helper script
python {baseDir}/scripts/group-files.py --analyzeOutput:
Group 1: feat(auth) - 3 impl files, 245 LOC
Group 2: test(auth) - 2 test files, 128 LOC
Group 3: fix(api) - 2 files, 15 LOC
Group 4: docs - 2 files, 67 LOCStep 3: Generate commit messages for each group
For each group:
- Determine type (feat, fix, test, docs)
- Extract scope from file paths
- Create descriptive subject
- Search for related issues
- Build complete conventional commit message
Step 4: Present plan to user
Found 12 changed files in 4 logical groups:
1. feat(auth): add JWT token refresh (3 files, +245 LOC)
- src/auth/jwt.ts
- src/auth/types.ts
- src/auth/index.ts
Related: #142
2. test(auth): add JWT refresh tests (2 files, +128 LOC)
- tests/auth/jwt.test.ts
- tests/auth/integration.test.ts
3. fix(api): resolve validation error (2 files, +15 LOC)
- src/api/validation.ts
- tests/api/validation.test.ts
Closes: #156
4. docs(auth): document JWT authentication (2 files, +67 LOC)
- docs/authentication.md
- README.md
Create these 4 commits? [y/n/edit]Step 5: Execute commits in order
# Commit 1
git add src/auth/jwt.ts src/auth/types.ts src/auth/index.ts
git commit -m "feat(auth): add JWT token refresh mechanism..."
# Commit 2
git add tests/auth/jwt.test.ts tests/auth/integration.test.ts
git commit -m "test(auth): add JWT refresh tests..."
# Continue for all groupsCommit Modes
Mode: all - Analyze all unstaged and staged changes
Strategy: Group all working directory changes
Use when: User wants to commit everythingMode: staged - Only commit what's staged
Strategy: Group staged files, suggest splits if multiple scopes
Use when: User has pre-selected files to commitMode: context - Analyze conversation history
Strategy: Match files to conversation topics
Use when: User wants to commit work from current conversation
Example: "User asked to implement JWT and fix validation bug"
→ Group 1: JWT files
→ Group 2: Validation filesMode: scope - Commit by specific scope
Strategy: Let user select scope (auth, api, etc.)
Use when: User wants to commit one functional areaMode: interactive - Interactive with confirmations
Strategy: Present plan, allow editing/confirming each commit
Use when: Default mode, gives user full controlHelper Scripts
File Grouper
{baseDir}/scripts/group-files.py:
# Analyze and group files
python {baseDir}/scripts/group-files.py
# Specific mode
python {baseDir}/scripts/group-files.py --mode scope
# JSON output for automation
python {baseDir}/scripts/group-files.py --jsonCommon Use Cases
Use Case 1: Create Feature Commit
User: "Commit my auth changes"
You:
Analyzing staged changes...
Files: src/auth/jwt.ts (+85), tests/auth/jwt.test.ts (+32)
Type: feat (new feature detected)
Scope: auth
Related issue: #142 "Implement JWT authentication"
Commit message:
feat(auth): add JWT token refresh mechanism
Implements automatic refresh 5 minutes before expiration.
Includes comprehensive test coverage.
Closes #142
Create this commit? [y/n]: y
✅ Committed: abc1234Use Case 2: Intelligent Multi-File Commit
User: "/commit-smart all"
You:
Analyzing working directory...
Found 12 changed files across 3 scopes:
📦 Group 1: feat(auth) - Authentication feature (5 files, +312 LOC)
M src/auth/jwt.ts (+185)
M src/auth/types.ts (+25)
M src/auth/index.ts (+35)
A tests/auth/jwt.test.ts (+95)
M docs/authentication.md (+42)
🐛 Group 2: fix(api) - API validation (2 files, +15 LOC)
M src/api/validation.ts (+12)
M tests/api/validation.test.ts (+3)
📚 Group 3: docs - Documentation (1 file, +8 LOC)
M README.md (+8)
Proposed commits:
1. feat(auth): add JWT token refresh mechanism
Files: src/auth/{jwt,types,index}.ts
Related: #142
2. test(auth): add JWT refresh tests
Files: tests/auth/jwt.test.ts
3. docs(auth): document JWT authentication
Files: docs/authentication.md
4. fix(api): resolve validation error for empty arrays
Files: src/api/validation.ts, tests/api/validation.test.ts
Closes: #156
5. docs: update README with authentication guide
Files: README.md
Create these 5 commits? [y/n/edit]: y
Creating commits...
✅ 1/5: feat(auth): add JWT token refresh [abc1234]
✅ 2/5: test(auth): add JWT refresh tests [def5678]
✅ 3/5: docs(auth): document JWT authentication [ghi9012]
✅ 4/5: fix(api): resolve validation error [jkl3456]
✅ 5/5: docs: update README [mno7890]
✅ All 5 commits created successfully!Use Case 3: Fix Commit History
User: "My commit messages are a mess, fix them"
You:
Reviewing commits on feature/auth...
Found 3 issues:
1. abc123: "wip" → fix(auth): resolve token validation error
2. def456: "stuff" → feat(auth): add user session management
3. ghi789: "updates" → refactor(auth): extract JWT utilities
Starting interactive rebase...
[Walks through each commit for review/edit]
✅ All commits reworded with conventional formatImportant Notes
- Atomic commits: One logical change per commit
- Clear subjects: Describe what, not how
- Link issues: Always reference related issues
- Test commits: Separate test commits from feature commits
- Breaking changes: Always document in footer
- Co-authors: Credit collaborators
Error Handling
Common issues:
- Empty commit message → Generate from changes
- No staged changes → Prompt to stage
- Format violations → Suggest correction
- Missing issue reference → Search and suggest
- Commit too large → Recommend splitting
When you encounter commit operations, use this expertise to help users maintain high-quality git history!
{
"feat": {
"type": "feat",
"description": "A new feature",
"template": "feat({scope}): {subject}\n\n{body}\n\n{footer}",
"examples": [
{
"message": "feat(auth): add JWT token refresh mechanism\n\nImplements automatic token refresh 5 minutes before expiration\nto maintain seamless user sessions without re-authentication.\n\nCloses #142",
"explanation": "New authentication feature with clear description and issue reference"
},
{
"message": "feat(api): add pagination support for user list endpoint\n\nAdds limit and offset query parameters.\nIncludes total count in response headers.\n\nCloses #234",
"explanation": "API enhancement with implementation details"
}
]
},
"fix": {
"type": "fix",
"description": "A bug fix",
"template": "fix({scope}): {subject}\n\n{body}\n\n{footer}",
"examples": [
{
"message": "fix(auth): resolve JWT validation error for expired tokens\n\nThe JWT validator was throwing unhandled errors when tokens expired.\nAdded proper error handling and user-friendly error messages.\n\nFixes #156",
"explanation": "Bug fix explaining the problem and solution"
},
{
"message": "fix(api): prevent null pointer exception in user lookup\n\nAdded null check before accessing user properties.\nReturns 404 instead of 500 when user not found.\n\nFixes #189",
"explanation": "Bug fix with error handling improvement"
}
]
},
"docs": {
"type": "docs",
"description": "Documentation only changes",
"template": "docs({scope}): {subject}\n\n{body}\n\n{footer}",
"examples": [
{
"message": "docs(api): update authentication endpoints documentation\n\nAdded examples for token refresh flow.\nClarified error response formats.\nUpdated JWT structure documentation.",
"explanation": "Documentation update with specific changes listed"
},
{
"message": "docs(readme): add installation instructions for Windows\n\nIncludes PowerShell and CMD examples.\nAdds troubleshooting section for common Windows issues.",
"explanation": "README improvement for specific platform"
}
]
},
"refactor": {
"type": "refactor",
"description": "Code change that neither fixes a bug nor adds a feature",
"template": "refactor({scope}): {subject}\n\n{body}\n\n{footer}",
"examples": [
{
"message": "refactor(auth): extract JWT utilities into separate module\n\nMoves token generation, validation, and refresh logic to\nauth/jwt-utils.ts for better organization and reusability.\n\nNo functional changes.",
"explanation": "Refactoring that doesn't change behavior"
},
{
"message": "refactor(api): simplify error handling with middleware\n\nReplaces try-catch blocks in each route with centralized\nerror handling middleware.\n\nReduces code duplication by ~200 LOC.",
"explanation": "Refactoring that improves code quality"
}
]
},
"test": {
"type": "test",
"description": "Adding or correcting tests",
"template": "test({scope}): {subject}\n\n{body}\n\n{footer}",
"examples": [
{
"message": "test(auth): add JWT refresh token tests\n\nAdds tests for:\n- Token refresh before expiration\n- Expired token handling\n- Invalid refresh token scenarios\n\nIncreases auth coverage to 95%.",
"explanation": "Test addition with coverage improvement"
},
{
"message": "test(api): add integration tests for user endpoints\n\nTests full request/response cycle including:\n- User creation\n- User retrieval\n- User updates\n- Error cases",
"explanation": "Integration test suite addition"
}
]
},
"chore": {
"type": "chore",
"description": "Changes to build process or auxiliary tools",
"template": "chore({scope}): {subject}\n\n{body}\n\n{footer}",
"examples": [
{
"message": "chore(deps): update dependencies to latest versions\n\nUpdates:\n- express: 4.17.1 -> 4.18.2\n- jsonwebtoken: 8.5.1 -> 9.0.0\n\nAll tests passing after update.",
"explanation": "Dependency update with version details"
},
{
"message": "chore(ci): add automated security scanning\n\nAdds npm audit check to CI pipeline.\nFails build if high/critical vulnerabilities found.",
"explanation": "CI/CD improvement"
}
]
},
"perf": {
"type": "perf",
"description": "A code change that improves performance",
"template": "perf({scope}): {subject}\n\n{body}\n\n{footer}",
"examples": [
{
"message": "perf(api): add caching for user lookup queries\n\nImplements Redis caching with 5-minute TTL.\nReduces database queries by ~80%.\nImproves response time from 150ms to 20ms.",
"explanation": "Performance optimization with metrics"
},
{
"message": "perf(auth): optimize JWT verification\n\nCaches public key for signature validation.\nReduces token verification time by 60%.",
"explanation": "Performance improvement with measurement"
}
]
},
"style": {
"type": "style",
"description": "Changes that do not affect code meaning (formatting, etc)",
"template": "style({scope}): {subject}\n\n{body}\n\n{footer}",
"examples": [
{
"message": "style(api): format code with Prettier\n\nApplies consistent formatting across all API files.\nNo functional changes.",
"explanation": "Code formatting only"
}
]
},
"ci": {
"type": "ci",
"description": "Changes to CI configuration files and scripts",
"template": "ci({scope}): {subject}\n\n{body}\n\n{footer}",
"examples": [
{
"message": "ci(github): add automated release workflow\n\nCreates GitHub releases on version tags.\nGenerates changelog from conventional commits.\nPublishes to npm automatically.",
"explanation": "CI/CD workflow addition"
}
]
},
"build": {
"type": "build",
"description": "Changes that affect the build system or dependencies",
"template": "build({scope}): {subject}\n\n{body}\n\n{footer}",
"examples": [
{
"message": "build(webpack): optimize production build\n\nEnables code splitting and tree shaking.\nReduces bundle size by 40%.",
"explanation": "Build optimization"
}
]
},
"revert": {
"type": "revert",
"description": "Reverts a previous commit",
"template": "revert: {subject}\n\nThis reverts commit {hash}.\n\n{reason}",
"examples": [
{
"message": "revert: feat(auth): add JWT token refresh\n\nThis reverts commit abc1234.\n\nReverted due to security vulnerability in refresh logic.\nWill be reimplemented with proper validation.",
"explanation": "Revert with explanation"
}
]
}
}
Commit Patterns and Anti-Patterns
Good Patterns
Atomic Commits
Each commit represents one logical change:
✅ Good:
feat(auth): add JWT validation
test(auth): add JWT validation tests
docs(auth): document JWT setup
❌ Bad:
feat(auth): implement authentication
(includes validation, tests, docs, and refactoring all mixed together)Descriptive Messages
Clear about what and why:
✅ Good:
fix(api): resolve race condition in user creation
The user creation endpoint had a race condition when multiple
requests arrived simultaneously. Added transaction locks to
ensure atomic user creation.
❌ Bad:
fix bugIssue Linking
Always link to related issues:
✅ Good:
feat(profile): add avatar upload
Implements user avatar upload with automatic resizing and
S3 storage integration.
Closes #142
❌ Bad:
feat(profile): add avatar upload
(no issue reference)Anti-Patterns
WIP Commits
❌ Avoid:
wip
temp changes
checkpoint
more stuff
✅ Instead:
Squash these before merging, or make them descriptive:
refactor(auth): extract validation logic (WIP)Vague Messages
❌ Avoid:
fix stuff
update code
changes
improvements
✅ Instead:
fix(api): resolve timeout in user search
refactor(utils): simplify date formatting
feat(ui): add loading spinner
perf(database): optimize user queriesLarge Commits
❌ Avoid:
feat(app): implement entire user management system
(1500 lines changed across 30 files)
✅ Instead:
Split into atomic commits:
feat(user): add user model and database schema
feat(user): add user CRUD API endpoints
feat(user): add user management UI
test(user): add user management tests
docs(user): document user management APINon-Conventional Format
❌ Avoid:
Updated authentication
Fixed the bug
Added new feature
✅ Instead:
refactor(auth): update authentication flow
fix(api): resolve validation error
feat(profile): add avatar uploadCommit Organization Strategies
Feature Branch Pattern
main
← feat(auth): add user model
← feat(auth): add authentication endpoints
← test(auth): add authentication tests
← docs(auth): document authentication flow
← merge: Merge authentication featureSquash and Merge
Squash multiple WIP commits into one clean commit before merging:
Before merge:
- wip: started auth
- wip: more auth work
- fix: fixed tests
- wip: final changes
After squash:
- feat(auth): add JWT authentication systemRebase and Clean
Use interactive rebase to clean history:
git rebase -i main
Commands:
- pick: keep commit as-is
- reword: change commit message
- squash: combine with previous commit
- fixup: like squash but discard message
- drop: remove commitCommit Size Guidelines
Ideal Sizes
- Tiny (< 10 LOC): Single logical change
fix(typo): correct spelling in README- Small (10-50 LOC): Typical atomic commit
feat(api): add user validation endpoint- Medium (50-200 LOC): Feature component
feat(auth): add JWT token management- Large (200-500 LOC): Consider splitting
feat(user): implement user profile management
(Could split into: model, API, tests)- Too Large (> 500 LOC): Definitely split
feat(app): implement entire authentication system
(Split into: models, auth logic, endpoints, tests, docs)Special Cases
Breaking Changes
Always use ! and document in footer:
feat(api)!: change user endpoint structure
BREAKING CHANGE: User endpoint now returns {user: {...}}
instead of direct user object. Update all API clients.
Closes #200Reverts
Use revert type and reference original:
revert: revert "feat(auth): add OAuth support"
This reverts commit abc1234.
OAuth integration caused issues with existing authentication.
Will reimplement after refactoring auth system.Co-Authors
Credit multiple authors:
feat(api): add GraphQL endpoint
Implements initial GraphQL API with user queries.
Co-authored-by: Alice <alice@example.com>
Co-authored-by: Bob <bob@example.com>Review Checklist
Before committing, verify:
- [ ] Atomic: One logical change only
- [ ] Formatted: Follows conventional commits
- [ ] Descriptive: Clear what and why
- [ ] Sized: Not too large (< 500 LOC)
- [ ] Linked: References issues if applicable
- [ ] Tested: Includes or updates tests
- [ ] Docs: Updates documentation if needed
Tools
Commitizen
Interactive commit tool:
npm install -g commitizen
git czCommitlint
Validate commit messages:
npm install --save-dev @commitlint/cli @commitlint/config-conventional
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.jsConventional Changelog
Generate changelogs from commits:
npm install -g conventional-changelog-cli
conventional-changelog -p angular -i CHANGELOG.md -sConventional Commits Specification
Format
<type>(<scope>): <subject>
<body>
<footer>Types
- feat: A new feature
- fix: A bug fix
- docs: Documentation only changes
- style: Changes that do not affect the meaning of the code (white-space, formatting, etc)
- refactor: A code change that neither fixes a bug nor adds a feature
- perf: A code change that improves performance
- test: Adding missing tests or correcting existing tests
- chore: Changes to the build process or auxiliary tools
- ci: Changes to CI configuration files and scripts
- build: Changes that affect the build system or external dependencies
- revert: Reverts a previous commit
Scope
Optional. Indicates the area of the codebase affected.
Examples: api, ui, auth, database, docs
Subject
- Use imperative, present tense: "change" not "changed" nor "changes"
- Don't capitalize first letter
- No period (.) at the end
- Maximum 50 characters
Body
- Use imperative, present tense
- Explain what and why vs. how
- Wrap at 72 characters
Footer
- BREAKING CHANGE: describes breaking changes
- Closes #N: closes issue N
- Ref #N: references issue N
Examples
Simple Feature
feat(auth): add JWT token authentication
Implements JWT-based authentication for API endpoints.
Tokens expire after 24 hours.Bug Fix with Issue
fix(api): resolve user validation error
The user validation was failing for emails with plus signs.
Fixed regex pattern to allow all valid email formats.
Closes #156Breaking Change
feat(api)!: change authentication endpoint
BREAKING CHANGE: The /auth endpoint now requires OAuth2.
Update all clients to use the new OAuth2 flow at /oauth/token.Multiple Issues
fix(validation): resolve input validation errors
Fixes multiple validation issues across user forms.
Closes #123, closes #124, closes #125Best Practices
1. Be atomic: One logical change per commit 2. Be specific: Clearly describe what changed 3. Link issues: Always reference related issues 4. Explain why: Body should explain rationale 5. Breaking changes: Always document in footer
#!/usr/bin/env python3
"""
Git Commit Analyzer
Analyzes commit quality, format compliance, and suggests improvements
"""
import argparse
import re
import subprocess
import sys
from typing import List, Dict, Tuple
# Color constants
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m'
# Conventional commit types
VALID_TYPES = ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'ci', 'build', 'revert']
# Conventional commit pattern
CONVENTIONAL_PATTERN = re.compile(
r'^(' + '|'.join(VALID_TYPES) + r')(\([a-z0-9-]+\))?!?: .{1,50}$'
)
def run_git(args: List[str]) -> str:
"""Execute git command"""
try:
result = subprocess.run(['git'] + args, capture_output=True, text=True, check=True)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"{RED}Git error: {e.stderr}{NC}", file=sys.stderr)
return ""
def get_commits(branch: str = None, count: int = 20) -> List[Dict]:
"""Get recent commits"""
if branch:
# Commits on branch not in main
range_spec = f"main...{branch}"
else:
range_spec = f"-{count}"
log_output = run_git(['log', range_spec, '--format=%H|%s|%b|%an|%ae'])
commits = []
for line in log_output.split('\n\n'):
if not line.strip():
continue
parts = line.split('|', 4)
if len(parts) >= 2:
commits.append({
'hash': parts[0],
'subject': parts[1],
'body': parts[2] if len(parts) > 2 else '',
'author': parts[3] if len(parts) > 3 else '',
'email': parts[4] if len(parts) > 4 else ''
})
return commits
def get_commit_stats(commit_hash: str) -> Tuple[int, int]:
"""Get insertions and deletions for a commit"""
stats = run_git(['show', '--stat', '--format=', commit_hash])
insertions = 0
deletions = 0
# Parse stats line like: "5 files changed, 123 insertions(+), 45 deletions(-)"
match = re.search(r'(\d+) insertion', stats)
if match:
insertions = int(match.group(1))
match = re.search(r'(\d+) deletion', stats)
if match:
deletions = int(match.group(1))
return insertions, deletions
def check_conventional_format(subject: str) -> Tuple[bool, List[str]]:
"""Check if commit message follows conventional commits"""
issues = []
# Check pattern
if not CONVENTIONAL_PATTERN.match(subject):
# Try to identify specific issues
if not any(subject.startswith(t) for t in VALID_TYPES):
issues.append(f"Missing or invalid type. Valid types: {', '.join(VALID_TYPES)}")
if len(subject) > 72:
issues.append(f"Subject too long ({len(subject)} chars). Keep under 72 chars.")
if subject.endswith('.'):
issues.append("Subject should not end with a period")
if subject != subject.lower() and not any(subject.startswith(t.upper()) for t in VALID_TYPES):
issues.append("Subject should be lowercase (except type)")
return len(issues) == 0, issues
def analyze_commit_quality(commit: Dict) -> Dict:
"""Analyze individual commit quality"""
subject = commit['subject']
body = commit['body']
quality = {
'hash': commit['hash'][:7],
'subject': subject,
'score': 0,
'issues': [],
'suggestions': []
}
# Check format (weight: 30 points)
is_conventional, format_issues = check_conventional_format(subject)
if is_conventional:
quality['score'] += 30
else:
quality['issues'].extend(format_issues)
# Check subject quality (weight: 25 points)
if len(subject) > 10:
quality['score'] += 10
else:
quality['issues'].append("Subject too short")
if not any(vague in subject.lower() for vague in ['wip', 'temp', 'stuff', 'things', 'update', 'fix']):
quality['score'] += 15
else:
quality['issues'].append("Subject too vague")
quality['suggestions'].append("Be more specific about what changed")
# Check body (weight: 20 points)
if body and len(body) > 20:
quality['score'] += 20
elif len(subject) > 50:
quality['suggestions'].append("Consider adding a body to explain changes")
# Check issue references (weight: 15 points)
if re.search(r'(closes?|fixes?|resolves?) #\d+', body, re.IGNORECASE):
quality['score'] += 15
elif re.search(r'#\d+', subject + body):
quality['score'] += 10
quality['suggestions'].append("Use 'Closes #N' syntax for automatic issue closing")
else:
quality['suggestions'].append("Add issue reference if applicable")
# Check commit size (weight: 10 points)
insertions, deletions = get_commit_stats(commit['hash'])
total_changes = insertions + deletions
if total_changes < 500:
quality['score'] += 10
elif total_changes < 1000:
quality['score'] += 5
quality['suggestions'].append("Consider splitting into smaller commits")
else:
quality['issues'].append(f"Very large commit ({total_changes} LOC)")
quality['suggestions'].append("Definitely split into multiple atomic commits")
quality['size'] = total_changes
# Overall rating
if quality['score'] >= 80:
quality['rating'] = f"{GREEN}Excellent{NC}"
elif quality['score'] >= 60:
quality['rating'] = f"{BLUE}Good{NC}"
elif quality['score'] >= 40:
quality['rating'] = f"{YELLOW}Needs Improvement{NC}"
else:
quality['rating'] = f"{RED}Poor{NC}"
return quality
def check_format(branch: str = None):
"""Check format compliance for commits"""
commits = get_commits(branch=branch)
print(f"\n{'='*60}")
print(f"Checking Conventional Commit Format")
print(f"{'='*60}\n")
compliant = 0
non_compliant = 0
for commit in commits:
is_conventional, issues = check_conventional_format(commit['subject'])
if is_conventional:
print(f"{GREEN}✓{NC} {commit['hash'][:7]}: {commit['subject']}")
compliant += 1
else:
print(f"{RED}✗{NC} {commit['hash'][:7]}: {commit['subject']}")
for issue in issues:
print(f" - {issue}")
non_compliant += 1
print(f"\nCompliance: {compliant}/{len(commits)} ({compliant/len(commits)*100:.0f}%)")
if non_compliant > 0:
print(f"\n{YELLOW}Suggestion:{NC} Run interactive rebase to fix non-compliant messages")
def find_fixups(branch: str = None):
"""Find commits that could be squashed"""
commits = get_commits(branch=branch)
print(f"\n{'='*60}")
print(f"Finding Fixup Opportunities")
print(f"{'='*60}\n")
fixup_candidates = []
for i, commit in enumerate(commits):
subject = commit['subject'].lower()
# Check for WIP/fixup/temp commits
if any(marker in subject for marker in ['wip', 'fixup', 'temp', 'tmp', 'checkpoint']):
fixup_candidates.append({
'index': i,
'commit': commit,
'reason': 'Temporary commit'
})
# Check for very similar subjects (potential duplicates)
for j, other_commit in enumerate(commits[i+1:], start=i+1):
if len(subject) > 10 and subject in other_commit['subject'].lower():
fixup_candidates.append({
'index': i,
'commit': commit,
'related_index': j,
'related_commit': other_commit,
'reason': 'Similar to another commit'
})
break
if not fixup_candidates:
print(f"{GREEN}✓{NC} No fixup opportunities found. Commit history looks clean!")
return
print(f"Found {len(fixup_candidates)} fixup opportunities:\n")
for candidate in fixup_candidates:
commit = candidate['commit']
print(f"{YELLOW}→{NC} {commit['hash'][:7]}: {commit['subject']}")
print(f" Reason: {candidate['reason']}")
if 'related_commit' in candidate:
related = candidate['related_commit']
print(f" Related: {related['hash'][:7]}: {related['subject']}")
print()
print(f"\n{BLUE}Suggestion:{NC} Use interactive rebase to squash/fixup these commits")
def analyze_size(branch: str = None):
"""Analyze commit sizes"""
commits = get_commits(branch=branch)
print(f"\n{'='*60}")
print(f"Commit Size Analysis")
print(f"{'='*60}\n")
sizes = []
for commit in commits:
insertions, deletions = get_commit_stats(commit['hash'])
total = insertions + deletions
sizes.append(total)
# Categorize
if total < 10:
category = f"{BLUE}Tiny{NC}"
elif total < 50:
category = f"{GREEN}Small{NC}"
elif total < 200:
category = f"{YELLOW}Medium{NC}"
elif total < 500:
category = f"{YELLOW}Large{NC}"
else:
category = f"{RED}Very Large{NC}"
print(f"{commit['hash'][:7]}: {total:4d} LOC - {category}")
if total > 500:
print(f" ⚠️ Consider splitting this commit")
print(f" {commit['subject']}")
print()
# Statistics
avg_size = sum(sizes) / len(sizes) if sizes else 0
print(f"\nAverage commit size: {avg_size:.0f} LOC")
print(f"Largest commit: {max(sizes)} LOC")
print(f"Smallest commit: {min(sizes)} LOC")
def generate_report(branch: str = None):
"""Generate comprehensive quality report"""
commits = get_commits(branch=branch)
print(f"\n{'='*60}")
print(f"Commit Quality Report")
if branch:
print(f"Branch: {branch}")
print(f"{'='*60}\n")
print(f"Analyzed {len(commits)} commits\n")
excellent = 0
good = 0
needs_improvement = 0
poor = 0
for commit in commits:
quality = analyze_commit_quality(commit)
print(f"{quality['hash']}: {quality['subject'][:60]}")
print(f" Score: {quality['score']}/100 - {quality['rating']}")
if quality['issues']:
print(f" {RED}Issues:{NC}")
for issue in quality['issues']:
print(f" - {issue}")
if quality['suggestions']:
print(f" {BLUE}Suggestions:{NC}")
for suggestion in quality['suggestions']:
print(f" - {suggestion}")
print()
# Count ratings
if quality['score'] >= 80:
excellent += 1
elif quality['score'] >= 60:
good += 1
elif quality['score'] >= 40:
needs_improvement += 1
else:
poor += 1
# Summary
print(f"\n{'='*60}")
print("Summary")
print(f"{'='*60}\n")
print(f"{GREEN}Excellent:{NC} {excellent}")
print(f"{BLUE}Good:{NC} {good}")
print(f"{YELLOW}Needs Improvement:{NC} {needs_improvement}")
print(f"{RED}Poor:{NC} {poor}")
overall_quality = (excellent + good) / len(commits) * 100 if commits else 0
print(f"\nOverall Quality: {overall_quality:.0f}%")
def main():
parser = argparse.ArgumentParser(description='Git Commit Analyzer')
subparsers = parser.add_subparsers(dest='command', help='Command to execute')
# check-format
format_parser = subparsers.add_parser('check-format', help='Check conventional commit format')
format_parser.add_argument('--branch', help='Branch to analyze')
# find-fixups
fixup_parser = subparsers.add_parser('find-fixups', help='Find commits to squash/fixup')
fixup_parser.add_argument('--branch', help='Branch to analyze')
# analyze-size
size_parser = subparsers.add_parser('analyze-size', help='Analyze commit sizes')
size_parser.add_argument('--branch', help='Branch to analyze')
# report
report_parser = subparsers.add_parser('report', help='Generate full quality report')
report_parser.add_argument('--branch', help='Branch to analyze')
args = parser.parse_args()
if not args.command:
parser.print_help()
return
# Execute command
if args.command == 'check-format':
check_format(args.branch)
elif args.command == 'find-fixups':
find_fixups(args.branch)
elif args.command == 'analyze-size':
analyze_size(args.branch)
elif args.command == 'report':
generate_report(args.branch)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
Conventional Commits Helper
Generates and validates conventional commit messages
"""
import argparse
import re
import subprocess
import sys
VALID_TYPES = ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'ci', 'build', 'revert']
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m'
def run_git(args):
"""Execute git command"""
try:
result = subprocess.run(['git'] + args, capture_output=True, text=True, check=True)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"{RED}Error: {e.stderr}{NC}", file=sys.stderr)
return ""
def validate_message(message):
"""Validate a commit message"""
lines = message.strip().split('\n')
subject = lines[0]
issues = []
# Check type
has_valid_type = any(subject.startswith(t + ':') or subject.startswith(t + '(') for t in VALID_TYPES)
if not has_valid_type:
issues.append(f"Missing valid type. Use one of: {', '.join(VALID_TYPES)}")
# Check subject length
if len(subject) > 72:
issues.append(f"Subject too long: {len(subject)} chars (max 72)")
# Check subject ends with period
if subject.endswith('.'):
issues.append("Subject should not end with period")
# Check imperative mood (basic heuristic)
if any(subject.lower().startswith(t + 'ed ') for t in VALID_TYPES):
issues.append("Use imperative mood ('add' not 'added')")
return len(issues) == 0, issues
def generate_from_diff():
"""Generate commit message from staged changes"""
# Get staged files
staged = run_git(['diff', '--cached', '--name-only'])
if not staged:
print(f"{YELLOW}No staged changes{NC}")
return
files = staged.split('\n')
print(f"\n{BLUE}Staged files:{NC}")
for f in files[:10]:
print(f" - {f}")
if len(files) > 10:
print(f" ... and {len(files) - 10} more")
# Get diff stats
stats = run_git(['diff', '--cached', '--stat'])
# Try to infer type and scope
scope = None
commit_type = None
# Infer from paths
if any('test' in f for f in files):
commit_type = 'test'
elif any('.md' in f or 'doc' in f.lower() for f in files):
commit_type = 'docs'
elif any('ci' in f or '.github' in f for f in files):
commit_type = 'ci'
# Infer scope from common path prefixes
common_paths = set()
for f in files:
parts = f.split('/')
if len(parts) > 1:
common_paths.add(parts[0])
if len(common_paths) == 1:
scope = list(common_paths)[0]
print(f"\n{BLUE}Suggested:{ NC}")
if commit_type:
print(f" Type: {commit_type}")
if scope:
print(f" Scope: {scope}")
print("\n" + "="*60)
print(stats)
print("="*60)
def interactive():
"""Interactive commit message builder"""
print(f"\n{BLUE}=== Interactive Commit Builder ==={NC}\n")
# Step 1: Show changes
generate_from_diff()
# Step 2: Get type
print(f"\n{BLUE}Step 1: Select type{NC}")
for i, t in enumerate(VALID_TYPES, 1):
print(f" {i}. {t}")
type_choice = input("Enter number (or type name): ").strip()
if type_choice.isdigit():
commit_type = VALID_TYPES[int(type_choice) - 1]
else:
commit_type = type_choice
# Step 3: Get scope (optional)
print(f"\n{BLUE}Step 2: Enter scope (optional){NC}")
scope = input("Scope: ").strip()
# Step 4: Get subject
print(f"\n{BLUE}Step 3: Enter subject{NC}")
subject = input("Subject: ").strip()
# Step 5: Get body (optional)
print(f"\n{BLUE}Step 4: Enter body (optional, press Enter twice to finish){NC}")
body_lines = []
while True:
line = input()
if not line:
break
body_lines.append(line)
body = '\n'.join(body_lines)
# Step 6: Get footer (optional)
print(f"\n{BLUE}Step 5: Add references (optional){NC}")
footer = input("Footer (e.g., 'Closes #42'): ").strip()
# Build message
if scope:
message_subject = f"{commit_type}({scope}): {subject}"
else:
message_subject = f"{commit_type}: {subject}"
full_message = message_subject
if body:
full_message += "\n\n" + body
if footer:
full_message += "\n\n" + footer
# Preview
print(f"\n{'='*60}")
print(full_message)
print(f"{'='*60}\n")
# Validate
is_valid, issues = validate_message(full_message)
if not is_valid:
print(f"{YELLOW}Warnings:{NC}")
for issue in issues:
print(f" - {issue}")
print()
# Confirm
confirm = input("Commit with this message? [y/n/e(dit)]: ").strip().lower()
if confirm == 'y':
# Write to temp file and commit
with open('/tmp/commit-msg.txt', 'w') as f:
f.write(full_message)
result = run_git(['commit', '-F', '/tmp/commit-msg.txt'])
if result:
print(f"\n{GREEN}✓ Committed{NC}")
elif confirm == 'e':
print("Opening editor...")
subprocess.run(['git', 'commit', '-e', '-m', full_message])
else:
print("Cancelled")
def validate_branch(branch):
"""Validate all commits on a branch"""
# Get commits
commits = run_git(['log', f'main...{branch}', '--format=%H|%s']).split('\n')
print(f"\n{'='*60}")
print(f"Validating commits on {branch}")
print(f"{'='*60}\n")
valid = 0
invalid = 0
for commit_line in commits:
if not commit_line:
continue
commit_hash, subject = commit_line.split('|', 1)
is_valid, issues = validate_message(subject)
if is_valid:
print(f"{GREEN}✓{NC} {commit_hash[:7]}: {subject}")
valid += 1
else:
print(f"{RED}✗{NC} {commit_hash[:7]}: {subject}")
for issue in issues:
print(f" - {issue}")
invalid += 1
total = valid + invalid
print(f"\nValid: {valid}/{total} ({valid/total*100:.0f}%)")
def main():
parser = argparse.ArgumentParser(description='Conventional Commits Helper')
subparsers = parser.add_subparsers(dest='command')
# validate
validate_parser = subparsers.add_parser('validate', help='Validate commit message')
validate_parser.add_argument('message', nargs='?', help='Commit message to validate')
# generate
subparsers.add_parser('generate', help='Generate message from changes')
# interactive
subparsers.add_parser('interactive', help='Interactive commit builder')
# validate-branch
branch_parser = subparsers.add_parser('validate-branch', help='Validate all commits on branch')
branch_parser.add_argument('branch', help='Branch to validate')
args = parser.parse_args()
if not args.command:
parser.print_help()
return
if args.command == 'validate':
if args.message:
is_valid, issues = validate_message(args.message)
if is_valid:
print(f"{GREEN}✓ Valid conventional commit{NC}")
else:
print(f"{RED}✗ Invalid commit message{NC}")
for issue in issues:
print(f" - {issue}")
sys.exit(1)
else:
print("Error: message required")
sys.exit(1)
elif args.command == 'generate':
generate_from_diff()
elif args.command == 'interactive':
interactive()
elif args.command == 'validate-branch':
validate_branch(args.branch)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
Intelligent File Grouping for Commits
Groups modified files by scope, type, and logical relationships
"""
import argparse
import json
import os
import re
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
from typing import List, Dict, Tuple, Set
# Color constants
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
MAGENTA = '\033[0;35m'
CYAN = '\033[0;36m'
NC = '\033[0m'
# Conventional commit types
COMMIT_TYPES = {
'feat': 'New feature',
'fix': 'Bug fix',
'docs': 'Documentation',
'test': 'Tests',
'refactor': 'Code refactoring',
'perf': 'Performance improvement',
'chore': 'Maintenance',
'ci': 'CI/CD changes',
'build': 'Build system changes',
}
# File patterns for type detection
TYPE_PATTERNS = {
'test': [r'\.test\.(ts|js|tsx|jsx|py)$', r'\.spec\.(ts|js|tsx|jsx)$', r'^tests?/'],
'docs': [r'\.md$', r'^docs?/'],
'config': [r'package\.json$', r'tsconfig\.json$', r'\.config\.(ts|js)$', r'\.(yml|yaml)$'],
}
def run_git(args: List[str]) -> str:
"""Execute git command"""
try:
result = subprocess.run(['git'] + args, capture_output=True, text=True, check=True)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"{RED}Git error: {e.stderr}{NC}", file=sys.stderr)
return ""
def get_changed_files(mode: str = 'all') -> List[Dict]:
"""Get changed files with stats"""
files = []
if mode == 'staged':
# Only staged files
status_output = run_git(['diff', '--cached', '--numstat'])
else:
# All changes (staged and unstaged)
status_output = run_git(['diff', 'HEAD', '--numstat'])
for line in status_output.split('\n'):
if not line.strip():
continue
parts = line.split('\t')
if len(parts) < 3:
continue
added = parts[0] if parts[0] != '-' else '0'
removed = parts[1] if parts[1] != '-' else '0'
filepath = parts[2]
files.append({
'path': filepath,
'added': int(added),
'removed': int(removed),
'total_changes': int(added) + int(removed),
})
# Also check for untracked files if mode is 'all'
if mode == 'all':
untracked = run_git(['ls-files', '--others', '--exclude-standard'])
for filepath in untracked.split('\n'):
if filepath.strip():
files.append({
'path': filepath.strip(),
'added': 0, # Unknown for untracked
'removed': 0,
'total_changes': 0,
})
return files
def detect_scope(filepath: str) -> str:
"""Detect scope from file path"""
path = Path(filepath)
parts = path.parts
# Common scope patterns
if len(parts) >= 2:
if parts[0] in ['src', 'lib', 'app']:
# src/auth/jwt.ts → auth
# src/components/Button.tsx → components or ui
scope = parts[1]
if scope in ['components', 'pages', 'views', 'layouts']:
return 'ui'
return scope
elif parts[0] == 'tests':
# tests/auth/jwt.test.ts → auth
if len(parts) >= 2:
return parts[1]
# Fallback: use first directory
if len(parts) > 1:
return parts[0]
return 'root'
def detect_type(filepath: str, added: int, removed: int) -> str:
"""Detect commit type from file characteristics"""
# Check type patterns
for type_name, patterns in TYPE_PATTERNS.items():
for pattern in patterns:
if re.search(pattern, filepath, re.IGNORECASE):
return type_name
# Check if it's a new file (likely feat)
if removed == 0 and added > 0:
return 'feat'
# Check if it's mostly deletions (could be refactor or fix)
if removed > added and removed > 10:
return 'refactor'
# Default: assume feature if substantial additions
if added > 20:
return 'feat'
# Default to fix for small changes
return 'fix'
def group_files_by_scope(files: List[Dict]) -> Dict[str, List[Dict]]:
"""Group files by scope"""
groups = defaultdict(list)
for file_info in files:
scope = detect_scope(file_info['path'])
groups[scope].append(file_info)
return dict(groups)
def group_files_by_type_and_scope(files: List[Dict]) -> List[Dict]:
"""Intelligently group files by type and scope"""
# First, categorize each file
categorized = []
for file_info in files:
scope = detect_scope(file_info['path'])
file_type = detect_type(file_info['path'], file_info['added'], file_info['removed'])
categorized.append({
**file_info,
'scope': scope,
'type': file_type,
})
# Group by (type, scope) combination
groups = defaultdict(list)
for file_info in categorized:
key = (file_info['type'], file_info['scope'])
groups[key].append(file_info)
# Convert to list of group dicts
result = []
for (file_type, scope), file_list in groups.items():
total_added = sum(f['added'] for f in file_list)
total_removed = sum(f['removed'] for f in file_list)
result.append({
'type': file_type,
'scope': scope,
'files': file_list,
'file_count': len(file_list),
'lines_added': total_added,
'lines_removed': total_removed,
'total_changes': total_added + total_removed,
})
# Sort by type priority, then scope
type_priority = {'feat': 1, 'fix': 2, 'refactor': 3, 'test': 4, 'docs': 5, 'config': 6, 'chore': 7}
result.sort(key=lambda g: (type_priority.get(g['type'], 99), g['scope']))
return result
def generate_commit_subject(group: Dict) -> str:
"""Generate commit subject line for a group"""
commit_type = group['type']
scope = group['scope']
file_count = group['file_count']
# Generate subject based on files
files = group['files']
filepaths = [f['path'] for f in files]
# Try to infer what changed
if commit_type == 'feat':
if file_count == 1:
filename = Path(filepaths[0]).stem
subject = f"add {filename} functionality"
else:
subject = f"add {scope} features"
elif commit_type == 'fix':
subject = f"resolve {scope} errors"
elif commit_type == 'test':
subject = f"add {scope} tests"
elif commit_type == 'docs':
if 'README' in filepaths[0]:
subject = "update README"
else:
subject = f"document {scope}"
elif commit_type == 'refactor':
subject = f"refactor {scope} implementation"
else:
subject = f"update {scope}"
return subject
def format_group_display(group: Dict, index: int) -> str:
"""Format group for display"""
icons = {
'feat': '📦',
'fix': '🐛',
'test': '✅',
'docs': '📚',
'refactor': '♻️',
'perf': '⚡',
'chore': '🔧',
}
icon = icons.get(group['type'], '📝')
commit_type = group['type']
scope = group['scope']
file_count = group['file_count']
changes = group['total_changes']
header = f"{icon} Group {index}: {commit_type}({scope}) - {file_count} files, {changes} LOC"
files_display = []
for f in group['files']:
added = f['added']
removed = f['removed']
path = f['path']
sign = '+' if added >= removed else '~'
files_display.append(f" {sign} {path} (+{added}, -{removed})")
subject = generate_commit_subject(group)
suggested_msg = f"\n Suggested: {commit_type}({scope}): {subject}"
return f"{CYAN}{header}{NC}\n" + "\n".join(files_display) + f"{YELLOW}{suggested_msg}{NC}"
def main():
parser = argparse.ArgumentParser(description='Intelligently group files for commits')
parser.add_argument('--mode', choices=['all', 'staged', 'scope'], default='all',
help='Grouping mode (default: all)')
parser.add_argument('--json', action='store_true', help='Output as JSON')
parser.add_argument('--analyze', action='store_true', help='Show analysis only')
args = parser.parse_args()
# Get changed files
files = get_changed_files(args.mode)
if not files:
print(f"{YELLOW}No changes detected.{NC}")
return 0
# Group files
if args.mode == 'scope':
scope_groups = group_files_by_scope(files)
print(f"\n{BLUE}Files grouped by scope:{NC}\n")
for scope, scope_files in scope_groups.items():
print(f"{GREEN}{scope}:{NC} {len(scope_files)} files")
for f in scope_files:
print(f" - {f['path']}")
else:
groups = group_files_by_type_and_scope(files)
if args.json:
# Output as JSON
output = {
'total_files': len(files),
'total_groups': len(groups),
'groups': groups,
}
print(json.dumps(output, indent=2))
else:
# Human-readable output
print(f"\n{BLUE}Found {len(files)} changed files in {len(groups)} logical groups:{NC}\n")
for i, group in enumerate(groups, 1):
print(format_group_display(group, i))
print()
return 0
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python3
"""
Initialize GitHub workflow environment.
Usage:
python init-environment.py # Initialize environment
python init-environment.py --force # Force re-initialization
python init-environment.py --check # Check if initialized
python init-environment.py --show # Show current environment
"""
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
# Environment file location (in .claude for project-specific storage)
ENV_DIR = ".claude/github-workflows"
ENV_FILE = "env.json"
def get_env_path():
"""Get the environment file path, creating directory if needed."""
env_dir = Path(ENV_DIR)
env_dir.mkdir(exist_ok=True)
return env_dir / ENV_FILE
def run_gh_command(args, default=None):
"""Run a GitHub CLI command and return output."""
try:
result = subprocess.run(
["gh"] + args,
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
if default is not None:
return default
return None
except FileNotFoundError:
print("Error: GitHub CLI (gh) not found. Install from https://cli.github.com/", file=sys.stderr)
return None
def run_git_command(args, default=None):
"""Run a git command and return output."""
try:
result = subprocess.run(
["git"] + args,
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError:
return default
except FileNotFoundError:
return default
def get_repository_info():
"""Get repository owner and name."""
output = run_gh_command(["repo", "view", "--json", "owner,name,url"])
if output:
try:
data = json.loads(output)
return {
"owner": data.get("owner", {}).get("login", "unknown"),
"name": data.get("name", "unknown"),
"fullName": f"{data.get('owner', {}).get('login', 'unknown')}/{data.get('name', 'unknown')}",
"url": data.get("url", "")
}
except json.JSONDecodeError:
pass
return None
def get_user_info():
"""Get current GitHub user info."""
output = run_gh_command(["api", "user", "--jq", ".login,.name"])
if output:
lines = output.split("\n")
return {
"login": lines[0] if len(lines) > 0 else "unknown",
"name": lines[1] if len(lines) > 1 else ""
}
return None
def get_project_board(owner):
"""Get the most recent active project board."""
# Try user projects first
output = run_gh_command([
"project", "list",
"--owner", "@me",
"--format", "json",
"--limit", "5"
])
if output:
try:
data = json.loads(output)
projects = data.get("projects", [])
if projects:
# Return most recent (first in list)
project = projects[0]
return {
"number": project.get("number"),
"title": project.get("title", ""),
"url": project.get("url", "")
}
except json.JSONDecodeError:
pass
# Try org projects if user has none
if owner and owner != "unknown":
output = run_gh_command([
"project", "list",
"--owner", owner,
"--format", "json",
"--limit", "5"
])
if output:
try:
data = json.loads(output)
projects = data.get("projects", [])
if projects:
project = projects[0]
return {
"number": project.get("number"),
"title": project.get("title", ""),
"url": project.get("url", "")
}
except json.JSONDecodeError:
pass
return None
def get_milestone(owner, repo):
"""Get the current active milestone."""
if not owner or not repo:
return None
output = run_gh_command([
"api",
f"repos/{owner}/{repo}/milestones",
"--jq", ".[0] | {title: .title, number: .number, dueOn: .due_on}"
])
if output:
try:
data = json.loads(output)
if data.get("title"):
return {
"title": data.get("title", ""),
"number": data.get("number"),
"dueOn": data.get("dueOn", "")
}
except json.JSONDecodeError:
pass
return None
def detect_branch_scope(branch_name, suggested_scopes):
"""Detect scope from branch name matching suggested scopes."""
if not branch_name or not suggested_scopes:
return None, None
branch_lower = branch_name.lower()
# Check each suggested scope
for scope in suggested_scopes:
scope_lower = scope.lower()
# Match scope anywhere in branch name
if scope_lower in branch_lower:
return scope, f"scope:{scope}"
# Also check hyphenated versions
if scope_lower.replace("-", "") in branch_lower.replace("-", ""):
return scope, f"scope:{scope}"
# Try to extract scope from common patterns
# e.g., feature/auth-login, fix/api-error, plugin/github-workflows
patterns = [
r"^(?:feature|fix|plugin|bugfix|hotfix)/([a-z0-9-]+)",
r"^([a-z0-9-]+)/",
]
for pattern in patterns:
match = re.search(pattern, branch_lower)
if match:
potential_scope = match.group(1).split("-")[0]
# Check if this matches a suggested scope
for scope in suggested_scopes:
if potential_scope == scope.lower():
return scope, f"scope:{scope}"
return None, None
def get_branch_info(suggested_scopes=None):
"""Get current branch and detect related issues and scope."""
branch_name = run_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
if not branch_name:
return None
# Extract issue numbers from branch name (can have multiple)
issue_numbers = []
patterns = [
r"issue-(\d+)",
r"/(\d+)-",
r"^(\d+)-",
r"-(\d+)(?:$|-)"
]
for pattern in patterns:
matches = re.findall(pattern, branch_name)
for match in matches:
num = int(match)
if num not in issue_numbers:
issue_numbers.append(num)
# Detect scope from branch name
detected_scope = None
scope_label = None
if suggested_scopes:
detected_scope, scope_label = detect_branch_scope(branch_name, suggested_scopes)
return {
"name": branch_name,
"relatedIssues": issue_numbers if issue_numbers else [],
"detectedScope": detected_scope,
"scopeLabel": scope_label
}
def get_suggested_scopes():
"""Get suggested scopes from git-conventional-commits.json or project structure."""
scopes = []
# First check for configured scopes in git-conventional-commits.json
config_path = Path("git-conventional-commits.json")
if config_path.exists():
try:
with open(config_path) as f:
data = json.load(f)
configured_scopes = data.get("convention", {}).get("commitScopes", [])
if configured_scopes:
return configured_scopes
except Exception:
pass
# Check for plugin directories
try:
plugin_dirs = []
for item in Path(".").iterdir():
if item.is_dir() and (item / "plugin.json").exists():
plugin_dirs.append(item.name)
elif item.is_dir() and (item / ".claude-plugin" / "plugin.json").exists():
plugin_dirs.append(item.name)
if plugin_dirs:
return plugin_dirs
except Exception:
pass
# Fall back to top-level directories (excluding common non-scopes)
exclude = {"node_modules", "dist", "build", "coverage", ".git", ".claude", "__pycache__", ".venv", "venv"}
try:
scopes = [
d.name for d in Path(".").iterdir()
if d.is_dir() and d.name not in exclude and not d.name.startswith(".")
]
except Exception:
pass
return scopes
def get_label_stocktake():
"""Get existing labels and identify missing standard labels."""
# Standard labels we expect
standard_labels = {
# Type labels
"bug", "feature", "enhancement", "documentation", "refactor", "chore",
# Priority labels
"priority:critical", "priority:high", "priority:medium", "priority:low"
}
# Get existing labels
output = run_gh_command([
"label", "list",
"--json", "name",
"--limit", "100"
])
existing_labels = set()
if output:
try:
data = json.loads(output)
existing_labels = {label.get("name", "") for label in data}
except json.JSONDecodeError:
pass
# Find missing labels
missing = sorted(list(standard_labels - existing_labels))
# Recommended labels (the type labels)
recommended = ["bug", "feature", "enhancement", "documentation", "refactor", "chore"]
return {
"existing": len(existing_labels),
"missing": missing,
"recommended": recommended
}
def get_issue_cache_info():
"""Get information about the issue cache."""
cache_path = Path(".claude/github-workflows") / "active-issues.json"
if not cache_path.exists():
return None
try:
with open(cache_path) as f:
data = json.load(f)
return {
"count": len(data.get("issues", [])),
"lastSync": data.get("lastSync", ""),
"filter": data.get("filter", "")
}
except Exception:
return None
def sync_issues():
"""Sync issues using the issue-tracker script."""
script_dir = Path(__file__).parent
tracker_script = script_dir / "issue-tracker.py"
if tracker_script.exists():
try:
subprocess.run(
[sys.executable, str(tracker_script), "sync", "assigned"],
capture_output=True,
check=True
)
return True
except subprocess.CalledProcessError:
pass
return False
def load_environment():
"""Load existing environment file."""
env_path = get_env_path()
if env_path.exists():
try:
with open(env_path) as f:
return json.load(f)
except Exception:
pass
return None
def is_initialized_today():
"""Check if environment was initialized today."""
env = load_environment()
if not env:
return False
try:
init_time = datetime.fromisoformat(env["initialized"].replace("Z", "+00:00"))
now = datetime.now(timezone.utc)
return (now - init_time) < timedelta(hours=24)
except Exception:
return False
def initialize_environment(force=False):
"""Initialize the GitHub workflow environment."""
# Check if already initialized
if not force and is_initialized_today():
print("Environment already initialized today. Use --force to re-initialize.")
show_environment()
return True
print("Initializing GitHub workflow environment...\n")
# Gather all context
env = {
"initialized": datetime.now(timezone.utc).isoformat()
}
# Repository info
print(" Detecting repository...", end=" ")
repo_info = get_repository_info()
if repo_info:
env["repository"] = repo_info
print(f"✓ {repo_info['fullName']}")
else:
print("✗ Not found")
return False
# User info
print(" Getting user info...", end=" ")
user_info = get_user_info()
if user_info:
env["user"] = user_info
print(f"✓ {user_info['login']}")
else:
print("✗ Not authenticated")
# Project board
print(" Finding project board...", end=" ")
owner = repo_info.get("owner", "") if repo_info else ""
project = get_project_board(owner)
if project:
env["projectBoard"] = project
print(f"✓ {project['title']} (#{project['number']})")
else:
print("- None found")
# Milestone
print(" Getting milestone...", end=" ")
repo_name = repo_info.get("name", "") if repo_info else ""
milestone = get_milestone(owner, repo_name)
if milestone:
env["milestone"] = milestone
due = f" (due {milestone['dueOn'][:10]})" if milestone.get('dueOn') else ""
print(f"✓ {milestone['title']}{due}")
else:
print("- None active")
# Get suggested scopes for branch detection
print(" Analyzing project scopes...", end=" ")
suggested_scopes = get_suggested_scopes()
if suggested_scopes:
print(f"✓ {len(suggested_scopes)} scopes")
else:
print("- None detected")
# Get label stocktake
print(" Checking labels...", end=" ")
label_stocktake = get_label_stocktake()
env["labels"] = {
"existing": label_stocktake["existing"],
"missing": label_stocktake["missing"],
"recommended": label_stocktake["recommended"],
"suggestedScopes": suggested_scopes if suggested_scopes else []
}
if label_stocktake["missing"]:
print(f"✓ {label_stocktake['existing']} labels ({len(label_stocktake['missing'])} missing)")
else:
print(f"✓ {label_stocktake['existing']} labels (all standard present)")
# Branch info
print(" Detecting branch...", end=" ")
branch = get_branch_info(suggested_scopes)
if branch:
env["branch"] = branch
issues_str = ""
if branch.get('relatedIssues'):
issue_nums = ", #".join(str(n) for n in branch['relatedIssues'])
issues_str = f" → Issues #{issue_nums}"
scope_str = ""
if branch.get('detectedScope'):
scope_str = f" [{branch['detectedScope']}]"
print(f"✓ {branch['name']}{issues_str}{scope_str}")
else:
print("- Not in git repo")
# Sync issues
print(" Syncing issues...", end=" ")
if sync_issues():
cache_info = get_issue_cache_info()
if cache_info:
env["issueCache"] = cache_info
print(f"✓ {cache_info['count']} issues cached")
else:
print("✓ Done")
else:
print("- Sync failed")
# Add preferences section
env["preferences"] = {
"projectType": "personal", # Default to personal project
"defaultIssueFilter": "all", # Default to all issues for personal projects
"defaultProject": project.get("number") if project else None
}
# Add setup section
env["setup"] = {
"labelsComplete": len(label_stocktake["missing"]) == 0,
"projectBoardExists": project is not None,
"milestonesExist": milestone is not None
}
# Save environment
env_path = get_env_path()
with open(env_path, "w") as f:
json.dump(env, f, indent=2)
print(f"\n✅ Environment saved to {env_path}")
# Show summary
print("\n" + "=" * 50)
show_summary(env)
return True
def show_environment():
"""Show current environment details."""
env = load_environment()
if not env:
print("Environment not initialized. Run: python init-environment.py")
return
print(json.dumps(env, indent=2))
def show_summary(env):
"""Show a summary of the environment."""
print("\n📋 Workflow Environment Summary\n")
if env.get("repository"):
print(f"Repository: {env['repository']['fullName']}")
if env.get("user"):
print(f"User: {env['user']['login']}")
if env.get("projectBoard"):
print(f"Project Board: {env['projectBoard']['title']} (#{env['projectBoard']['number']})")
if env.get("milestone"):
due = f" - due {env['milestone']['dueOn'][:10]}" if env['milestone'].get('dueOn') else ""
print(f"Milestone: {env['milestone']['title']}{due}")
if env.get("branch"):
issues = ""
if env['branch'].get('relatedIssues'):
issue_nums = ", #".join(str(n) for n in env['branch']['relatedIssues'])
issues = f" → Issues #{issue_nums}"
print(f"Branch: {env['branch']['name']}{issues}")
if env['branch'].get('detectedScope'):
print(f" Scope: {env['branch']['detectedScope']} ({env['branch'].get('scopeLabel', '')})")
if env.get("labels", {}).get("suggestedScopes"):
print(f"Suggested Scopes: {', '.join(env['labels']['suggestedScopes'][:5])}")
if env.get("issueCache"):
print(f"Cached Issues: {env['issueCache']['count']}")
# Display label stocktake
if env.get("labels"):
labels = env["labels"]
print(f"\n📋 Label Stocktake:")
print(f" Existing: {labels.get('existing', 0)} labels")
if labels.get("missing"):
print(f" Missing: {len(labels['missing'])} standard labels")
for label in labels["missing"][:5]:
print(f" - {label}")
if len(labels["missing"]) > 5:
print(f" ... and {len(labels['missing']) - 5} more")
# Display preferences
if env.get("preferences"):
prefs = env["preferences"]
print(f"\n⚙️ Preferences:")
print(f" Project Type: {prefs.get('projectType', 'personal')}")
print(f" Default Issue Filter: {prefs.get('defaultIssueFilter', 'all')}")
if prefs.get("defaultProject"):
print(f" Default Project: #{prefs['defaultProject']}")
# Display setup recommendations
if env.get("setup"):
setup = env["setup"]
print(f"\n🔧 Setup Recommendations:")
if setup.get("labelsComplete"):
print(" ✅ All standard labels present")
else:
print(" ⚠️ Missing labels - run /github-workflows:label-sync standard to create")
if setup.get("projectBoardExists"):
print(" ✅ Project board exists")
else:
print(" ⚠️ No project board - run /github-workflows:project-create to create")
if setup.get("milestonesExist"):
print(" ✅ Active milestone found")
else:
print(" ⚠️ No active milestone - run /github-workflows:milestone-create to create")
print("\n💡 Tips:")
print(" - Use /commit-smart to commit with auto issue refs")
print(" - Use /workflow-status for detailed workflow state")
print(" - Use /issue-track to refresh issue cache")
def check_initialized():
"""Check if environment is initialized and recent."""
if is_initialized_today():
print("initialized")
return 0
elif load_environment():
print("stale")
return 1
else:
print("not_initialized")
return 2
def main():
if len(sys.argv) < 2:
initialize_environment()
return
arg = sys.argv[1]
if arg == "--force":
initialize_environment(force=True)
elif arg == "--check":
sys.exit(check_initialized())
elif arg == "--show":
show_environment()
elif arg == "--summary":
env = load_environment()
if env:
show_summary(env)
else:
print("Not initialized")
else:
print(f"Unknown argument: {arg}")
print(__doc__)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Issue Tracker - Sync and cache GitHub issues for commit integration.
Usage:
python issue-tracker.py sync [filter] [value] # Sync issues from GitHub
python issue-tracker.py show # Display cached issues
python issue-tracker.py context # Show issues filtered by context
python issue-tracker.py scope # Show issues matching branch scope
python issue-tracker.py branch # Show only branch-selected issues
python issue-tracker.py select <numbers...> # Select issues for current branch
python issue-tracker.py find-related [files...] # Find related issues
python issue-tracker.py get [number] # Get specific issue
python issue-tracker.py suggest-refs # Suggest issue refs for staged changes
python issue-tracker.py clear # Clear the cache
"""
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
# Cache file location (in .claude for project-specific storage)
CACHE_DIR = ".claude/github-workflows"
CACHE_FILE = "active-issues.json"
def get_cache_path():
"""Get the cache file path, creating directory if needed."""
cache_dir = Path(CACHE_DIR)
cache_dir.mkdir(exist_ok=True)
return cache_dir / CACHE_FILE
def run_gh_command(args):
"""Run a GitHub CLI command and return JSON output."""
try:
result = subprocess.run(
["gh"] + args,
capture_output=True,
text=True,
encoding='utf-8',
errors='replace',
check=True
)
return json.loads(result.stdout) if result.stdout.strip() else []
except subprocess.CalledProcessError as e:
print(f"Error running gh command: {e.stderr}", file=sys.stderr)
return None
except json.JSONDecodeError as e:
print(f"Error parsing JSON: {e}", file=sys.stderr)
return None
def get_repo_info():
"""Get current repository owner/name."""
try:
result = subprocess.run(
["gh", "repo", "view", "--json", "nameWithOwner"],
capture_output=True,
text=True,
encoding='utf-8',
errors='replace',
check=True
)
data = json.loads(result.stdout)
return data.get("nameWithOwner", "unknown/unknown")
except Exception:
return "unknown/unknown"
def sync_issues(filter_type="assigned", filter_value=None):
"""Sync issues from GitHub based on filter."""
args = ["issue", "list", "--state", "open", "--json",
"number,title,state,labels,assignees,milestone,createdAt,updatedAt,url,body"]
if filter_type == "assigned":
args.extend(["--assignee", "@me"])
elif filter_type == "labeled" and filter_value:
args.extend(["--label", filter_value])
elif filter_type == "milestone" and filter_value:
args.extend(["--milestone", filter_value])
elif filter_type == "all":
pass # No additional filters
else:
args.extend(["--assignee", "@me"]) # Default to assigned
print(f"Syncing issues (filter: {filter_type})...", file=sys.stderr)
issues = run_gh_command(args)
if issues is None:
return False
# Process issues
processed = []
for issue in issues:
processed.append({
"number": issue.get("number"),
"title": issue.get("title", ""),
"state": issue.get("state", "open"),
"labels": [l.get("name", "") for l in issue.get("labels", [])],
"assignees": [a.get("login", "") for a in issue.get("assignees", [])],
"milestone": issue.get("milestone", {}).get("title") if issue.get("milestone") else None,
"created_at": issue.get("createdAt", ""),
"updated_at": issue.get("updatedAt", ""),
"url": issue.get("url", ""),
"body_preview": (issue.get("body", "") or "")[:500]
})
# Save to cache
cache_data = {
"lastSync": datetime.now(timezone.utc).isoformat(),
"repository": get_repo_info(),
"filter": filter_type,
"filterValue": filter_value,
"issues": processed
}
cache_path = get_cache_path()
with open(cache_path, "w") as f:
json.dump(cache_data, f, indent=2)
print(f"Cached {len(processed)} issues to {cache_path}", file=sys.stderr)
return True
def load_cache():
"""Load issues from cache."""
cache_path = get_cache_path()
if not cache_path.exists():
return None
try:
with open(cache_path) as f:
return json.load(f)
except Exception as e:
print(f"Error loading cache: {e}", file=sys.stderr)
return None
def load_environment():
"""Load environment from env.json."""
env_path = Path(CACHE_DIR) / "env.json"
if not env_path.exists():
return None
try:
with open(env_path) as f:
return json.load(f)
except Exception:
return None
def filter_by_scope(issues, scope_label):
"""Filter issues by scope label."""
if not scope_label:
return issues
# Extract scope name for exact matching
scope_name = scope_label.replace("scope:", "")
return [
issue for issue in issues
if scope_label in issue.get("labels", []) or
f"scope:{scope_name}" in issue.get("labels", []) or
scope_name in issue.get("labels", []) # Exact match only, no substring
]
def filter_by_project(issues, project_number):
"""Filter issues that are in a specific project board."""
if not project_number:
return issues
# Get issues from project via GraphQL
env = load_environment()
if not env:
print("Warning: No environment loaded, skipping project filtering", file=sys.stderr)
return issues
owner = env.get("user", {}).get("login", "")
if not owner:
print("Warning: No user login in environment, skipping project filtering", file=sys.stderr)
return issues
# Also get repository owner for org projects
repo_owner = env.get("repository", {}).get("owner", "")
def try_query(query_owner, is_org=False):
"""Try to query project items for user or org."""
if is_org:
query = '''
query($owner: String!, $number: Int!) {
organization(login: $owner) {
projectV2(number: $number) {
items(first: 100) {
nodes {
content {
... on Issue {
number
}
}
}
}
}
}
}
'''
else:
query = '''
query($owner: String!, $number: Int!) {
user(login: $owner) {
projectV2(number: $number) {
items(first: 100) {
nodes {
content {
... on Issue {
number
}
}
}
}
}
}
}
'''
result = subprocess.run(
["gh", "api", "graphql",
"-f", f"query={query}",
"-f", f"owner={query_owner}",
"-F", f"number={project_number}"],
capture_output=True,
text=True,
encoding='utf-8',
errors='replace'
)
if result.returncode != 0:
return None, result.stderr
data = json.loads(result.stdout)
# Check for errors in response
if data.get("errors"):
error_msg = data["errors"][0].get("message", "Unknown GraphQL error")
return None, error_msg
# Extract items based on query type
if is_org:
items = data.get("data", {}).get("organization", {}).get("projectV2", {}).get("items", {}).get("nodes", [])
else:
items = data.get("data", {}).get("user", {}).get("projectV2", {}).get("items", {}).get("nodes", [])
return items, None
# Try user project first
items, error = try_query(owner, is_org=False)
# If user query failed and we have a different repo owner, try org
if items is None and repo_owner and repo_owner != owner:
items, error = try_query(repo_owner, is_org=True)
# If still no items, log the error
if items is None:
print(f"Warning: Project filtering failed: {error}", file=sys.stderr)
print("Troubleshooting: Check that project #{} exists and you have access".format(project_number), file=sys.stderr)
return issues
# Extract issue numbers
project_issues = set()
for item in items:
content = item.get("content", {})
if content and content.get("number"):
project_issues.add(content["number"])
return [issue for issue in issues if issue["number"] in project_issues]
def filter_by_assignment(issues, user_login):
"""Filter issues assigned to a specific user."""
if not user_login:
return issues
return [
issue for issue in issues
if user_login in issue.get("assignees", [])
]
def apply_context_filters(issues, env):
"""Apply contextual filters based on environment settings."""
if not env:
return issues
filtered = issues
# Filter by project if set
default_project = env.get("preferences", {}).get("defaultProject")
if default_project:
filtered = filter_by_project(filtered, default_project)
# Filter by detected scope if available
scope_label = env.get("branch", {}).get("scopeLabel")
if scope_label:
scope_filtered = filter_by_scope(filtered, scope_label)
# Only apply if it doesn't filter everything out
if scope_filtered:
filtered = scope_filtered
# Filter by assignment for team projects
project_type = env.get("preferences", {}).get("projectType")
if project_type == "team":
user_login = env.get("user", {}).get("login")
if user_login:
filtered = filter_by_assignment(filtered, user_login)
return filtered
def get_branch_issues():
"""Get issues related to current branch from env.json."""
env = load_environment()
if not env:
return []
return env.get("branch", {}).get("relatedIssues", [])
def show_issues():
"""Display cached issues as a task list."""
cache = load_cache()
if not cache:
print("No cached issues. Run: python issue-tracker.py sync")
return
# Check cache age
last_sync = datetime.fromisoformat(cache["lastSync"].replace("Z", "+00:00"))
age_minutes = (datetime.now(timezone.utc) - last_sync).total_seconds() / 60
if age_minutes > 60:
print(f"⚠️ Cache is {int(age_minutes)} minutes old. Consider running: sync")
print()
print(f"📋 Active Issues (synced {int(age_minutes)} minutes ago)")
print(f"Repository: {cache['repository']}")
print(f"Filter: {cache['filter']}")
if cache.get('filterValue'):
print(f"Value: {cache['filterValue']}")
print()
issues = cache.get("issues", [])
if not issues:
print("No issues found.")
return
# Sort by priority
high_priority = []
normal = []
for issue in issues:
labels = issue.get("labels", [])
is_high = any("high" in l.lower() for l in labels)
if is_high:
high_priority.append(issue)
else:
normal.append(issue)
def print_issue(issue):
labels = ", ".join(issue.get("labels", [])) or "none"
milestone = issue.get("milestone") or "none"
print(f"┌─ #{issue['number']} {issue['title']}")
print(f"│ Labels: {labels}")
if milestone != "none":
print(f"│ Milestone: {milestone}")
print(f"└─ Use: Closes #{issue['number']} or Refs #{issue['number']}")
print()
if high_priority:
print("HIGH PRIORITY:")
for issue in high_priority:
print_issue(issue)
if normal:
print("NORMAL PRIORITY:")
for issue in normal:
print_issue(issue)
print("💡 Tip: Use /commit-smart to auto-suggest these in commits")
def get_current_branch():
"""Get the current git branch name."""
try:
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
capture_output=True,
text=True,
encoding='utf-8',
errors='replace',
check=True
)
return result.stdout.strip()
except Exception:
return None
def extract_issue_from_branch(branch_name):
"""Extract issue number from branch name."""
if not branch_name:
return None
# Patterns: feature/issue-42, feature/42-auth, fix/42, 42-something
patterns = [
r"issue-(\d+)",
r"/(\d+)-",
r"^(\d+)-",
r"-(\d+)$"
]
for pattern in patterns:
match = re.search(pattern, branch_name)
if match:
return int(match.group(1))
return None
def find_related_issues(files=None):
"""Find issues related to given files or staged changes."""
cache = load_cache()
if not cache:
print("No cached issues. Run sync first.", file=sys.stderr)
return []
issues = cache.get("issues", [])
if not issues:
return []
# Get files to analyze
if not files:
# Get staged files
try:
result = subprocess.run(
["git", "diff", "--cached", "--name-only"],
capture_output=True,
text=True,
encoding='utf-8',
errors='replace',
check=True
)
files = result.stdout.strip().split("\n") if result.stdout.strip() else []
except Exception:
files = []
# Score each issue
scored = []
branch = get_current_branch()
branch_issue = extract_issue_from_branch(branch)
# Get all related issues from env.json
env = load_environment()
branch_related_issues = env.get("branch", {}).get("relatedIssues", []) if env else []
detected_scope = env.get("branch", {}).get("scopeLabel") if env else None
default_project = env.get("preferences", {}).get("defaultProject") if env else None
for issue in issues:
score = 0
reasons = []
# Branch match (highest priority) - check both env.json and branch name
if issue["number"] in branch_related_issues:
score += 100
reasons.append("branch selected issue")
elif branch_issue and issue["number"] == branch_issue:
score += 100
reasons.append("branch name match")
# Scope match
if detected_scope and detected_scope in issue.get("labels", []):
score += 50
reasons.append(f"scope match: {detected_scope}")
# Keyword matching
issue_text = f"{issue['title']} {issue.get('body_preview', '')}".lower()
for file in files:
# Extract keywords from file path
parts = Path(file).stem.replace("-", " ").replace("_", " ").split()
for part in parts:
if len(part) > 2 and part.lower() in issue_text:
score += 10
if f"file keyword: {part}" not in reasons:
reasons.append(f"file keyword: {part}")
# Label matching
labels = issue.get("labels", [])
for file in files:
if "test" in file and any("test" in l.lower() for l in labels):
score += 5
if "auth" in file and any("auth" in l.lower() for l in labels):
score += 5
if "api" in file and any("api" in l.lower() for l in labels):
score += 5
if score > 0:
scored.append({
"issue": issue,
"score": score,
"reasons": reasons
})
# Sort by score
scored.sort(key=lambda x: x["score"], reverse=True)
return scored
def suggest_refs():
"""Suggest issue references for current staged changes."""
related = find_related_issues()
if not related:
print("No related issues found.")
return
branch = get_current_branch()
branch_issue = extract_issue_from_branch(branch)
# Get all related issues from env.json
env = load_environment()
branch_related_issues = env.get("branch", {}).get("relatedIssues", []) if env else []
print("Suggested issue references for your commit:\n")
for i, item in enumerate(related[:5]): # Top 5
issue = item["issue"]
score = item["score"]
reasons = item["reasons"]
# Determine reference type
is_branch_issue = issue["number"] in branch_related_issues or (branch_issue and issue["number"] == branch_issue)
if is_branch_issue:
ref_type = "Closes"
confidence = "HIGH"
elif score >= 50:
ref_type = "Closes"
confidence = "HIGH"
elif score >= 20:
ref_type = "Refs"
confidence = "MEDIUM"
else:
ref_type = "Refs"
confidence = "LOW"
print(f"{i+1}. [{confidence}] {ref_type} #{issue['number']}")
print(f" Title: {issue['title']}")
print(f" Match: {', '.join(reasons)}")
print()
# Provide formatted footer for all branch issues
if related:
# Collect all branch issues first
branch_refs = []
other_refs = []
for item in related[:5]:
issue = item["issue"]
is_branch_issue = issue["number"] in branch_related_issues or (branch_issue and issue["number"] == branch_issue)
if is_branch_issue:
branch_refs.append(f"Closes #{issue['number']}")
elif item["score"] >= 20:
other_refs.append(f"Refs #{issue['number']}")
print("Suggested commit footer:")
if branch_refs:
print("\n".join(branch_refs))
if other_refs:
print("\n".join(other_refs[:2])) # Limit other refs
def get_issue(number):
"""Get a specific issue from cache."""
cache = load_cache()
if not cache:
return None
for issue in cache.get("issues", []):
if issue["number"] == number:
return issue
return None
def show_filtered_issues(filter_type="all"):
"""Display issues with specific filtering."""
cache = load_cache()
if not cache:
print("No cached issues. Run: python issue-tracker.py sync")
return
issues = cache.get("issues", [])
env = load_environment()
# Apply filters based on type
if filter_type == "context":
issues = apply_context_filters(issues, env)
filter_desc = "context (project + scope + assignment)"
elif filter_type == "scope":
scope_label = env.get("branch", {}).get("scopeLabel") if env else None
if scope_label:
issues = filter_by_scope(issues, scope_label)
filter_desc = f"scope: {scope_label}"
else:
filter_desc = "scope (none detected)"
elif filter_type == "branch":
branch_issues = get_branch_issues()
issues = [i for i in issues if i["number"] in branch_issues]
filter_desc = f"branch issues: {branch_issues}"
else:
filter_desc = "all"
# Display
last_sync = datetime.fromisoformat(cache["lastSync"].replace("Z", "+00:00"))
age_minutes = (datetime.now(timezone.utc) - last_sync).total_seconds() / 60
print(f"📋 Filtered Issues ({filter_desc})")
print(f"Synced {int(age_minutes)} minutes ago")
print()
if not issues:
print("No issues match this filter.")
return
# Sort by priority
high_priority = []
normal = []
for issue in issues:
labels = issue.get("labels", [])
is_high = any("high" in l.lower() for l in labels)
if is_high:
high_priority.append(issue)
else:
normal.append(issue)
def print_issue(issue):
labels = ", ".join(issue.get("labels", [])) or "none"
print(f"┌─ #{issue['number']} {issue['title']}")
print(f"│ Labels: {labels}")
print(f"└─ Use: Closes #{issue['number']} or Refs #{issue['number']}")
print()
if high_priority:
print("HIGH PRIORITY:")
for issue in high_priority:
print_issue(issue)
if normal:
print("NORMAL PRIORITY:")
for issue in normal:
print_issue(issue)
def select_branch_issues(issue_numbers):
"""Set the related issues for the current branch."""
env = load_environment()
if not env:
print("Environment not initialized. Run /github-workflows:init first.")
return False
# Validate issue numbers exist in cache
cache = load_cache()
if cache:
cached_numbers = {i["number"] for i in cache.get("issues", [])}
invalid = [n for n in issue_numbers if n not in cached_numbers]
if invalid:
print(f"Warning: Issues not in cache: {invalid}")
print("Run /issue-track sync to update cache.")
# Update env.json
if "branch" not in env:
env["branch"] = {"name": "", "relatedIssues": []}
env["branch"]["relatedIssues"] = issue_numbers
env_path = Path(CACHE_DIR) / "env.json"
with open(env_path, "w") as f:
json.dump(env, f, indent=2)
print(f"✓ Selected issues for branch: {issue_numbers}")
return True
def clear_cache():
"""Clear the issue cache."""
cache_path = get_cache_path()
if cache_path.exists():
cache_path.unlink()
print(f"Cleared cache: {cache_path}")
else:
print("No cache to clear.")
def main():
if len(sys.argv) < 2:
show_issues()
return
command = sys.argv[1]
if command == "sync":
filter_type = sys.argv[2] if len(sys.argv) > 2 else "assigned"
filter_value = sys.argv[3] if len(sys.argv) > 3 else None
if sync_issues(filter_type, filter_value):
show_issues()
elif command == "show":
show_issues()
elif command == "context":
# Show issues filtered by context (project + scope + assignment)
show_filtered_issues("context")
elif command == "scope":
# Show issues matching branch scope
show_filtered_issues("scope")
elif command == "branch":
# Show only issues selected for current branch
show_filtered_issues("branch")
elif command == "select":
# Select issues for current branch
if len(sys.argv) < 3:
print("Usage: issue-tracker.py select <issue_numbers...>")
print("Example: issue-tracker.py select 42 43 44")
return
try:
issue_numbers = [int(n) for n in sys.argv[2:]]
select_branch_issues(issue_numbers)
except ValueError:
print("Error: Issue numbers must be integers")
return
elif command == "find-related":
files = sys.argv[2:] if len(sys.argv) > 2 else None
related = find_related_issues(files)
print(json.dumps(related, indent=2, default=str))
elif command == "suggest-refs":
suggest_refs()
elif command == "get":
if len(sys.argv) < 3:
print("Usage: issue-tracker.py get <number>")
return
number = int(sys.argv[2])
issue = get_issue(number)
if issue:
print(json.dumps(issue, indent=2))
else:
print(f"Issue #{number} not found in cache.")
elif command == "clear":
clear_cache()
elif command == "json":
# Output cache as JSON for other tools
cache = load_cache()
if cache:
print(json.dumps(cache, indent=2))
else:
print("{}")
else:
print(f"Unknown command: {command}")
print(__doc__)
if __name__ == "__main__":
main()
{
"schema_version": "2.0",
"meta": {
"generated_at": "2026-01-16T20:43:33.011Z",
"slug": "c0ntr0lledcha0s-managing-commits",
"source_url": "https://github.com/C0ntr0lledCha0s/claude-code-plugin-automations/tree/main/github-workflows/skills/managing-commits",
"source_ref": "main",
"model": "claude",
"analysis_version": "3.0.0",
"source_type": "community",
"content_hash": "c956243d46d4f1466dfaa02861cdb4c73913b999ae312dcb8cb9bfaabc3f9597",
"tree_hash": "2fcd24a6a09b135774eedd4b2e7072688a4f6b2b88aed50e7b443a5a6b9a5784"
},
"skill": {
"name": "managing-commits",
"description": "Git commit quality and conventional commits expertise with automatic issue tracking integration. Auto-invokes when the user explicitly asks about commit message format, commit quality, conventional commits, commit history analysis, issue references in commits, or requests help writing commit messages. Integrates with the issue cache for automatic issue references.",
"summary": "Git commit quality and conventional commits expertise with automatic issue tracking integration. Aut...",
"icon": "🔧",
"version": "1.2.0",
"author": "C0ntr0lledCha0s",
"license": "MIT",
"category": "devops",
"tags": [
"git",
"conventional-commits",
"commit-messages",
"github",
"version-control"
],
"supported_tools": [
"claude",
"codex",
"claude-code"
],
"risk_factors": [
"scripts",
"external_commands",
"filesystem",
"network"
]
},
"security_audit": {
"risk_level": "low",
"is_blocked": false,
"safe_to_publish": true,
"summary": "Legitimate git commit management skill. All 249 static findings are FALSE POSITIVES. The 'Weak cryptographic algorithm' detections are JSON commit templates and documentation being misidentified. External commands use safe subprocess patterns with hardcoded command names and list arguments. Git operations (log, diff, status) are read-only and necessary for commit analysis. Network access is limited to GitHub CLI (gh) for issue tracking. Purpose matches documented capabilities.",
"risk_factor_evidence": [
{
"factor": "scripts",
"evidence": [
{
"file": "scripts/conventional-commits.py",
"line_start": 1,
"line_end": 271
},
{
"file": "scripts/commit-analyzer.py",
"line_start": 1,
"line_end": 402
},
{
"file": "scripts/issue-tracker.py",
"line_start": 1,
"line_end": 773
}
]
},
{
"factor": "external_commands",
"evidence": [
{
"file": "scripts/conventional-commits.py",
"line_start": 21,
"line_end": 28
},
{
"file": "scripts/commit-analyzer.py",
"line_start": 29,
"line_end": 36
},
{
"file": "scripts/issue-tracker.py",
"line_start": 36,
"line_end": 53
}
]
},
{
"factor": "filesystem",
"evidence": [
{
"file": "scripts/conventional-commits.py",
"line_start": 177,
"line_end": 190
},
{
"file": "scripts/issue-tracker.py",
"line_start": 26,
"line_end": 34
}
]
},
{
"factor": "network",
"evidence": [
{
"file": "scripts/init-environment.py",
"line_start": 30,
"line_end": 46
},
{
"file": "scripts/issue-tracker.py",
"line_start": 36,
"line_end": 53
}
]
}
],
"critical_findings": [],
"high_findings": [],
"medium_findings": [],
"low_findings": [],
"dangerous_patterns": [],
"files_scanned": 10,
"total_lines": 4175,
"audit_model": "claude",
"audited_at": "2026-01-16T20:43:33.010Z"
},
"content": {
"user_title": "Create conventional commits with automatic issue linking",
"value_statement": "Writing quality commit messages is difficult and time-consuming. This skill provides expert guidance on conventional commit formats, analyzes commit quality, and automatically links commits to GitHub issues for better traceability.",
"seo_keywords": [
"conventional commits",
"git commit messages",
"claude commit helper",
"codex git workflow",
"claude-code commits",
"commit message format",
"git history quality",
"github issue tracking"
],
"actual_capabilities": [
"Generates conventional commit messages from staged changes",
"Validates commit message format against conventional commits spec",
"Analyzes commit history for quality, size, and patterns",
"Automatically links commits to GitHub issues by branch name",
"Groups multiple files into logical atomic commits",
"Provides interactive commit workflow with step-by-step guidance"
],
"limitations": [
"Does not execute git commands directly (skill provides guidance only)",
"Requires GitHub CLI (gh) installed for issue tracking features",
"Cannot commit changes without explicit user confirmation",
"Issue linking requires active issue cache or branch-based detection"
],
"use_cases": [
{
"target_user": "Individual developers",
"title": "Write better commits",
"description": "Generate conventional commit messages automatically and validate your commit history for quality standards"
},
{
"target_user": "Team leads",
"title": "Enforce commit standards",
"description": "Review team commit history, identify quality issues, and provide consistent commit message patterns"
},
{
"target_user": "CI/CD engineers",
"title": "Link issues to commits",
"description": "Automatically connect GitHub issues to commits based on branch names and issue references"
}
],
"prompt_templates": [
{
"title": "Basic commit help",
"scenario": "Need to commit staged changes",
"prompt": "Help me commit these staged changes with a proper commit message"
},
{
"title": "Conventional format",
"scenario": "Follow commit conventions",
"prompt": "Generate a conventional commit message for my changes following the feat/fix/docs pattern"
},
{
"title": "Multi-file commits",
"scenario": "Group multiple changes",
"prompt": "Analyze my changes and group them into logical atomic commits with proper messages"
},
{
"title": "Issue linking",
"scenario": "Connect to GitHub issues",
"prompt": "Find related GitHub issues for my current changes and suggest proper issue references"
}
],
"output_examples": [
{
"input": "Help me commit my staged auth changes",
"output": [
"Detected 3 files changed (+127 lines)",
"Type: feat (new feature)",
"Scope: auth",
"Related issue: #142 \"Implement JWT authentication\"",
"",
"Generated commit message:",
"feat(auth): add JWT token refresh mechanism",
"",
"Implements automatic token refresh 5 minutes before expiration",
"to maintain seamless user sessions without re-authentication.",
"",
"Closes #142",
"",
"Create this commit? [y/n]"
]
},
{
"input": "Analyze my commit history for quality issues",
"output": [
"Found 8 commits on current branch",
"",
"Quality breakdown: 5 good, 2 need improvement, 1 poor",
"",
"Issues identified:",
"- Commit 3 (abc123): \"wip\" - Not descriptive, needs reword",
"- Commit 5 (def456): \"more changes\" - No conventional format",
"- Commit 7 (ghi789): Too large (842 LOC) - Should be split",
"",
"Recommendations:",
"- Squash commits 3 and 4 (both WIP)",
"- Reword commit 5 with proper format",
"- Consider splitting commit 7 into 3 smaller commits"
]
},
{
"input": "Commit all my changes with proper grouping",
"output": [
"Analyzed 12 changed files across 3 scopes",
"",
"Group 1: feat(auth) - 5 files (+312 LOC)",
" - src/auth/jwt.ts, src/auth/types.ts, src/auth/index.ts",
" - tests/auth/jwt.test.ts, docs/authentication.md",
"",
"Group 2: fix(api) - 2 files (+15 LOC)",
" - src/api/validation.ts, tests/api/validation.test.ts",
"",
"Group 3: docs - 1 file (+8 LOC)",
" - README.md",
"",
"Create these 4 commits? [y/n/edit]"
]
}
],
"best_practices": [
"Use conventional commit format with type (feat, fix, docs) and optional scope",
"Keep subject under 50 characters and use imperative mood",
"Include body text explaining what and why changed, not how",
"Link commits to issues using Closes #N or Refs #N in footer"
],
"anti_patterns": [
"Writing vague messages like \"fix bug\" or \"update code\" without specifics",
"Including multiple unrelated changes in a single commit",
"Committing large changes over 500 lines without splitting",
"Missing issue references when working on tracked features"
],
"faq": [
{
"question": "What are conventional commits?",
"answer": "Standardized format using types like feat and fix, optional scope, and descriptive subject. Improves changelog generation and automation."
},
{
"question": "Does this skill commit changes?",
"answer": "No. The skill generates and validates commit messages but requires user confirmation before any git commit execution."
},
{
"question": "How does issue linking work?",
"answer": "It detects issue numbers from branch names (feature/issue-42) and matches files to issue titles using keyword analysis."
},
{
"question": "Is GitHub CLI required?",
"answer": "Required only for issue sync features. Commit generation works without gh CLI installed."
},
{
"question": "Can I use this offline?",
"answer": "Yes. Commit message generation and validation work offline. Issue tracking features require network access."
},
{
"question": "How is this different from commitlint?",
"answer": "Commitlint enforces rules. This skill provides AI-powered guidance, generates messages, and offers interactive workflows."
}
]
},
"file_structure": [
{
"name": "assets",
"type": "dir",
"path": "assets",
"children": [
{
"name": "commit-templates.json",
"type": "file",
"path": "assets/commit-templates.json",
"lines": 152
}
]
},
{
"name": "references",
"type": "dir",
"path": "references",
"children": [
{
"name": "commit-patterns.md",
"type": "file",
"path": "references/commit-patterns.md",
"lines": 268
},
{
"name": "conventional-commits.md",
"type": "file",
"path": "references/conventional-commits.md",
"lines": 100
}
]
},
{
"name": "scripts",
"type": "dir",
"path": "scripts",
"children": [
{
"name": "commit-analyzer.py",
"type": "file",
"path": "scripts/commit-analyzer.py",
"lines": 402
},
{
"name": "conventional-commits.py",
"type": "file",
"path": "scripts/conventional-commits.py",
"lines": 271
},
{
"name": "group-files.py",
"type": "file",
"path": "scripts/group-files.py",
"lines": 321
},
{
"name": "init-environment.py",
"type": "file",
"path": "scripts/init-environment.py",
"lines": 632
},
{
"name": "issue-tracker.py",
"type": "file",
"path": "scripts/issue-tracker.py",
"lines": 773
}
]
},
{
"name": "SKILL.md",
"type": "file",
"path": "SKILL.md",
"lines": 912
}
]
}
Related skills
FAQ
What commit format does managing-commits use?
The Conventional Commits format: <type>(<scope>): <subject> with optional body and footer, using Angular-convention types like feat, fix, docs, and chore.
Can it link commits to GitHub issues?
Yes, it integrates with an issue cache and adds references like Closes #N and Ref #N to commit footers.