Skip to content

Commit de4022a

Browse files
committed
chrome extension
1 parent e75bd70 commit de4022a

File tree

11 files changed

+604
-22
lines changed

11 files changed

+604
-22
lines changed

showcase/code-analysis/README.md

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
1-
# Code Analysis CLI
1+
# GitHub Repository Analyzer
22

3-
A Python CLI application for analyzing GitHub repository complexity.
3+
A Chrome extension that analyzes GitHub repositories for development patterns and metrics.
44

5-
## Setup
5+
## Features
6+
- Commit frequency analysis
7+
- Contributor activity tracking
8+
- Commit patterns by weekday
9+
- Average commit size metrics
10+
11+
## Prerequisites
12+
- Python 3.8 or higher
13+
- Google Chrome browser
14+
- Git
15+
16+
## Setup Instructions
17+
18+
### 1. Clone the Repository
19+
```bash
20+
git clone <your-repo-url>
21+
cd code-analysis
22+
```
623

24+
### 2. Set Up Python Environment
725
```bash
826
# Create and activate virtual environment
927
python -m venv .venv
@@ -16,35 +34,60 @@ pip install -e .
1634
pip install -e ".[dev]"
1735
```
1836

19-
## Usage
20-
37+
### 3. Start the Backend Server
2138
```bash
22-
# List available commands
23-
python -m cli --help
24-
25-
# Analyze commit frequency for a repository
26-
python -m cli analyze_commit_frequency /path/to/git/repo
27-
28-
# Output results in JSON format
29-
python -m cli analyze_commit_frequency /path/to/git/repo --json
39+
# Start the FastAPI server
40+
./start_server.sh
3041
```
42+
The server will run at http://localhost:8000
3143

32-
## Features
44+
### 4. Install Chrome Extension
45+
1. Open Chrome and navigate to `chrome://extensions/`
46+
2. Enable "Developer mode" in the top right
47+
3. Click "Load unpacked"
48+
4. Select the `extension` directory from this project
3349

34-
- analyze_commit_frequency: Analyze commit frequency by day
35-
- More features coming soon!
50+
### 5. Using the Extension
51+
1. Navigate to any GitHub repository
52+
2. Click the extension icon in Chrome's toolbar
53+
3. Click "Analyze Repository" to see metrics
3654

3755
## Development
3856

57+
### Running Type Checks
3958
```bash
40-
# Run type checker
4159
mypy cli app
60+
```
4261

43-
# Format code
62+
### Format Code
63+
```bash
4464
black cli app
45-
46-
# Run FastAPI server
47-
uvicorn app.main:app --reload
4865
```
4966

50-
The FastAPI app will be available at http://localhost:8000 with automatic API documentation at http://localhost:8000/docs.
67+
### API Documentation
68+
When the server is running, visit:
69+
- http://localhost:8000/docs for interactive API documentation
70+
- http://localhost:8000/redoc for alternative documentation view
71+
72+
## Troubleshooting
73+
74+
### Common Issues
75+
1. If the extension shows "No repository detected":
76+
- Refresh the GitHub page
77+
- Make sure you're on a repository page
78+
79+
2. If analysis fails:
80+
- Check that the backend server is running
81+
- Look for errors in the server logs
82+
83+
3. If the extension doesn't load:
84+
- Check Chrome's extension page for errors
85+
- Try reloading the extension
86+
87+
### Server Logs
88+
The FastAPI server logs all operations. Check the terminal where you ran `start_server.sh` for detailed error messages.
89+
90+
## Architecture
91+
- `cli/`: Command-line interface for git analysis
92+
- `app/`: FastAPI web service
93+
- `extension/`: Chrome extension files
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""FastAPI application for git repository analysis."""

showcase/code-analysis/app/main.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import os
2+
import tempfile
3+
import logging
4+
from git import Repo
5+
from fastapi import FastAPI, HTTPException
6+
from fastapi.middleware.cors import CORSMiddleware
7+
from typing import Dict, Union
8+
from pydantic import BaseModel
9+
from cli.git_analysis import (
10+
analyze_commit_frequency,
11+
analyze_contributor_activity,
12+
analyze_commit_frequency_by_weekday,
13+
analyze_commit_frequency_by_hour,
14+
analyze_average_commit_size,
15+
analyze_file_change_frequency,
16+
)
17+
18+
# Configure logging
19+
logging.basicConfig(level=logging.INFO)
20+
logger = logging.getLogger(__name__)
21+
22+
app = FastAPI(title="Git Analysis API")
23+
24+
# Enable CORS
25+
app.add_middleware(
26+
CORSMiddleware,
27+
allow_origins=["chrome-extension://*"], # Allow requests from any Chrome extension
28+
allow_credentials=True,
29+
allow_methods=["*"],
30+
allow_headers=["*"],
31+
)
32+
33+
# Store repo paths temporarily
34+
repo_cache: Dict[str, str] = {}
35+
36+
37+
class CloneRequest(BaseModel):
38+
url: str
39+
40+
41+
@app.post("/clone")
42+
async def clone_repository(request: CloneRequest) -> Dict[str, str]:
43+
"""Clone a git repository and return a temporary ID to reference it."""
44+
try:
45+
# Create a temporary directory
46+
temp_dir = tempfile.mkdtemp()
47+
logger.info(f"Created temp directory: {temp_dir}")
48+
49+
# Clone the repository
50+
logger.info(f"Cloning repository from {request.url}")
51+
repo = Repo.clone_from(request.url, temp_dir)
52+
53+
# Generate a simple ID (you might want to use UUID in production)
54+
repo_id = str(hash(request.url))
55+
56+
# Store the mapping
57+
repo_cache[repo_id] = temp_dir
58+
logger.info(f"Successfully cloned repository. ID: {repo_id}")
59+
60+
return {"repo_id": repo_id}
61+
except Exception as e:
62+
logger.error(f"Error cloning repository: {str(e)}", exc_info=True)
63+
raise HTTPException(status_code=500, detail=str(e))
64+
65+
66+
@app.get("/analyze/{analysis_type}")
67+
async def analyze_repo(
68+
analysis_type: str, repo_id: str
69+
) -> Dict[str, Union[int, float]]:
70+
"""Analyze a git repository using the specified analysis type."""
71+
try:
72+
# Get repo path from cache
73+
repo_path = repo_cache.get(repo_id)
74+
if not repo_path:
75+
logger.error(f"Repository not found for ID: {repo_id}")
76+
raise HTTPException(
77+
status_code=404, detail="Repository not found. Please clone it first."
78+
)
79+
80+
logger.info(f"Analyzing repository at {repo_path}")
81+
82+
analysis_functions = {
83+
"commit-frequency": analyze_commit_frequency,
84+
"contributor-activity": analyze_contributor_activity,
85+
"commit-frequency-by-weekday": analyze_commit_frequency_by_weekday,
86+
"commit-frequency-by-hour": analyze_commit_frequency_by_hour,
87+
"average-commit-size": analyze_average_commit_size,
88+
"file-change-frequency": analyze_file_change_frequency,
89+
}
90+
91+
if analysis_type not in analysis_functions:
92+
logger.error(f"Invalid analysis type: {analysis_type}")
93+
raise HTTPException(
94+
status_code=400,
95+
detail=f"Invalid analysis type. Must be one of: {', '.join(analysis_functions.keys())}",
96+
) # Execute analysis
97+
logger.info(f"Running {analysis_type} analysis")
98+
results = analysis_functions[analysis_type](repo_path)
99+
logger.info(f"Analysis complete: {results}")
100+
101+
# Cast the dictionary to the expected type
102+
if not isinstance(results, dict):
103+
raise HTTPException(
104+
status_code=500, detail="Analysis returned invalid type"
105+
)
106+
return results
107+
except Exception as e:
108+
logger.error(f"Error during analysis: {str(e)}", exc_info=True)
109+
raise HTTPException(status_code=500, detail=str(e))
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
from typing import Dict
2+
from git import Repo
3+
from datetime import datetime, timezone
4+
from collections import defaultdict
5+
6+
7+
def analyze_commit_frequency(repo_path: str) -> Dict[str, int]:
8+
"""Analyze commit frequency by day for a git repository."""
9+
repo = Repo(repo_path)
10+
frequency: Dict[str, int] = defaultdict(int)
11+
12+
for commit in repo.iter_commits():
13+
date = datetime.fromtimestamp(commit.committed_date, timezone.utc)
14+
day = date.strftime("%Y-%m-%d")
15+
frequency[day] += 1
16+
17+
return dict(frequency)
18+
19+
20+
def analyze_commit_frequency_by_weekday(repo_path: str) -> Dict[str, int]:
21+
"""
22+
Analyze commit frequency by weekday (e.g., Monday, Tuesday).
23+
"""
24+
repo = Repo(repo_path)
25+
# Initialize all weekdays with zero count
26+
frequency = {
27+
"Monday": 0,
28+
"Tuesday": 0,
29+
"Wednesday": 0,
30+
"Thursday": 0,
31+
"Friday": 0,
32+
"Saturday": 0,
33+
"Sunday": 0,
34+
}
35+
36+
for commit in repo.iter_commits():
37+
date = datetime.fromtimestamp(commit.committed_date, timezone.utc)
38+
weekday = date.strftime("%A") # e.g., "Monday"
39+
frequency[weekday] += 1
40+
41+
return frequency
42+
43+
44+
def analyze_commit_frequency_by_hour(repo_path: str) -> Dict[str, int]:
45+
"""
46+
Analyze commit frequency by hour of day (0-23).
47+
"""
48+
repo = Repo(repo_path)
49+
# Initialize all hours with zero count
50+
frequency = {f"{hour:02d}": 0 for hour in range(24)}
51+
52+
for commit in repo.iter_commits():
53+
date = datetime.fromtimestamp(commit.committed_date, timezone.utc)
54+
hour = date.strftime("%H") # e.g., "13" for 1pm
55+
frequency[hour] += 1
56+
57+
return frequency
58+
59+
60+
def analyze_average_commit_size(repo_path: str) -> Dict[str, float]:
61+
"""
62+
Compute the average commit size (approximate lines added or removed per commit).
63+
"""
64+
repo = Repo(repo_path)
65+
total_changes = 0
66+
commit_count = 0
67+
68+
for commit in repo.iter_commits():
69+
# If the commit has no parent (e.g., the initial commit), skip
70+
if not commit.parents:
71+
continue
72+
73+
# Compare commit to its first parent
74+
diffs = commit.diff(commit.parents[0], create_patch=True)
75+
changes_in_commit = 0
76+
for diff in diffs:
77+
if not diff.diff:
78+
continue
79+
# diff.diff can be either string or bytes
80+
# A rough approach is to count the number of lines that start with "+" or "-"
81+
# ignoring lines that start with "+++" or "---" in the patch header
82+
if isinstance(diff.diff, bytes):
83+
patch_lines = diff.diff.decode("utf-8", errors="ignore").split("\n")
84+
else:
85+
patch_lines = diff.diff.split("\n")
86+
for line in patch_lines:
87+
if line.startswith("+") and not line.startswith("+++"):
88+
changes_in_commit += 1
89+
elif line.startswith("-") and not line.startswith("---"):
90+
changes_in_commit += 1
91+
92+
total_changes += changes_in_commit
93+
commit_count += 1
94+
95+
average_changes = total_changes / commit_count if commit_count else 0
96+
return {"average_commit_size": average_changes}
97+
98+
99+
def analyze_file_change_frequency(repo_path: str) -> Dict[str, int]:
100+
"""
101+
Analyze how often each file is changed in the repository.
102+
"""
103+
repo = Repo(repo_path)
104+
file_frequency: Dict[str, int] = defaultdict(int)
105+
106+
for commit in repo.iter_commits():
107+
# Skip the initial commit (no parent to compare against)
108+
if not commit.parents:
109+
continue
110+
111+
parent = commit.parents[0]
112+
# diff() returns a list of diff objects representing file changes
113+
diffs = commit.diff(parent)
114+
115+
for diff in diffs:
116+
# a_path and b_path might be different if the file was renamed
117+
file_path = diff.a_path or diff.b_path
118+
if file_path: # Only count if we have a valid path
119+
file_frequency[file_path] += 1
120+
121+
return dict(file_frequency)
122+
123+
124+
def analyze_contributor_activity(repo_path: str) -> Dict[str, int]:
125+
"""Analyze commit count per contributor in a git repository."""
126+
repo = Repo(repo_path)
127+
activity: Dict[str, int] = defaultdict(int)
128+
129+
for commit in repo.iter_commits():
130+
author = f"{commit.author.name} <{commit.author.email}>"
131+
activity[author] += 1
132+
133+
return dict(activity)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Store repository info from content scripts
2+
let currentRepoInfo = null;
3+
4+
// Listen for messages from content script
5+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
6+
if (message.type === 'REPO_DETECTED') {
7+
currentRepoInfo = message.data;
8+
}
9+
});
10+
11+
// Listen for messages from popup
12+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
13+
if (message.type === 'GET_REPO_INFO') {
14+
sendResponse({ data: currentRepoInfo });
15+
}
16+
return true; // Keep channel open for async response
17+
});

0 commit comments

Comments
 (0)