Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:45:53 +08:00
commit 74958112ad
11 changed files with 1882 additions and 0 deletions

View 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"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# git
Git workflow automation and utilities

211
commands/branch-cleanup.md Normal file
View 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

View 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
View 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 Commitsstyle 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
View 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

View 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
View 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
View 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": []
}
}

View 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

View 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()