Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "git",
|
||||
"description": "Git workflow automation and utilities",
|
||||
"version": "0.0.1",
|
||||
"author": {
|
||||
"name": "github.com/openshift-eng"
|
||||
},
|
||||
"skills": [
|
||||
"./skills"
|
||||
],
|
||||
"commands": [
|
||||
"./commands"
|
||||
]
|
||||
}
|
||||
211
commands/branch-cleanup.md
Normal file
211
commands/branch-cleanup.md
Normal file
@@ -0,0 +1,211 @@
|
||||
---
|
||||
description: Clean up old and defunct branches that are no longer needed
|
||||
argument-hint: [--dry-run] [--merged-only] [--remote]
|
||||
---
|
||||
|
||||
## Name
|
||||
git:branch-cleanup
|
||||
|
||||
## Synopsis
|
||||
```
|
||||
/git:branch-cleanup [--dry-run] [--merged-only] [--remote]
|
||||
```
|
||||
|
||||
## Description
|
||||
The `git:branch-cleanup` command identifies and removes old, defunct, or merged branches from your local repository (and optionally from remote). It helps maintain a clean repository by removing branches that are no longer needed, such as:
|
||||
- Branches that have been merged into the main branch
|
||||
- Branches that no longer exist on the remote
|
||||
- Stale feature branches from completed work
|
||||
|
||||
The command performs safety checks to prevent deletion of:
|
||||
- The current branch
|
||||
- Protected branches (main, master, develop, etc.)
|
||||
- Branches with unmerged commits (unless explicitly overridden)
|
||||
|
||||
The spec sections is inspired by https://man7.org/linux/man-pages/man7/man-pages.7.html#top_of_page
|
||||
|
||||
## Implementation
|
||||
The command should follow these steps:
|
||||
|
||||
1. **Identify Main Branch**
|
||||
- Detect the primary branch (main, master, etc.)
|
||||
- Use `git symbolic-ref refs/remotes/origin/HEAD` or `git branch -r` to determine
|
||||
|
||||
2. **Gather Branch Information**
|
||||
- List all local branches: `git branch`
|
||||
- Get current branch: `git branch --show-current`
|
||||
- Identify merged branches: `git branch --merged <main-branch>`
|
||||
- Check remote tracking: `git branch -vv`
|
||||
- Find remote-deleted branches: `git remote prune origin --dry-run`
|
||||
|
||||
3. **Categorize Branches**
|
||||
- **Merged branches**: Fully merged into main branch
|
||||
- **Gone branches**: Remote tracking branch no longer exists
|
||||
- **Stale branches**: Last commit older than threshold (e.g., 3 months)
|
||||
- **Protected branches**: main, master, develop, release/*, hotfix/*
|
||||
|
||||
4. **Present Analysis to User**
|
||||
- Show categorized list of branches with:
|
||||
- Branch name
|
||||
- Last commit date
|
||||
- Merge status
|
||||
- Remote tracking status
|
||||
- Number of commits ahead/behind
|
||||
- Recommend branches safe to delete
|
||||
|
||||
5. **Confirm Deletion**
|
||||
- Ask user to confirm which branches to delete
|
||||
- Present options: all merged, all gone, specific branches, or custom selection
|
||||
- If `--dry-run` flag is present, only show what would be deleted
|
||||
|
||||
6. **Delete Branches**
|
||||
- Local deletion: `git branch -d <branch>` (merged) or `git branch -D <branch>` (force)
|
||||
- Remote deletion (if `--remote` flag): `git push origin --delete <branch>`
|
||||
- Prune remote references: `git remote prune origin`
|
||||
|
||||
7. **Report Results**
|
||||
- List deleted branches
|
||||
- Show any errors or branches that couldn't be deleted
|
||||
- Provide summary statistics
|
||||
|
||||
Implementation logic:
|
||||
```bash
|
||||
# Determine main branch
|
||||
main_branch=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
|
||||
if [ -z "$main_branch" ]; then
|
||||
main_branch="main" # fallback
|
||||
fi
|
||||
|
||||
# Get current branch
|
||||
current_branch=$(git branch --show-current)
|
||||
|
||||
# Find merged branches
|
||||
git branch --merged "$main_branch" | grep -v "^\*" | grep -v "$main_branch"
|
||||
|
||||
# Find branches with deleted remotes ("gone")
|
||||
git branch -vv | grep ': gone]' | awk '{print $1}'
|
||||
|
||||
# Find stale branches (older than 3 months)
|
||||
git for-each-ref --sort=-committerdate --format='%(refname:short)|%(committerdate:iso)|%(upstream:track)' refs/heads/
|
||||
|
||||
# Delete local branch (merged)
|
||||
git branch -d <branch-name>
|
||||
|
||||
# Delete local branch (force)
|
||||
git branch -D <branch-name>
|
||||
|
||||
# Delete remote branch
|
||||
git push origin --delete <branch-name>
|
||||
|
||||
# Prune remote references
|
||||
git remote prune origin
|
||||
```
|
||||
|
||||
## Return Value
|
||||
- **Claude agent text**: Analysis and results including:
|
||||
- List of branches categorized by status (merged, gone, stale)
|
||||
- Recommendation for which branches are safe to delete
|
||||
- Confirmation prompt for user approval
|
||||
- Summary of deleted branches and any errors
|
||||
- Statistics (e.g., "Deleted 5 branches, freed X MB")
|
||||
|
||||
## Examples
|
||||
|
||||
1. **Basic usage (interactive)**:
|
||||
```
|
||||
/git:branch-cleanup
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Analyzing branches in repository...
|
||||
|
||||
Main branch: main
|
||||
Current branch: feature/new-api
|
||||
|
||||
=== Merged Branches (safe to delete) ===
|
||||
feature/bug-fix-123 Merged 2 weeks ago
|
||||
feature/update-deps Merged 1 month ago
|
||||
|
||||
=== Gone Branches (remote deleted) ===
|
||||
feature/old-feature Remote: gone
|
||||
hotfix/urgent-fix Remote: gone
|
||||
|
||||
=== Stale Branches (no activity > 3 months) ===
|
||||
experiment/prototype Last commit: 4 months ago, not merged
|
||||
|
||||
=== Protected Branches (will not delete) ===
|
||||
main
|
||||
develop
|
||||
|
||||
Recommendations:
|
||||
- Safe to delete: feature/bug-fix-123, feature/update-deps (merged)
|
||||
- Safe to delete: feature/old-feature, hotfix/urgent-fix (remote gone)
|
||||
- Review needed: experiment/prototype (unmerged, stale)
|
||||
|
||||
What would you like to delete?
|
||||
```
|
||||
|
||||
2. **Dry run (preview only)**:
|
||||
```
|
||||
/git:branch-cleanup --dry-run
|
||||
```
|
||||
Output:
|
||||
```
|
||||
[DRY RUN MODE - No changes will be made]
|
||||
|
||||
Would delete the following merged branches:
|
||||
- feature/bug-fix-123
|
||||
- feature/update-deps
|
||||
|
||||
Would delete the following gone branches:
|
||||
- feature/old-feature
|
||||
- hotfix/urgent-fix
|
||||
|
||||
Total: 4 branches would be deleted
|
||||
```
|
||||
|
||||
3. **Merged branches only**:
|
||||
```
|
||||
/git:branch-cleanup --merged-only
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Analyzing merged branches...
|
||||
|
||||
Found 3 merged branches:
|
||||
- feature/bug-fix-123
|
||||
- feature/update-deps
|
||||
- feature/ui-improvements
|
||||
|
||||
Delete these branches? (y/n)
|
||||
```
|
||||
|
||||
4. **Including remote cleanup**:
|
||||
```
|
||||
/git:branch-cleanup --remote
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Deleting local and remote branches...
|
||||
|
||||
✓ Deleted local: feature/bug-fix-123
|
||||
✓ Deleted remote: origin/feature/bug-fix-123
|
||||
✓ Deleted local: feature/update-deps
|
||||
✓ Deleted remote: origin/feature/update-deps
|
||||
|
||||
Summary: Deleted 2 branches locally and remotely
|
||||
```
|
||||
|
||||
## Arguments
|
||||
- `--dry-run`: Preview which branches would be deleted without actually deleting them
|
||||
- `--merged-only`: Only consider branches that have been fully merged into the main branch
|
||||
- `--remote`: Also delete branches from the remote repository (requires push permissions)
|
||||
- `--force`: Force delete branches even if they have unmerged commits (use with caution)
|
||||
- `--older-than=<days>`: Only consider branches with no commits in the last N days (default: 90)
|
||||
|
||||
## Safety Considerations
|
||||
- **Never delete**: Current branch, main, master, develop, or release/* branches
|
||||
- **Require confirmation**: Always ask user before deleting branches
|
||||
- **Preserve unmerged work**: By default, only delete merged branches unless `--force` is used
|
||||
- **Backup suggestion**: Recommend creating a backup of unmerged branches before deletion
|
||||
- **Remote deletion**: Only delete remote branches if user explicitly requests with `--remote` flag
|
||||
50
commands/cherry-pick-by-patch.md
Normal file
50
commands/cherry-pick-by-patch.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
argument-hint: <commit_hash>
|
||||
description: Cherry-pick git commit into current branch by "patch" command
|
||||
---
|
||||
|
||||
## Name
|
||||
git:cherry-pick-by-patch
|
||||
|
||||
## Synopsis
|
||||
```
|
||||
/git:cherry-pick-by-patch commit_hash
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
The `/git-cherry-pick-by-patch commit_hash` command cherry-picks commit with hash
|
||||
`commit_hash` into current branch. Rather then doing `git cherry-pick commit_hash`,
|
||||
the command streams the output of `git show commit_hash` to
|
||||
`patch -p1 --no-backup-if-mismatch`, and then commit changes with commit message
|
||||
from `commit_hash` commit.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Pre-requisites
|
||||
|
||||
The commit with hash `commit_hash` must exist. To verify that use:
|
||||
```bash
|
||||
git show commit_hash
|
||||
```
|
||||
and check if exit code is zero.
|
||||
|
||||
Fail, if there is no `commit_hash` in the current repository checkout.
|
||||
|
||||
### Cherry-pick `commit_hash` into current branch
|
||||
|
||||
1. Execute command
|
||||
```bash
|
||||
git show commit_hash | patch -p1 --no-backup-if-mismatch
|
||||
```
|
||||
and check if exit code is zero. Fail if exit code is not zero.
|
||||
|
||||
2. Find files removed from local checkout by the patch command and execute `git rm` for them.
|
||||
|
||||
3. Find files added or modified by the patch command and execute `git add` for them.
|
||||
|
||||
4. Commit changes by `git commit` command and use commit title and description from `commit_hash` commit.
|
||||
|
||||
## Arguments
|
||||
|
||||
- **$1** (required): Commit hash (e.g., `902409c0`) of commit to cherry-pick.
|
||||
140
commands/commit-suggest.md
Normal file
140
commands/commit-suggest.md
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
description: Generate Conventional Commits style commit messages or summarize existing commits
|
||||
argument-hint: [N]
|
||||
---
|
||||
|
||||
## Name
|
||||
git:commit-suggest
|
||||
|
||||
## Synopsis
|
||||
```
|
||||
/git:commit-suggest # Analyze staged changes
|
||||
/git:commit-suggest [N] # Analyze last N commits (1-100)
|
||||
```
|
||||
|
||||
## Description
|
||||
AI-powered command that analyzes code changes and generates Conventional Commits–style messages.
|
||||
|
||||
**Modes:**
|
||||
- **Mode 1 (no argument)** – Analyze staged changes (`git add` required)
|
||||
- **Mode 2 (with N)** – Analyze last N commits to rewrite (N=1) or summarize for squash (N≥2)
|
||||
|
||||
**Use cases:**
|
||||
- Create standardized commit messages
|
||||
- Improve or rewrite existing commits
|
||||
- Generate squash messages for PR merges
|
||||
|
||||
**Difference from `/git:summary`** – That command is read-only, while `git:commit-suggest` generates actionable commit message suggestions for user review and manual use.
|
||||
|
||||
## Implementation
|
||||
|
||||
The command operates in two modes based on input:
|
||||
|
||||
**Mode 1 (no argument):**
|
||||
1. Collect staged changes via `git diff --cached`
|
||||
2. Analyze file paths and code content to identify type (feat/fix/etc.) and scope
|
||||
3. Generate 3 commit message suggestions (Recommended, Standard, Minimal)
|
||||
4. Display formatted suggestions and prompt user for selection
|
||||
- Ask: "Which suggestion would you like to use? (1/2/3 or skip)"
|
||||
- Support responses: `1`, `use option 2`, `commit with option 3`, `skip`
|
||||
- Execute `git commit` with selected message if user requests
|
||||
|
||||
**Mode 2 (with N):**
|
||||
1. Retrieve last N commits using `git log`
|
||||
2. Parse commit messages to extract types, scopes, and descriptions
|
||||
3. For **N=1**: Suggest improved rewrite
|
||||
For **N≥2**: Merge commits intelligently by type priority (`fix > feat > perf > refactor > docs > test > chore`)
|
||||
4. Generate 3 commit message suggestions (Recommended, Standard, Minimal)
|
||||
5. Display formatted suggestions and prompt user for selection
|
||||
- Ask: "Which suggestion would you like to use? (1/2/3 or skip)"
|
||||
- Support responses: `1`, `use option 2`, `amend with option 3`, `skip`
|
||||
- Execute `git commit --amend` (N=1) or squash operation (N≥2) if user requests
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Generate message for staged files
|
||||
git add src/auth.ts src/middleware.ts
|
||||
/git:commit-suggest
|
||||
|
||||
# Rewrite last commit message
|
||||
/git:commit-suggest 1
|
||||
|
||||
# Summarize last 5 commits for squash
|
||||
/git:commit-suggest 5
|
||||
```
|
||||
|
||||
## Return Value
|
||||
|
||||
Generates 3 commit message suggestions:
|
||||
- **Suggestion #1 (Recommended)** – Detailed with full body and metadata
|
||||
- **Suggestion #2 (Standard)** – Concise with main points
|
||||
- **Suggestion #3 (Minimal)** – Title and short summary
|
||||
|
||||
Each suggestion includes:
|
||||
- Conventional Commits message (`type(scope): description`)
|
||||
- Blank line between title and body
|
||||
- Optional body text explaining the changes
|
||||
- Optional footer (issue refs, co-authors, breaking changes, etc.)
|
||||
|
||||
**Example:**
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Suggestion #1 (Recommended)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
feat(auth): add JWT authentication middleware
|
||||
|
||||
Implement token-based authentication for API endpoints.
|
||||
The middleware verifies JWT tokens and extracts user information.
|
||||
|
||||
Fixes: #123
|
||||
|
||||
Which suggestion would you like to use? (1/2/3 or skip)
|
||||
```
|
||||
|
||||
### Mode 2 Specifics
|
||||
|
||||
- **N=1** – Suggest improved rewrite for the last commit
|
||||
- **N≥2** – Generate unified squash message with footer: `Squashed from N commits:` + original commit list
|
||||
|
||||
## Conventional Commits Reference
|
||||
|
||||
### Format
|
||||
```
|
||||
type(scope): description
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
### Common Types
|
||||
- `feat` – New feature
|
||||
- `fix` – Bug fix
|
||||
- `docs` – Documentation changes
|
||||
- `refactor` – Code refactoring
|
||||
- `perf` – Performance improvements
|
||||
- `test` – Test additions or modifications
|
||||
- `build` – Build system or dependency changes
|
||||
- `ci` – CI configuration changes
|
||||
- `chore` – Other changes that don't modify src or test files
|
||||
|
||||
### Scope & Footer Examples
|
||||
|
||||
**Scope**: `auth`, `api`, `ui`, `db`, `deps` (indicates affected module)
|
||||
|
||||
**Footer**:
|
||||
- Issue refs: `Fixes: #123`, `Closes: #456`, `Related: #789`
|
||||
- Breaking changes: `BREAKING CHANGE: description`
|
||||
- Co-authors: `Co-authored-by: Name <email@example.com>`
|
||||
|
||||
## Arguments
|
||||
|
||||
- **[N]** (optional): Number of recent commits to analyze (1-100)
|
||||
- If omitted: Analyzes staged changes (Mode 1)
|
||||
- If N=1: Suggests improved rewrite for the last commit
|
||||
- If N≥2: Generates unified squash message for last N commits
|
||||
|
||||
## See Also
|
||||
- **`/git:summary`** – Display repository status and recent commits (read-only)
|
||||
- [Conventional Commits Specification](https://www.conventionalcommits.org/)
|
||||
206
commands/debt-scan.md
Normal file
206
commands/debt-scan.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
description: Analyze technical debt indicators in the repository
|
||||
argument-hint:
|
||||
---
|
||||
|
||||
## Name
|
||||
git:debt-scan
|
||||
|
||||
## Synopsis
|
||||
```
|
||||
/git:debt-scan
|
||||
```
|
||||
|
||||
## Description
|
||||
The `git:debt-scan` command provides a comprehensive analysis of technical debt indicators in the current Git repository. It scans for common code health signals including TODO/FIXME comments, stale branches, large files, uncommitted changes, and recent development activity patterns. This command is designed to give developers quick insights into areas that may need attention without making any modifications to the repository.
|
||||
|
||||
It provides essential information for developers including:
|
||||
- Count and locations of TODO, FIXME, HACK, and XXX comments
|
||||
- Stale branches tracking openshift org remotes that may need cleanup (excludes main, master, develop, release-*, gh-pages, local-only branches, and personal forks)
|
||||
- Large git-tracked files that might benefit from refactoring
|
||||
- Uncommitted or unstaged changes
|
||||
- Recent commit activity trends
|
||||
|
||||
The spec sections is inspired by https://man7.org/linux/man-pages/man7/man-pages.7.html#top_of_page
|
||||
|
||||
## Implementation
|
||||
- Executes multiple analysis commands to gather technical debt indicators
|
||||
- Searches codebase for technical debt comments (TODO, FIXME, HACK, XXX)
|
||||
- Identifies stale branches tracking openshift org GitHub remotes (excludes main, master, develop, release-*, gh-pages, local-only branches, and personal forks)
|
||||
- Verifies branches still exist on upstream remote to avoid false positives
|
||||
- Finds large git-tracked files that may need refactoring
|
||||
- Shows uncommitted changes
|
||||
- Analyzes recent commit patterns
|
||||
- Formats output for clear readability
|
||||
- All operations are read-only with no side effects
|
||||
|
||||
Implementation logic:
|
||||
```bash
|
||||
# Search for technical debt comments
|
||||
echo "=== Technical Debt Comments ==="
|
||||
FILE_PATTERNS="*.{js,ts,go,py,java,rb,c,cpp,h,hpp,cs,php,swift,kt}" && for marker in TODO FIXME HACK XXX; do echo "$marker comments: $(grep -r "$marker" --include="$FILE_PATTERNS" . 2>/dev/null | grep -v '.git/' | wc -l | xargs)"; done
|
||||
|
||||
# Show top 10 files with most debt comments
|
||||
echo -e "\n=== Files with Most Debt Comments ==="
|
||||
grep -r 'TODO\|FIXME\|HACK\|XXX' --include="*.{js,ts,go,py,java,rb,c,cpp,h,hpp,cs,php,swift,kt}" . 2>/dev/null | grep -v '.git/' | cut -d: -f1 | sort | uniq -c | sort -rn | head -10
|
||||
|
||||
# Check for stale branches on openshift remotes (excluding main, master, release branches)
|
||||
echo -e "\n=== Stale Branches (not updated in 30+ days) ===" && bash -c 'OPENSHIFT_REMOTES=$(git remote -v | grep -E "github\.com[:/]openshift/" | grep fetch | awk "{print \$1}" | sort -u); if [ -z "$OPENSHIFT_REMOTES" ]; then echo "(No openshift GitHub remotes found)"; else for remote in $OPENSHIFT_REMOTES; do git ls-remote --heads "$remote" | awk "{print \$2}" | sed "s|refs/heads/||" | grep -vE "^(main|master|develop|release-.*|gh-pages)$" | while read branch; do commit_info=$(git log -1 --format="%cr|%an" "$remote/$branch" 2>/dev/null || echo ""); if [ -n "$commit_info" ]; then date=$(echo "$commit_info" | cut -d"|" -f1); author=$(echo "$commit_info" | cut -d"|" -f2); echo "$date" | grep -qE "years? ago|months? ago|[4-9] weeks ago|[0-9]{2,} weeks ago" && echo "$branch - Last commit: $date by $author"; fi; done | head -10; done; fi'
|
||||
|
||||
# Find large files (only git-tracked files)
|
||||
echo -e "\n=== Large Files (>1MB, tracked by git) ==="
|
||||
git ls-files | xargs ls -lh 2>/dev/null | awk '$5 ~ /M$/ && $5+0 > 1 {print $9 " - " $5}' | head -10
|
||||
|
||||
# Check uncommitted changes
|
||||
echo -e "\n=== Uncommitted Changes ==="
|
||||
git status --porcelain | head -20
|
||||
|
||||
# Recent commit activity
|
||||
echo -e "\n=== Commit Activity ===" && echo "Commits in last week: $(git log --since='1 week ago' --oneline 2>/dev/null | wc -l | xargs)" && echo "Commits in last month: $(git log --since='1 month ago' --oneline 2>/dev/null | wc -l | xargs)" && bash -c 'count=$(git log --since="30 days ago" --oneline 2>/dev/null | wc -l | xargs); if command -v bc >/dev/null 2>&1; then avg=$(echo "scale=1; $count / 30" | bc 2>/dev/null); echo "Average commits per day (last 30 days): ${avg:-0}"; else echo "Average commits per day (last 30 days): ~$((count / 30))"; fi'
|
||||
```
|
||||
|
||||
## Return Value
|
||||
- **Claude agent text**: Formatted analysis including:
|
||||
- Count of technical debt comments by type (TODO, FIXME, HACK, XXX)
|
||||
- List of files with the most debt comments
|
||||
- List of stale branches (30+ days old)
|
||||
- List of large files (>1MB) tracked by git that may need refactoring
|
||||
- Summary of uncommitted/unstaged changes
|
||||
- Recent commit activity statistics
|
||||
|
||||
## Examples
|
||||
|
||||
1. **Clean repository with minimal debt**:
|
||||
```
|
||||
/git:debt-scan
|
||||
```
|
||||
Output:
|
||||
```
|
||||
=== Technical Debt Comments ===
|
||||
TODO comments: 5
|
||||
FIXME comments: 1
|
||||
HACK comments: 0
|
||||
XXX comments: 0
|
||||
|
||||
=== Files with Most Debt Comments ===
|
||||
3 ./src/api/users.ts
|
||||
2 ./src/utils/helpers.js
|
||||
1 ./tests/integration.test.ts
|
||||
|
||||
=== Stale Branches (not updated in 30+ days) ===
|
||||
feature/old-experiment - Last commit: 3 months ago by Alice
|
||||
bugfix/minor-issue - Last commit: 2 months ago by Bob
|
||||
|
||||
=== Large Files (>1MB, tracked by git) ===
|
||||
data/seed.json - 2.1M
|
||||
|
||||
=== Uncommitted Changes ===
|
||||
|
||||
|
||||
=== Commit Activity ===
|
||||
Commits in last week: 12
|
||||
Commits in last month: 48
|
||||
Average commits per day (last 30 days): 1.6
|
||||
```
|
||||
|
||||
2. **Repository with technical debt to address**:
|
||||
```
|
||||
/git:debt-scan
|
||||
```
|
||||
Output:
|
||||
```
|
||||
=== Technical Debt Comments ===
|
||||
TODO comments: 47
|
||||
FIXME comments: 23
|
||||
HACK comments: 8
|
||||
XXX comments: 5
|
||||
|
||||
=== Files with Most Debt Comments ===
|
||||
12 ./src/legacy/payment-processor.js
|
||||
9 ./src/controllers/auth.ts
|
||||
7 ./src/services/notifications.py
|
||||
5 ./src/utils/data-transformer.go
|
||||
4 ./tests/e2e/checkout.test.js
|
||||
|
||||
=== Stale Branches (not updated in 30+ days) ===
|
||||
feature/refactor-database - Last commit: 5 months ago by Carol
|
||||
feature/new-ui - Last commit: 4 months ago by Dave
|
||||
bugfix/memory-leak - Last commit: 6 weeks ago by Eve
|
||||
|
||||
=== Large Files (>1MB, tracked by git) ===
|
||||
src/legacy/monolith.js - 5.3M
|
||||
dist/bundle.min.js - 3.2M
|
||||
data/migrations.sql - 2.8M
|
||||
|
||||
=== Uncommitted Changes ===
|
||||
M src/api/routes.ts
|
||||
?? temp/debug-logs.txt
|
||||
?? scripts/experimental.sh
|
||||
|
||||
=== Commit Activity ===
|
||||
Commits in last week: 3
|
||||
Commits in last month: 15
|
||||
Average commits per day (last 30 days): 0.5
|
||||
```
|
||||
|
||||
3. **Active development repository**:
|
||||
```
|
||||
/git:debt-scan
|
||||
```
|
||||
Output:
|
||||
```
|
||||
=== Technical Debt Comments ===
|
||||
TODO comments: 18
|
||||
FIXME comments: 4
|
||||
HACK comments: 2
|
||||
XXX comments: 0
|
||||
|
||||
=== Files with Most Debt Comments ===
|
||||
6 ./src/features/new-feature.ts
|
||||
4 ./src/api/v2/endpoints.go
|
||||
3 ./tests/unit/service.test.js
|
||||
|
||||
=== Stale Branches (not updated in 30+ days) ===
|
||||
(no stale branches found)
|
||||
|
||||
=== Large Files (>1MB, tracked by git) ===
|
||||
docs/api-reference.pdf - 1.2M
|
||||
|
||||
=== Uncommitted Changes ===
|
||||
M src/features/new-feature.ts
|
||||
M tests/unit/service.test.js
|
||||
|
||||
=== Commit Activity ===
|
||||
Commits in last week: 28
|
||||
Commits in last month: 89
|
||||
Average commits per day (last 30 days): 3.0
|
||||
```
|
||||
|
||||
## Interpretation Guide
|
||||
|
||||
**Technical Debt Comments:**
|
||||
- 0-10: Excellent - minimal documented debt
|
||||
- 11-30: Good - manageable debt levels
|
||||
- 31-100: Moderate - consider dedicating time to address
|
||||
- 100+: High - prioritize technical debt reduction
|
||||
|
||||
**Stale Branches:**
|
||||
- Branches inactive for 30+ days may be abandoned
|
||||
- Consider cleaning up or merging forgotten branches
|
||||
- Review with team before deletion
|
||||
|
||||
**Large Files:**
|
||||
- Git-tracked files over 1MB may benefit from:
|
||||
- Code splitting or modularization
|
||||
- Moving data to separate files or Git LFS
|
||||
- Compression or optimization
|
||||
- Being added to .gitignore if they're generated/built files
|
||||
|
||||
**Commit Activity:**
|
||||
- Low activity may indicate stagnant project
|
||||
- Very high activity may indicate need for better planning
|
||||
- Consistent activity suggests healthy development pace
|
||||
|
||||
## Arguments:
|
||||
- None
|
||||
|
||||
356
commands/suggest-reviewers.md
Normal file
356
commands/suggest-reviewers.md
Normal file
@@ -0,0 +1,356 @@
|
||||
---
|
||||
description: Suggest appropriate reviewers for a PR based on git blame and OWNERS files
|
||||
argument-hint: [base-branch]
|
||||
---
|
||||
|
||||
## Name
|
||||
git:suggest-reviewers
|
||||
|
||||
## Synopsis
|
||||
```
|
||||
/git:suggest-reviewers [base-branch]
|
||||
```
|
||||
|
||||
## Description
|
||||
The `git:suggest-reviewers` command analyzes changed files and suggests the most appropriate reviewers for a pull request. It works with both committed changes on feature branches and uncommitted changes (even on the main branch), making it useful before you've created a branch or made any commits. It prioritizes developers who have actually contributed to the code being modified, using git blame data as the primary signal and OWNERS files as supporting information.
|
||||
|
||||
The command performs the following analysis:
|
||||
- Identifies all files changed (both committed and uncommitted changes)
|
||||
- Runs git blame on changed lines to find recent and frequent contributors
|
||||
- Searches for OWNERS files in the directories of changed files (and parent directories)
|
||||
- Aggregates and ranks potential reviewers based on:
|
||||
- Frequency and recency of contributions to modified code (highest priority)
|
||||
- Presence in OWNERS files (secondary consideration)
|
||||
- Outputs a prioritized list of suggested reviewers
|
||||
|
||||
This command is particularly useful for large codebases with distributed ownership where choosing the right reviewer can be challenging. You can use it at any stage of development - from uncommitted local changes to a complete feature branch ready for PR.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Step 1: Determine the base branch
|
||||
- If `base-branch` argument is provided, use it
|
||||
- Otherwise, detect the main branch (usually `main` or `master`)
|
||||
- Verify the base branch exists
|
||||
|
||||
```bash
|
||||
# Detect main branch if not provided
|
||||
git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'
|
||||
```
|
||||
|
||||
### Step 2: Get changed files
|
||||
- Determine the current branch name
|
||||
- Detect if we're on the base branch (main/master) or a feature branch
|
||||
- Detect if there are uncommitted changes (staged or unstaged)
|
||||
- List all modified, added, or renamed files based on the scenario
|
||||
- Exclude deleted files (no one to blame)
|
||||
|
||||
**Scenario detection:**
|
||||
```bash
|
||||
# Get current branch
|
||||
current_branch=$(git branch --show-current)
|
||||
|
||||
# Check if on base branch
|
||||
if [ "$current_branch" = "$base_branch" ]; then
|
||||
on_base_branch=true
|
||||
else
|
||||
on_base_branch=false
|
||||
fi
|
||||
|
||||
# Check for uncommitted changes
|
||||
has_uncommitted=$(git status --short | grep -v '^??' | wc -l)
|
||||
```
|
||||
|
||||
**Case 1: On base branch (main/master) with uncommitted changes**
|
||||
```bash
|
||||
# Get staged changes
|
||||
git diff --name-only --diff-filter=d --cached
|
||||
|
||||
# Get unstaged changes
|
||||
git diff --name-only --diff-filter=d
|
||||
|
||||
# Combine and deduplicate
|
||||
```
|
||||
|
||||
**Case 2: On feature branch with only committed changes**
|
||||
```bash
|
||||
# Get all changes from base branch to HEAD
|
||||
git diff --name-only --diff-filter=d ${base_branch}...HEAD
|
||||
```
|
||||
|
||||
**Case 3: On feature branch with committed + uncommitted changes**
|
||||
```bash
|
||||
# Get committed changes
|
||||
git diff --name-only --diff-filter=d ${base_branch}...HEAD
|
||||
|
||||
# Get uncommitted changes
|
||||
git diff --name-only --diff-filter=d HEAD
|
||||
git diff --name-only --diff-filter=d --cached
|
||||
|
||||
# Combine and deduplicate all files
|
||||
```
|
||||
|
||||
**Case 4: On base branch with no changes**
|
||||
- Display error: "No changes detected. Please make some changes or switch to a feature branch."
|
||||
|
||||
### Step 3: Analyze git blame for changed lines
|
||||
|
||||
**IMPORTANT: Use the helper script** `${CLAUDE_PLUGIN_ROOT}/skills/suggest-reviewers/analyze_blame.py` to perform this analysis. Do NOT implement this logic manually.
|
||||
|
||||
The script automatically handles:
|
||||
- Parsing git diff to identify specific line ranges that were modified
|
||||
- Running git blame on those line ranges (not entire files)
|
||||
- Extracting and aggregating author information
|
||||
- Filtering out bot accounts
|
||||
|
||||
**For uncommitted changes:**
|
||||
```bash
|
||||
python3 ${CLAUDE_PLUGIN_ROOT}/skills/suggest-reviewers/analyze_blame.py \
|
||||
--mode uncommitted \
|
||||
--file <file1> \
|
||||
--file <file2> \
|
||||
--output json
|
||||
```
|
||||
|
||||
**For committed changes on feature branch:**
|
||||
```bash
|
||||
python3 ${CLAUDE_PLUGIN_ROOT}/skills/suggest-reviewers/analyze_blame.py \
|
||||
--mode committed \
|
||||
--base-branch ${base_branch} \
|
||||
--file <file1> \
|
||||
--file <file2> \
|
||||
--output json
|
||||
```
|
||||
|
||||
**Output format:**
|
||||
```json
|
||||
{
|
||||
"Author Name": {
|
||||
"line_count": 45,
|
||||
"most_recent_date": "2024-10-15T14:23:10",
|
||||
"files": ["file1.go", "file2.go"],
|
||||
"email": "author@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Find OWNERS files
|
||||
- For each changed file, search for OWNERS files in:
|
||||
- The same directory
|
||||
- Parent directories up to repository root
|
||||
- Parse OWNERS files to extract:
|
||||
- `approvers`: People who can approve changes
|
||||
- `reviewers`: People who can review changes
|
||||
- OWNERS file format (YAML):
|
||||
```yaml
|
||||
approvers:
|
||||
- username1
|
||||
- username2
|
||||
reviewers:
|
||||
- username3
|
||||
- username4
|
||||
```
|
||||
|
||||
### Step 5: Aggregate and rank reviewers
|
||||
- Combine data from git blame and OWNERS files
|
||||
- Rank potential reviewers based on weighted scoring:
|
||||
1. **Lines contributed** (weight: 10) - More lines modified = better knowledge
|
||||
2. **Recency** (weight: 5) - Recent contributions = current knowledge
|
||||
3. **OWNERS approver + contributor** (weight: 3 bonus) - Authority + knowledge
|
||||
4. **OWNERS reviewer + contributor** (weight: 2 bonus) - Review rights + knowledge
|
||||
5. **OWNERS only, no contributions** (weight: 1) - Authority but may lack specific knowledge
|
||||
- Exclude the current PR author from suggestions
|
||||
- Filter out bot accounts (e.g., "openshift-bot", "k8s-ci-robot", "*[bot]")
|
||||
- Normalize scores and sort by total score
|
||||
|
||||
### Step 6: Output results
|
||||
- Display reviewers in ranked order
|
||||
- Show why each reviewer is suggested (contribution count, recency, OWNERS role)
|
||||
- Group by priority tiers based on score ranges
|
||||
- Include GitHub usernames if available
|
||||
- Show files each reviewer has worked on
|
||||
|
||||
## Return Value
|
||||
- **Claude agent text**: Formatted list of suggested reviewers including:
|
||||
- **Primary reviewers**: Major contributors to the modified code
|
||||
- **Secondary reviewers**: Moderate contributors or OWNERS with some contributions
|
||||
- **Additional reviewers**: OWNERS members or minor contributors
|
||||
- Explanation for each suggestion (e.g., "Modified 45 lines across 3 files, last contribution 5 days ago, OWNERS approver")
|
||||
- Summary of analysis (files analyzed, OWNERS files found, total lines changed)
|
||||
|
||||
## Examples
|
||||
|
||||
1. **Basic usage** (auto-detect base branch):
|
||||
```
|
||||
/git:suggest-reviewers
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Analyzed 8 files changed from main (245 lines modified)
|
||||
Found 3 OWNERS files
|
||||
|
||||
PRIMARY REVIEWERS:
|
||||
- @alice (modified 89 lines across 4 files, last contribution 5 days ago, OWNERS approver)
|
||||
- @bob (modified 67 lines in pkg/controller/manager.go, last contribution 2 weeks ago)
|
||||
|
||||
SECONDARY REVIEWERS:
|
||||
- @charlie (modified 45 lines across 2 files, last contribution 1 month ago, OWNERS reviewer)
|
||||
- @diana (modified 23 lines in pkg/api/handler.go, last contribution 3 weeks ago)
|
||||
|
||||
ADDITIONAL REVIEWERS:
|
||||
- @eve (OWNERS approver in pkg/util/, no recent contributions to changed code)
|
||||
|
||||
Recommendation: Add @alice and @bob as reviewers
|
||||
```
|
||||
|
||||
2. **Specify base branch**:
|
||||
```
|
||||
/git:suggest-reviewers release-4.15
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Analyzed 3 files changed from release-4.15 (78 lines modified)
|
||||
Found 2 OWNERS files
|
||||
|
||||
PRIMARY REVIEWERS:
|
||||
- @frank (modified 56 lines in vendor/kubernetes/client.go, last contribution 1 week ago, OWNERS approver)
|
||||
|
||||
SECONDARY REVIEWERS:
|
||||
- @grace (modified 12 lines in vendor/kubernetes/types.go, last contribution 2 months ago)
|
||||
- @henry (OWNERS reviewer in vendor/kubernetes/, contributed to adjacent code 1 month ago)
|
||||
|
||||
Recommendation: Add @frank as primary reviewer, @grace as optional
|
||||
```
|
||||
|
||||
3. **No OWNERS files found**:
|
||||
```
|
||||
/git:suggest-reviewers
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Analyzed 5 files changed from main (156 lines modified)
|
||||
No OWNERS files found in modified paths
|
||||
|
||||
SUGGESTED REVIEWERS (based on code contributions):
|
||||
- @isabel (modified 89 lines across 4 files, last contribution 5 days ago)
|
||||
- @jack (modified 34 lines in src/main.ts, last contribution 10 days ago)
|
||||
- @karen (modified 12 lines in src/utils.ts, last contribution 3 months ago)
|
||||
|
||||
Note: No OWNERS files found. Consider consulting team leads for additional reviewers.
|
||||
```
|
||||
|
||||
4. **Single file change**:
|
||||
```
|
||||
/git:suggest-reviewers
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Analyzed 1 file changed from main: src/auth/login.ts (34 lines modified)
|
||||
Found 1 OWNERS file
|
||||
|
||||
PRIMARY REVIEWERS:
|
||||
- @lisa (modified 28 lines, last contribution 3 weeks ago, OWNERS approver)
|
||||
|
||||
SECONDARY REVIEWERS:
|
||||
- @mike (modified 6 lines, last contribution 2 months ago, OWNERS reviewer)
|
||||
|
||||
Recommendation: Add @lisa as reviewer
|
||||
```
|
||||
|
||||
5. **OWNERS members with no contributions**:
|
||||
```
|
||||
/git:suggest-reviewers
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Analyzed 4 files changed from main (112 lines modified)
|
||||
Found 2 OWNERS files
|
||||
|
||||
PRIMARY REVIEWERS:
|
||||
- @noah (modified 78 lines across 3 files, last contribution 1 week ago)
|
||||
|
||||
SECONDARY REVIEWERS:
|
||||
- @olivia (modified 34 lines in pkg/config/parser.go, last contribution 5 weeks ago)
|
||||
|
||||
ADDITIONAL REVIEWERS:
|
||||
- @paul (OWNERS approver in pkg/, no contributions to changed code)
|
||||
- @quinn (OWNERS reviewer in pkg/, no contributions to changed code)
|
||||
|
||||
Recommendation: Add @noah as primary reviewer. OWNERS members @paul and @quinn
|
||||
may provide approval but consider @noah for technical review.
|
||||
```
|
||||
|
||||
6. **Uncommitted changes on main branch**:
|
||||
```
|
||||
/git:suggest-reviewers
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Analyzing uncommitted changes on main branch
|
||||
Found 3 modified files (2 staged, 1 unstaged) - 87 lines modified
|
||||
Found 1 OWNERS file
|
||||
|
||||
PRIMARY REVIEWERS:
|
||||
- @rachel (modified 45 lines across 2 files, last contribution 2 weeks ago, OWNERS approver)
|
||||
- @steve (modified 32 lines in src/api/handler.ts, last contribution 1 month ago)
|
||||
|
||||
SECONDARY REVIEWERS:
|
||||
- @tina (modified 10 lines in src/utils/format.ts, last contribution 3 months ago)
|
||||
|
||||
Recommendation: Add @rachel and @steve as reviewers
|
||||
|
||||
Note: These are uncommitted changes. Consider creating a feature branch and committing before creating a PR.
|
||||
```
|
||||
|
||||
7. **Uncommitted changes on feature branch**:
|
||||
```
|
||||
/git:suggest-reviewers
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Analyzing branch feature/add-logging (includes uncommitted changes)
|
||||
- Committed changes from main: 4 files, 156 lines
|
||||
- Uncommitted changes: 2 files, 34 lines
|
||||
Total: 5 unique files, 190 lines modified
|
||||
Found 2 OWNERS files
|
||||
|
||||
PRIMARY REVIEWERS:
|
||||
- @uma (modified 98 lines across 3 files, last contribution 1 week ago, OWNERS reviewer)
|
||||
- @victor (modified 67 lines in pkg/logger/logger.go, last contribution 2 weeks ago)
|
||||
|
||||
SECONDARY REVIEWERS:
|
||||
- @wendy (modified 25 lines in pkg/config/settings.go, last contribution 1 month ago, OWNERS approver)
|
||||
|
||||
Recommendation: Add @uma and @victor as reviewers
|
||||
|
||||
Note: You have uncommitted changes. Consider committing them before creating a PR.
|
||||
```
|
||||
|
||||
8. **No changes detected**:
|
||||
```
|
||||
/git:suggest-reviewers
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Error: No changes detected.
|
||||
|
||||
You are on branch 'main' with no uncommitted changes.
|
||||
|
||||
To use this command:
|
||||
- Make some changes to files (staged or unstaged), or
|
||||
- Switch to a feature branch with committed changes, or
|
||||
- Create a new feature branch with: git checkout -b feature/your-feature-name
|
||||
```
|
||||
|
||||
## Arguments
|
||||
- `base-branch` (optional): The base branch to compare against (default: auto-detect main branch, usually `main` or `master`)
|
||||
|
||||
## Notes
|
||||
- The command analyzes both committed and uncommitted changes
|
||||
- Works on any branch, including main/master (analyzes uncommitted changes in this case)
|
||||
- For uncommitted changes, git blame is run on HEAD; for committed changes, on the base branch
|
||||
- OWNERS files must be in YAML format with `approvers` and/or `reviewers` fields
|
||||
- The current user (detected from git config) is automatically excluded from suggestions
|
||||
- Reviewers are ranked primarily by their contribution to the specific code being changed
|
||||
- OWNERS membership provides a bonus but is not the primary ranking factor
|
||||
- If no reviewers are found via git blame, OWNERS members will be suggested as fallback
|
||||
- If you're on the base branch with no uncommitted changes, the command will display an error
|
||||
102
commands/summary.md
Normal file
102
commands/summary.md
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
description: Show current branch, git status, and recent commits for quick context
|
||||
argument-hint:
|
||||
---
|
||||
|
||||
## Name
|
||||
git:summary
|
||||
|
||||
## Synopsis
|
||||
```
|
||||
/git:summary
|
||||
```
|
||||
|
||||
## Description
|
||||
The `git:summary` command provides a comprehensive overview of the current Git repository state. It displays the current branch, tracking status, working tree status, and recent commit history in a single concise view. This command is designed to give developers quick context about their repository without running multiple Git commands manually.
|
||||
|
||||
It provides essential information for developers including:
|
||||
- Current branch and remote tracking status (ahead/behind)
|
||||
- Working tree status (modified, staged, and untracked files)
|
||||
- Recent commit history with one-line summaries
|
||||
- Uncommitted changes summary
|
||||
|
||||
The spec sections is inspired by https://man7.org/linux/man-pages/man7/man-pages.7.html#top_of_page
|
||||
|
||||
## Implementation
|
||||
- Executes multiple git commands to gather repository state
|
||||
- Retrieves current branch name and tracking information
|
||||
- Shows git status for modified, staged, and untracked files
|
||||
- Displays last 5 commits with one-line summaries
|
||||
- Summarizes uncommitted changes
|
||||
- Formats output for clear readability
|
||||
- All information is read-only with no side effects
|
||||
|
||||
Implementation logic:
|
||||
```bash
|
||||
# Get current branch and tracking status
|
||||
git branch -vv
|
||||
|
||||
# Show working tree status
|
||||
git status --short
|
||||
|
||||
# Display recent commits
|
||||
git log --oneline -5
|
||||
|
||||
# Summarize uncommitted changes
|
||||
git diff --stat
|
||||
```
|
||||
|
||||
## Return Value
|
||||
- **Claude agent text**: Formatted summary including:
|
||||
- Current branch name and remote tracking status
|
||||
- List of modified, staged, and untracked files
|
||||
- Last 5 commit messages with hashes
|
||||
- Statistics of uncommitted changes
|
||||
|
||||
## Examples
|
||||
|
||||
1. **Basic usage**:
|
||||
```
|
||||
/git:summary
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Current branch: main
|
||||
Your branch is up to date with 'origin/main'.
|
||||
|
||||
Modified files:
|
||||
M src/index.ts
|
||||
?? temp/
|
||||
|
||||
Recent commits:
|
||||
abc123 Fix authentication bug
|
||||
def456 Add user profile feature
|
||||
ghi789 Update dependencies
|
||||
jkl012 Refactor database layer
|
||||
mno345 Initial commit
|
||||
|
||||
Uncommitted changes:
|
||||
1 file changed, 15 insertions(+), 3 deletions(-)
|
||||
```
|
||||
|
||||
2. **Repository with no changes**:
|
||||
```
|
||||
/git:summary
|
||||
```
|
||||
Output:
|
||||
```
|
||||
Current branch: develop
|
||||
Your branch is up to date with 'origin/develop'.
|
||||
|
||||
Working tree clean
|
||||
|
||||
Recent commits:
|
||||
pqr678 Merge pull request #42
|
||||
stu901 Add test coverage
|
||||
vwx234 Fix linting issues
|
||||
yza567 Update README
|
||||
bcd890 Release v2.0.0
|
||||
```
|
||||
|
||||
## Arguments:
|
||||
- None
|
||||
73
plugin.lock.json
Normal file
73
plugin.lock.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:openshift-eng/ai-helpers:plugins/git",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "b06a73433894d0d004f47ddf5c2afecbf942f14d",
|
||||
"treeHash": "0e5818818c47cadb8e43e250826144d536d998453815f8d61e3a64c3c815c2f1",
|
||||
"generatedAt": "2025-11-28T10:27:27.519601Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "git",
|
||||
"description": "Git workflow automation and utilities",
|
||||
"version": "0.0.1"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "6ceab38d7dca8b572e21f5119826df251d525ee5524cd935c390e7eac8f3c5af"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "e60d712973eab467e6454e688554a28baca773db9259ff28d207288255e37c8d"
|
||||
},
|
||||
{
|
||||
"path": "commands/commit-suggest.md",
|
||||
"sha256": "6ecad981c5f5b5a9ec7aa1c3edf913ab37860955d02bb424d7371e1be6e2606b"
|
||||
},
|
||||
{
|
||||
"path": "commands/summary.md",
|
||||
"sha256": "63174a94ccbe119ae0505eaa208cdabf58723f0ca743278a28d600c04ad0484f"
|
||||
},
|
||||
{
|
||||
"path": "commands/suggest-reviewers.md",
|
||||
"sha256": "9a069b9ed676592355f4a96a0eba7b9e9552b2b388de13e949e342dd67370ff2"
|
||||
},
|
||||
{
|
||||
"path": "commands/debt-scan.md",
|
||||
"sha256": "60faa5136ae8eb84e4be3e3c4a0947ed230ac602a2dbcb247b9d8d14fd6829be"
|
||||
},
|
||||
{
|
||||
"path": "commands/branch-cleanup.md",
|
||||
"sha256": "425b214ad3fd49156a25dd79266907a6ef51b12950d0eb81f07eed365e634e7a"
|
||||
},
|
||||
{
|
||||
"path": "commands/cherry-pick-by-patch.md",
|
||||
"sha256": "b9f8cc4a254ffaa6b8e0dc9c574673556051d26e6fc751028e0a4c1707a8e477"
|
||||
},
|
||||
{
|
||||
"path": "skills/suggest-reviewers/SKILL.md",
|
||||
"sha256": "8d102e9b9ac30801eb137c8c91694f29f6084686ce7226ada5302ab8dfd05df0"
|
||||
},
|
||||
{
|
||||
"path": "skills/suggest-reviewers/analyze_blame.py",
|
||||
"sha256": "80432aec822122ba5dcbac71a670a1ee755ebc85ef2f289be169fa82a1ca1065"
|
||||
}
|
||||
],
|
||||
"dirSha256": "0e5818818c47cadb8e43e250826144d536d998453815f8d61e3a64c3c815c2f1"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
347
skills/suggest-reviewers/SKILL.md
Normal file
347
skills/suggest-reviewers/SKILL.md
Normal file
@@ -0,0 +1,347 @@
|
||||
---
|
||||
name: Suggest Reviewers Helper
|
||||
description: Git blame analysis helper for the suggest-reviewers command
|
||||
---
|
||||
|
||||
# Suggest Reviewers Helper
|
||||
|
||||
This skill provides a Python helper script that analyzes git blame data for the `/git:suggest-reviewers` command. The script handles the complex task of identifying which lines were changed and who authored the original code.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when implementing the `/git:suggest-reviewers` command. The helper script should be invoked during Step 3 of the command implementation (analyzing git blame for changed lines).
|
||||
|
||||
**DO NOT implement git blame analysis manually** - always use the provided `analyze_blame.py` script.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.6 or higher
|
||||
- Git repository with commit history
|
||||
- Git CLI available in PATH
|
||||
|
||||
## Helper Script: analyze_blame.py
|
||||
|
||||
The `analyze_blame.py` script automates the complex process of:
|
||||
1. Parsing git diff output to identify specific line ranges that were modified
|
||||
2. Running git blame on only the changed line ranges (not entire files)
|
||||
3. Extracting and aggregating author information with statistics
|
||||
4. Filtering out bot accounts automatically
|
||||
|
||||
### Usage
|
||||
|
||||
**For uncommitted changes:**
|
||||
```bash
|
||||
python3 ${CLAUDE_PLUGIN_ROOT}/skills/suggest-reviewers/analyze_blame.py \
|
||||
--mode uncommitted \
|
||||
--file path/to/file1.go \
|
||||
--file path/to/file2.py \
|
||||
--output json
|
||||
```
|
||||
|
||||
**For committed changes on a feature branch:**
|
||||
```bash
|
||||
python3 ${CLAUDE_PLUGIN_ROOT}/skills/suggest-reviewers/analyze_blame.py \
|
||||
--mode committed \
|
||||
--base-branch main \
|
||||
--file path/to/file1.go \
|
||||
--file path/to/file2.py \
|
||||
--output json
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- `--mode`: Required. Either `uncommitted` or `committed`
|
||||
- `uncommitted`: Analyzes unstaged/staged changes against HEAD
|
||||
- `committed`: Analyzes committed changes against a base branch
|
||||
|
||||
- `--base-branch`: Required when mode is `committed`. The base branch to compare against (e.g., `main`, `master`)
|
||||
|
||||
- `--file`: Can be specified multiple times. Each file to analyze for blame information. Only changed files should be passed.
|
||||
|
||||
- `--output`: Output format. Default is `json`. Options:
|
||||
- `json`: Machine-readable JSON output
|
||||
- `text`: Human-readable text output
|
||||
|
||||
### Output Format (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"Author Name": {
|
||||
"line_count": 45,
|
||||
"most_recent_date": "2024-10-15T14:23:10",
|
||||
"files": ["file1.go", "file2.go"],
|
||||
"email": "author@example.com"
|
||||
},
|
||||
"Another Author": {
|
||||
"line_count": 23,
|
||||
"most_recent_date": "2024-09-20T09:15:33",
|
||||
"files": ["file3.py"],
|
||||
"email": "another@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Output Fields
|
||||
|
||||
- `line_count`: Total number of modified lines authored by this person
|
||||
- `most_recent_date`: ISO 8601 timestamp of their most recent contribution to the changed code
|
||||
- `files`: Array of files where this author has contributions in the changed lines
|
||||
- `email`: Author's email address from git commits
|
||||
|
||||
### Bot Filtering
|
||||
|
||||
The script automatically filters out common bot accounts:
|
||||
- GitHub bots (e.g., `dependabot[bot]`, `renovate[bot]`)
|
||||
- CI bots (e.g., `openshift-ci-robot`, `k8s-ci-robot`)
|
||||
- Generic bot patterns (any name containing `[bot]` or ending in `-bot`)
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Collect changed files
|
||||
|
||||
Before invoking the script, collect the list of changed files based on the scenario:
|
||||
|
||||
**Uncommitted changes:**
|
||||
```bash
|
||||
# Get staged and unstaged files
|
||||
files=$(git diff --name-only --diff-filter=d HEAD)
|
||||
files+=" $(git diff --name-only --diff-filter=d --cached)"
|
||||
```
|
||||
|
||||
**Committed changes:**
|
||||
```bash
|
||||
# Get files changed from base branch
|
||||
files=$(git diff --name-only --diff-filter=d ${base_branch}...HEAD)
|
||||
```
|
||||
|
||||
### Step 2: Invoke the script
|
||||
|
||||
Build the command with the appropriate mode and all changed files:
|
||||
|
||||
```bash
|
||||
# Start building the command
|
||||
cmd="python3 ${CLAUDE_PLUGIN_ROOT}/skills/suggest-reviewers/analyze_blame.py"
|
||||
|
||||
# Add mode
|
||||
if [ "$has_uncommitted" = true ] || [ "$on_base_branch" = true ]; then
|
||||
cmd="$cmd --mode uncommitted"
|
||||
else
|
||||
cmd="$cmd --mode committed --base-branch $base_branch"
|
||||
fi
|
||||
|
||||
# Add each file
|
||||
for file in $files; do
|
||||
cmd="$cmd --file $file"
|
||||
done
|
||||
|
||||
# Add output format
|
||||
cmd="$cmd --output json"
|
||||
|
||||
# Execute and capture JSON output
|
||||
blame_data=$($cmd)
|
||||
```
|
||||
|
||||
### Step 3: Parse the output
|
||||
|
||||
The JSON output can be parsed using Python, jq, or any JSON parser:
|
||||
|
||||
```bash
|
||||
# Example using jq to get top contributor
|
||||
echo "$blame_data" | jq -r 'to_entries | sort_by(-.value.line_count) | .[0].key'
|
||||
|
||||
# Example using Python
|
||||
python3 << EOF
|
||||
import json
|
||||
import sys
|
||||
|
||||
data = json.loads('''$blame_data''')
|
||||
|
||||
# Sort by line count
|
||||
sorted_authors = sorted(data.items(), key=lambda x: x[1]['line_count'], reverse=True)
|
||||
|
||||
for author, stats in sorted_authors:
|
||||
print(f"{author}: {stats['line_count']} lines, last modified {stats['most_recent_date']}")
|
||||
EOF
|
||||
```
|
||||
|
||||
### Step 4: Combine with OWNERS data
|
||||
|
||||
After getting blame data, merge it with OWNERS file information to produce the final ranked list of reviewers.
|
||||
|
||||
## Error Handling
|
||||
|
||||
### No changed files
|
||||
|
||||
If no files are passed to the script:
|
||||
```
|
||||
Error: No files specified. Use --file option at least once.
|
||||
```
|
||||
|
||||
**Resolution:** Ensure you've detected changed files correctly before invoking the script.
|
||||
|
||||
### Invalid mode
|
||||
|
||||
If an invalid mode is specified:
|
||||
```
|
||||
Error: Invalid mode 'invalid'. Must be 'uncommitted' or 'committed'.
|
||||
```
|
||||
|
||||
**Resolution:** Use either `--mode uncommitted` or `--mode committed`.
|
||||
|
||||
### Missing base branch in committed mode
|
||||
|
||||
If `--mode committed` is used without `--base-branch`:
|
||||
```
|
||||
Error: --base-branch is required when mode is 'committed'.
|
||||
```
|
||||
|
||||
**Resolution:** Provide the base branch: `--base-branch main`
|
||||
|
||||
### File not in repository
|
||||
|
||||
If a specified file is not tracked by git:
|
||||
```
|
||||
Warning: File 'path/to/file' is not tracked by git, skipping.
|
||||
```
|
||||
|
||||
**Resolution:** This is a warning and can be safely ignored. The script will skip untracked files.
|
||||
|
||||
### No blame data found
|
||||
|
||||
If git blame returns no data for any files:
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
**Resolution:** This can happen if:
|
||||
- All changed files are newly created (no blame history)
|
||||
- All changes are in binary files
|
||||
- Git blame is unable to run
|
||||
|
||||
In this case, fall back to OWNERS-only suggestions.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Analyze uncommitted changes
|
||||
|
||||
```bash
|
||||
$ python3 analyze_blame.py --mode uncommitted --file src/main.go --file src/utils.go --output json
|
||||
{
|
||||
"Alice Developer": {
|
||||
"line_count": 45,
|
||||
"most_recent_date": "2024-10-15T14:23:10",
|
||||
"files": ["src/main.go", "src/utils.go"],
|
||||
"email": "alice@example.com"
|
||||
},
|
||||
"Bob Engineer": {
|
||||
"line_count": 12,
|
||||
"most_recent_date": "2024-09-20T09:15:33",
|
||||
"files": ["src/main.go"],
|
||||
"email": "bob@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Analyze committed changes on feature branch
|
||||
|
||||
```bash
|
||||
$ python3 analyze_blame.py --mode committed --base-branch main --file pkg/controller/manager.go --output json
|
||||
{
|
||||
"Charlie Contributor": {
|
||||
"line_count": 78,
|
||||
"most_recent_date": "2024-10-01T11:42:55",
|
||||
"files": ["pkg/controller/manager.go"],
|
||||
"email": "charlie@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Text output format
|
||||
|
||||
```bash
|
||||
$ python3 analyze_blame.py --mode uncommitted --file README.md --output text
|
||||
|
||||
Blame Analysis Results:
|
||||
=======================
|
||||
|
||||
Alice Developer (alice@example.com)
|
||||
Lines: 23
|
||||
Most recent: 2024-10-15T14:23:10
|
||||
Files: README.md
|
||||
|
||||
Bob Engineer (bob@example.com)
|
||||
Lines: 5
|
||||
Most recent: 2024-08-12T16:30:21
|
||||
Files: README.md
|
||||
```
|
||||
|
||||
### Example 4: Multiple files with mixed results
|
||||
|
||||
```bash
|
||||
$ python3 analyze_blame.py --mode committed --base-branch release-4.15 \
|
||||
--file vendor/k8s.io/client-go/kubernetes/clientset.go \
|
||||
--file pkg/controller/node.go \
|
||||
--file docs/README.md \
|
||||
--output json
|
||||
{
|
||||
"Diana Developer": {
|
||||
"line_count": 156,
|
||||
"most_recent_date": "2024-09-28T13:15:42",
|
||||
"files": ["vendor/k8s.io/client-go/kubernetes/clientset.go", "pkg/controller/node.go"],
|
||||
"email": "diana@example.com"
|
||||
},
|
||||
"Eve Technical Writer": {
|
||||
"line_count": 34,
|
||||
"most_recent_date": "2024-10-10T10:22:18",
|
||||
"files": ["docs/README.md"],
|
||||
"email": "eve@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
### How the script works
|
||||
|
||||
1. **Determine diff range**: Based on mode, calculates what to compare:
|
||||
- `uncommitted`: Compares working directory against HEAD
|
||||
- `committed`: Compares HEAD against base branch
|
||||
|
||||
2. **Parse diff output**: Runs `git diff` with unified format to identify:
|
||||
- Which files changed
|
||||
- Which line ranges were added/modified
|
||||
- Ignores deleted lines (can't blame what doesn't exist)
|
||||
|
||||
3. **Run git blame**: For each file and line range:
|
||||
- Runs `git blame -L start,end --line-porcelain file`
|
||||
- Parses porcelain format to extract author, email, and timestamp
|
||||
- Aggregates data across all changed lines
|
||||
|
||||
4. **Filter and aggregate**:
|
||||
- Removes bot accounts
|
||||
- Groups by author name
|
||||
- Counts total lines per author
|
||||
- Tracks most recent contribution date
|
||||
- Lists all files each author contributed to
|
||||
|
||||
5. **Output results**: Formats as JSON or text based on `--output` parameter
|
||||
|
||||
### Performance considerations
|
||||
|
||||
- Only blames changed line ranges, not entire files (much faster for small changes to large files)
|
||||
- Processes files in parallel when possible
|
||||
- Caches git commands where appropriate
|
||||
- Skips binary files automatically
|
||||
|
||||
## Limitations
|
||||
|
||||
- Does not handle file renames/moves (treats as delete + add)
|
||||
- Bot filtering is based on common patterns; custom bots may not be filtered
|
||||
- Requires git history; newly initialized repos may not have useful data
|
||||
- Does not consider commit message content or PR review history
|
||||
|
||||
## See Also
|
||||
|
||||
- Main command: `/git:suggest-reviewers` in `plugins/git/commands/suggest-reviewers.md`
|
||||
- Git blame documentation: https://git-scm.com/docs/git-blame
|
||||
- Git diff documentation: https://git-scm.com/docs/git-diff
|
||||
380
skills/suggest-reviewers/analyze_blame.py
Normal file
380
skills/suggest-reviewers/analyze_blame.py
Normal file
@@ -0,0 +1,380 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Git Blame Analysis Helper for suggest-reviewers command.
|
||||
|
||||
This script helps identify the authors of code lines being modified in a PR,
|
||||
aggregating git blame data to suggest the most relevant reviewers.
|
||||
|
||||
Usage:
|
||||
python analyze_blame.py --mode <uncommitted|committed> --file <filepath> [--base-branch <branch>]
|
||||
|
||||
Modes:
|
||||
uncommitted: Analyze uncommitted changes (compares against HEAD)
|
||||
committed: Analyze committed changes on feature branch (compares against base branch)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
|
||||
|
||||
class BlameAnalyzer:
|
||||
"""Analyzes git blame for changed lines in files."""
|
||||
|
||||
# Bot patterns to filter out
|
||||
BOT_PATTERNS = [
|
||||
r'.*\[bot\]',
|
||||
r'openshift-bot',
|
||||
r'k8s-ci-robot',
|
||||
r'openshift-merge-robot',
|
||||
r'openshift-ci\[bot\]',
|
||||
r'dependabot',
|
||||
r'renovate\[bot\]',
|
||||
]
|
||||
|
||||
def __init__(self, mode: str, base_branch: Optional[str] = None):
|
||||
"""
|
||||
Initialize the analyzer.
|
||||
|
||||
Args:
|
||||
mode: 'uncommitted' or 'committed'
|
||||
base_branch: Base branch for committed mode (e.g., 'main')
|
||||
"""
|
||||
self.mode = mode
|
||||
self.base_branch = base_branch
|
||||
self.authors = defaultdict(lambda: {
|
||||
'line_count': 0,
|
||||
'most_recent_date': None,
|
||||
'files': set(),
|
||||
'email': None
|
||||
})
|
||||
|
||||
if mode == 'committed' and not base_branch:
|
||||
raise ValueError("base_branch required for 'committed' mode")
|
||||
|
||||
# Get current user to exclude from suggestions
|
||||
self.current_user_name = self._get_git_config('user.name')
|
||||
self.current_user_email = self._get_git_config('user.email')
|
||||
|
||||
def _get_git_config(self, key: str) -> Optional[str]:
|
||||
"""Get a git config value."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'config', '--get', key],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def is_bot(self, author: str) -> bool:
|
||||
"""Check if an author name matches bot patterns."""
|
||||
for pattern in self.BOT_PATTERNS:
|
||||
if re.match(pattern, author, re.IGNORECASE):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_current_user(self, author: str, email: Optional[str]) -> bool:
|
||||
"""Check if the author is the current user."""
|
||||
if self.current_user_name and author == self.current_user_name:
|
||||
return True
|
||||
if self.current_user_email and email and email == self.current_user_email:
|
||||
return True
|
||||
return False
|
||||
|
||||
def parse_diff_ranges(self, file_path: str) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
Parse git diff output to extract changed line ranges.
|
||||
|
||||
Returns:
|
||||
List of (start_line, line_count) tuples for changed ranges
|
||||
"""
|
||||
ranges = []
|
||||
|
||||
try:
|
||||
if self.mode == 'uncommitted':
|
||||
# Check staged changes
|
||||
diff_cmd = ['git', 'diff', '--cached', '--unified=0', file_path]
|
||||
result = subprocess.run(diff_cmd, capture_output=True, text=True, check=False)
|
||||
ranges.extend(self._extract_ranges_from_diff(result.stdout))
|
||||
|
||||
# Check unstaged changes
|
||||
diff_cmd = ['git', 'diff', 'HEAD', '--unified=0', file_path]
|
||||
result = subprocess.run(diff_cmd, capture_output=True, text=True, check=False)
|
||||
ranges.extend(self._extract_ranges_from_diff(result.stdout))
|
||||
else:
|
||||
# Committed changes: compare against base branch
|
||||
diff_cmd = ['git', 'diff', f'{self.base_branch}...HEAD', '--unified=0', file_path]
|
||||
result = subprocess.run(diff_cmd, capture_output=True, text=True, check=True)
|
||||
ranges.extend(self._extract_ranges_from_diff(result.stdout))
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error running diff for {file_path}: {e}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
# Deduplicate and merge overlapping ranges
|
||||
return self._merge_ranges(ranges)
|
||||
|
||||
def _extract_ranges_from_diff(self, diff_output: str) -> List[Tuple[int, int]]:
|
||||
"""
|
||||
Extract line ranges from diff @@ markers.
|
||||
|
||||
Diff format: @@ -old_start,old_count +new_start,new_count @@
|
||||
We want the 'old' ranges (lines being replaced/modified in the base)
|
||||
|
||||
For pure additions (count=0), we analyze context lines before the insertion
|
||||
point to find relevant code owners.
|
||||
"""
|
||||
ranges = []
|
||||
# Match @@ -start[,count] +start[,count] @@
|
||||
pattern = r'^@@\s+-(\d+)(?:,(\d+))?\s+\+\d+(?:,\d+)?\s+@@'
|
||||
|
||||
for line in diff_output.split('\n'):
|
||||
match = re.match(pattern, line)
|
||||
if match:
|
||||
start = int(match.group(1))
|
||||
count = int(match.group(2)) if match.group(2) else 1
|
||||
|
||||
if start > 0:
|
||||
if count > 0:
|
||||
# Regular modification/deletion
|
||||
ranges.append((start, count))
|
||||
else:
|
||||
# Pure addition (count=0): analyze context before insertion
|
||||
# Look at up to 5 lines before the insertion point
|
||||
context_start = max(1, start - 5)
|
||||
context_count = start - context_start
|
||||
if context_count > 0:
|
||||
ranges.append((context_start, context_count))
|
||||
|
||||
return ranges
|
||||
|
||||
def _merge_ranges(self, ranges: List[Tuple[int, int]]) -> List[Tuple[int, int]]:
|
||||
"""Merge overlapping line ranges."""
|
||||
if not ranges:
|
||||
return []
|
||||
|
||||
# Sort by start line
|
||||
sorted_ranges = sorted(ranges, key=lambda x: x[0])
|
||||
merged = [sorted_ranges[0]]
|
||||
|
||||
for start, count in sorted_ranges[1:]:
|
||||
last_start, last_count = merged[-1]
|
||||
last_end = last_start + last_count - 1
|
||||
current_end = start + count - 1
|
||||
|
||||
# Check if ranges overlap or are adjacent
|
||||
if start <= last_end + 1:
|
||||
# Merge ranges
|
||||
new_end = max(last_end, current_end)
|
||||
new_count = new_end - last_start + 1
|
||||
merged[-1] = (last_start, new_count)
|
||||
else:
|
||||
merged.append((start, count))
|
||||
|
||||
return merged
|
||||
|
||||
def analyze_file(self, file_path: str) -> None:
|
||||
"""
|
||||
Analyze git blame for a specific file.
|
||||
|
||||
Args:
|
||||
file_path: Path to file relative to repo root
|
||||
"""
|
||||
# Get changed line ranges
|
||||
ranges = self.parse_diff_ranges(file_path)
|
||||
|
||||
if not ranges:
|
||||
return
|
||||
|
||||
# Determine which revision to blame
|
||||
if self.mode == 'uncommitted':
|
||||
blame_target = 'HEAD'
|
||||
else:
|
||||
blame_target = self.base_branch
|
||||
|
||||
# Run git blame on each range
|
||||
for start, count in ranges:
|
||||
end = start + count - 1
|
||||
self._blame_range(file_path, start, end, blame_target)
|
||||
|
||||
def _blame_range(self, file_path: str, start: int, end: int, revision: str) -> None:
|
||||
"""
|
||||
Run git blame on a specific line range and extract author data.
|
||||
|
||||
Args:
|
||||
file_path: File to blame
|
||||
start: Start line number
|
||||
end: End line number
|
||||
revision: Git revision to blame (e.g., 'HEAD', 'main')
|
||||
"""
|
||||
try:
|
||||
# Use porcelain format for easier parsing
|
||||
blame_cmd = [
|
||||
'git', 'blame',
|
||||
'--porcelain',
|
||||
'-L', f'{start},{end}',
|
||||
revision,
|
||||
'--',
|
||||
file_path
|
||||
]
|
||||
|
||||
result = subprocess.run(blame_cmd, capture_output=True, text=True, check=True)
|
||||
self._parse_blame_output(result.stdout, file_path)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error running blame on {file_path}:{start}-{end}: {e}", file=sys.stderr)
|
||||
|
||||
def _parse_blame_output(self, blame_output: str, file_path: str) -> None:
|
||||
"""
|
||||
Parse git blame --porcelain output and aggregate author data.
|
||||
|
||||
Porcelain format:
|
||||
<commit-hash> <original-line> <final-line> <num-lines>
|
||||
author <author-name>
|
||||
author-mail <email>
|
||||
author-time <unix-timestamp>
|
||||
...
|
||||
\t<line-content>
|
||||
"""
|
||||
lines = blame_output.split('\n')
|
||||
i = 0
|
||||
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Check if this is a commit header line
|
||||
if line and not line.startswith('\t'):
|
||||
parts = line.split()
|
||||
if len(parts) >= 4 and len(parts[0]) == 40: # Looks like a SHA
|
||||
# Parse commit metadata
|
||||
author = None
|
||||
email = None
|
||||
timestamp = None
|
||||
|
||||
# Look ahead for author info
|
||||
j = i + 1
|
||||
while j < len(lines) and not lines[j].startswith('\t'):
|
||||
if lines[j].startswith('author '):
|
||||
author = lines[j][7:] # Remove 'author ' prefix
|
||||
elif lines[j].startswith('author-mail '):
|
||||
email = lines[j][12:].strip('<>') # Remove 'author-mail ' and <>
|
||||
elif lines[j].startswith('author-time '):
|
||||
timestamp = int(lines[j][12:])
|
||||
j += 1
|
||||
|
||||
# Update author data (exclude bots and current user)
|
||||
if author and not self.is_bot(author) and not self.is_current_user(author, email):
|
||||
author_date = datetime.fromtimestamp(timestamp) if timestamp else None
|
||||
|
||||
self.authors[author]['line_count'] += 1
|
||||
self.authors[author]['files'].add(file_path)
|
||||
self.authors[author]['email'] = email
|
||||
|
||||
# Track most recent contribution
|
||||
if author_date:
|
||||
current_recent = self.authors[author]['most_recent_date']
|
||||
if current_recent is None or author_date > current_recent:
|
||||
self.authors[author]['most_recent_date'] = author_date
|
||||
|
||||
i = j
|
||||
continue
|
||||
|
||||
i += 1
|
||||
|
||||
def get_results(self) -> Dict:
|
||||
"""
|
||||
Get aggregated results as a dictionary.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping author names to their statistics
|
||||
"""
|
||||
results = {}
|
||||
|
||||
for author, data in self.authors.items():
|
||||
results[author] = {
|
||||
'line_count': data['line_count'],
|
||||
'most_recent_date': data['most_recent_date'].isoformat() if data['most_recent_date'] else None,
|
||||
'files': sorted(list(data['files'])),
|
||||
'email': data['email']
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Analyze git blame for changed lines to identify code authors'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--mode',
|
||||
choices=['uncommitted', 'committed'],
|
||||
required=True,
|
||||
help='Analysis mode: uncommitted (vs HEAD) or committed (vs base branch)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--file',
|
||||
required=True,
|
||||
action='append',
|
||||
dest='files',
|
||||
help='File(s) to analyze (can be specified multiple times)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--base-branch',
|
||||
help='Base branch for committed mode (e.g., main, master)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
choices=['json', 'text'],
|
||||
default='json',
|
||||
help='Output format (default: json)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate arguments
|
||||
if args.mode == 'committed' and not args.base_branch:
|
||||
print("Error: --base-branch required for 'committed' mode", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Analyze files
|
||||
analyzer = BlameAnalyzer(mode=args.mode, base_branch=args.base_branch)
|
||||
|
||||
for file_path in args.files:
|
||||
analyzer.analyze_file(file_path)
|
||||
|
||||
# Output results
|
||||
results = analyzer.get_results()
|
||||
|
||||
if args.output == 'json':
|
||||
print(json.dumps(results, indent=2))
|
||||
else:
|
||||
# Text output
|
||||
print(f"\nAuthors of modified code ({len(results)} found):\n")
|
||||
|
||||
# Sort by line count
|
||||
sorted_authors = sorted(
|
||||
results.items(),
|
||||
key=lambda x: x[1]['line_count'],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
for author, data in sorted_authors:
|
||||
print(f"{author} <{data['email']}>")
|
||||
print(f" Lines: {data['line_count']}")
|
||||
print(f" Most recent: {data['most_recent_date'] or 'unknown'}")
|
||||
print(f" Files: {', '.join(data['files'])}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user