commitpulse 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- commitpulse-0.1.0/PKG-INFO +126 -0
- commitpulse-0.1.0/README.md +117 -0
- commitpulse-0.1.0/commitpulse/__init__.py +1 -0
- commitpulse-0.1.0/commitpulse/analyzer.py +162 -0
- commitpulse-0.1.0/commitpulse/main.py +118 -0
- commitpulse-0.1.0/commitpulse/renderer.py +537 -0
- commitpulse-0.1.0/commitpulse.egg-info/PKG-INFO +126 -0
- commitpulse-0.1.0/commitpulse.egg-info/SOURCES.txt +12 -0
- commitpulse-0.1.0/commitpulse.egg-info/dependency_links.txt +1 -0
- commitpulse-0.1.0/commitpulse.egg-info/entry_points.txt +2 -0
- commitpulse-0.1.0/commitpulse.egg-info/requires.txt +1 -0
- commitpulse-0.1.0/commitpulse.egg-info/top_level.txt +1 -0
- commitpulse-0.1.0/pyproject.toml +23 -0
- commitpulse-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: commitpulse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Premium Git repository analytics and dashboard generator
|
|
5
|
+
Author: Antigravity AI
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests>=2.31.0
|
|
9
|
+
|
|
10
|
+
# Commit Pulse 🚀
|
|
11
|
+
|
|
12
|
+
Commit Pulse is a powerful, locally-first CLI tool that generates high-fidelity engineering dashboards for your Git repositories. It analyzes your development ecosystem, visualizes commit intensity heatmaps, and identifies top contributors using real GitHub profiles.
|
|
13
|
+
|
|
14
|
+
## ✨ Key Features
|
|
15
|
+
- **Zero-PAT Analysis**: Analyzes local `.git` metadata directly. No Personal Access Tokens or cloud APIs required for core stats.
|
|
16
|
+
- **Accurate Project Timelines**: Dynamic contribution grids that span from the repository's inception to the **last official commit**.
|
|
17
|
+
- **GitHub Profile Integration**: Automatically fetches actual contributor avatars via the public GitHub Search API.
|
|
18
|
+
- **Master Scan**: A recursive drive scanner that aggregates stats from every project on your machine into a single interactive dashboard.
|
|
19
|
+
- **Premium Aesthetics**: Glassmorphic UI with dark mode, smooth animations, and official GitHub iconography.
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
### 1. Local (Developer Mode)
|
|
24
|
+
If you have the source code locally:
|
|
25
|
+
```bash
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Direct from GitHub
|
|
30
|
+
Anyone can install your tool directly if the repo is public:
|
|
31
|
+
```bash
|
|
32
|
+
pip install git+https://github.com/YOUR_USERNAME/commitpulse.git
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 3. Global Command
|
|
36
|
+
Once installed, the `commitpulse` command is available everywhere in your terminal.
|
|
37
|
+
|
|
38
|
+
## 🚀 Usage
|
|
39
|
+
|
|
40
|
+
### Analyze the current repository
|
|
41
|
+
```bash
|
|
42
|
+
commitpulse
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Analyze all repositories on your computer (Master Scan)
|
|
46
|
+
```bash
|
|
47
|
+
commitpulse --scan
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Specify a target directory
|
|
51
|
+
```bash
|
|
52
|
+
commitpulse /path/to/projects
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 🛠️ Options
|
|
56
|
+
- `--scan`: Crawls subdirectories to find and aggregate all Git repositories.
|
|
57
|
+
- `--no-open`: Generates the dashboard without automatically opening it in the browser.
|
|
58
|
+
|
|
59
|
+
## 🚀 Deployment & Sharing
|
|
60
|
+
|
|
61
|
+
Commit Pulse offers two ways to view and share your results:
|
|
62
|
+
|
|
63
|
+
### 1. Local-First (Default)
|
|
64
|
+
Run the command to generate a self-contained, interactive HTML dashboard on your machine. Perfect for private analysis.
|
|
65
|
+
```bash
|
|
66
|
+
commitpulse --scan
|
|
67
|
+
```
|
|
68
|
+
*Output: `stats_dashboard.html`*
|
|
69
|
+
|
|
70
|
+
### 2. Commit Pulse Cloud (Global Sharing)
|
|
71
|
+
Ready to show the world? Use the `--publish` flag to host your dashboard on the cloud and get a unique, shareable URL.
|
|
72
|
+
```bash
|
|
73
|
+
commitpulse --publish
|
|
74
|
+
```
|
|
75
|
+
*Output: `https://commitpulse.app/v/your-unique-id`*
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 🛠️ Setting up your own Cloud Instance (Optional)
|
|
80
|
+
|
|
81
|
+
If you are hosting your own version of **Commit Pulse Cloud**:
|
|
82
|
+
|
|
83
|
+
1. **Neon DB**:
|
|
84
|
+
- Go to [Neon.tech](https://neon.tech) and create a free project.
|
|
85
|
+
- Copy your **Connection String** (e.g., `postgresql://user:pass@ep-cool-beach-123.us-east-2.aws.neon.tech/neondb?sslmode=require`).
|
|
86
|
+
2. **Environment Setup**:
|
|
87
|
+
- In the `commitpulse-cloud` directory, rename `.env.example` to `.env`.
|
|
88
|
+
- Paste your connection string into `DATABASE_URL`.
|
|
89
|
+
3. **Vercel Deployment**:
|
|
90
|
+
- Deploy the `commitpulse-cloud` folder to Vercel.
|
|
91
|
+
- Add the `DATABASE_URL` to your Vercel Environment Variables.
|
|
92
|
+
|
|
93
|
+
## 🌍 Distribution & PyPI Hosting
|
|
94
|
+
|
|
95
|
+
To share **Commit Pulse** with the global developer community via PyPI:
|
|
96
|
+
|
|
97
|
+
### 1. Prepare your Account
|
|
98
|
+
- Register at [pypi.org](https://pypi.org/).
|
|
99
|
+
- Generate an API Token in your account settings.
|
|
100
|
+
|
|
101
|
+
### 2. Build the Distribution
|
|
102
|
+
Ensure you have the latest build tools:
|
|
103
|
+
```bash
|
|
104
|
+
python -m pip install --upgrade build twine
|
|
105
|
+
```
|
|
106
|
+
Then build the package:
|
|
107
|
+
```bash
|
|
108
|
+
python -m build
|
|
109
|
+
```
|
|
110
|
+
This creates a `dist/` folder with your `.whl` and `.tar.gz` files.
|
|
111
|
+
|
|
112
|
+
### 3. Upload to PyPI
|
|
113
|
+
Use `twine` to securely upload your package:
|
|
114
|
+
```bash
|
|
115
|
+
python -m twine upload dist/*
|
|
116
|
+
```
|
|
117
|
+
- **Username**: `__token__`
|
|
118
|
+
- **Password**: `[Your API Token]`
|
|
119
|
+
|
|
120
|
+
Once uploaded, anyone can install it via:
|
|
121
|
+
```bash
|
|
122
|
+
pip install commitpulse
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
*Built with ❤️ for the development ecosystem.*
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Commit Pulse 🚀
|
|
2
|
+
|
|
3
|
+
Commit Pulse is a powerful, locally-first CLI tool that generates high-fidelity engineering dashboards for your Git repositories. It analyzes your development ecosystem, visualizes commit intensity heatmaps, and identifies top contributors using real GitHub profiles.
|
|
4
|
+
|
|
5
|
+
## ✨ Key Features
|
|
6
|
+
- **Zero-PAT Analysis**: Analyzes local `.git` metadata directly. No Personal Access Tokens or cloud APIs required for core stats.
|
|
7
|
+
- **Accurate Project Timelines**: Dynamic contribution grids that span from the repository's inception to the **last official commit**.
|
|
8
|
+
- **GitHub Profile Integration**: Automatically fetches actual contributor avatars via the public GitHub Search API.
|
|
9
|
+
- **Master Scan**: A recursive drive scanner that aggregates stats from every project on your machine into a single interactive dashboard.
|
|
10
|
+
- **Premium Aesthetics**: Glassmorphic UI with dark mode, smooth animations, and official GitHub iconography.
|
|
11
|
+
|
|
12
|
+
## 📦 Installation
|
|
13
|
+
|
|
14
|
+
### 1. Local (Developer Mode)
|
|
15
|
+
If you have the source code locally:
|
|
16
|
+
```bash
|
|
17
|
+
pip install -e .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 2. Direct from GitHub
|
|
21
|
+
Anyone can install your tool directly if the repo is public:
|
|
22
|
+
```bash
|
|
23
|
+
pip install git+https://github.com/YOUR_USERNAME/commitpulse.git
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 3. Global Command
|
|
27
|
+
Once installed, the `commitpulse` command is available everywhere in your terminal.
|
|
28
|
+
|
|
29
|
+
## 🚀 Usage
|
|
30
|
+
|
|
31
|
+
### Analyze the current repository
|
|
32
|
+
```bash
|
|
33
|
+
commitpulse
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Analyze all repositories on your computer (Master Scan)
|
|
37
|
+
```bash
|
|
38
|
+
commitpulse --scan
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Specify a target directory
|
|
42
|
+
```bash
|
|
43
|
+
commitpulse /path/to/projects
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 🛠️ Options
|
|
47
|
+
- `--scan`: Crawls subdirectories to find and aggregate all Git repositories.
|
|
48
|
+
- `--no-open`: Generates the dashboard without automatically opening it in the browser.
|
|
49
|
+
|
|
50
|
+
## 🚀 Deployment & Sharing
|
|
51
|
+
|
|
52
|
+
Commit Pulse offers two ways to view and share your results:
|
|
53
|
+
|
|
54
|
+
### 1. Local-First (Default)
|
|
55
|
+
Run the command to generate a self-contained, interactive HTML dashboard on your machine. Perfect for private analysis.
|
|
56
|
+
```bash
|
|
57
|
+
commitpulse --scan
|
|
58
|
+
```
|
|
59
|
+
*Output: `stats_dashboard.html`*
|
|
60
|
+
|
|
61
|
+
### 2. Commit Pulse Cloud (Global Sharing)
|
|
62
|
+
Ready to show the world? Use the `--publish` flag to host your dashboard on the cloud and get a unique, shareable URL.
|
|
63
|
+
```bash
|
|
64
|
+
commitpulse --publish
|
|
65
|
+
```
|
|
66
|
+
*Output: `https://commitpulse.app/v/your-unique-id`*
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 🛠️ Setting up your own Cloud Instance (Optional)
|
|
71
|
+
|
|
72
|
+
If you are hosting your own version of **Commit Pulse Cloud**:
|
|
73
|
+
|
|
74
|
+
1. **Neon DB**:
|
|
75
|
+
- Go to [Neon.tech](https://neon.tech) and create a free project.
|
|
76
|
+
- Copy your **Connection String** (e.g., `postgresql://user:pass@ep-cool-beach-123.us-east-2.aws.neon.tech/neondb?sslmode=require`).
|
|
77
|
+
2. **Environment Setup**:
|
|
78
|
+
- In the `commitpulse-cloud` directory, rename `.env.example` to `.env`.
|
|
79
|
+
- Paste your connection string into `DATABASE_URL`.
|
|
80
|
+
3. **Vercel Deployment**:
|
|
81
|
+
- Deploy the `commitpulse-cloud` folder to Vercel.
|
|
82
|
+
- Add the `DATABASE_URL` to your Vercel Environment Variables.
|
|
83
|
+
|
|
84
|
+
## 🌍 Distribution & PyPI Hosting
|
|
85
|
+
|
|
86
|
+
To share **Commit Pulse** with the global developer community via PyPI:
|
|
87
|
+
|
|
88
|
+
### 1. Prepare your Account
|
|
89
|
+
- Register at [pypi.org](https://pypi.org/).
|
|
90
|
+
- Generate an API Token in your account settings.
|
|
91
|
+
|
|
92
|
+
### 2. Build the Distribution
|
|
93
|
+
Ensure you have the latest build tools:
|
|
94
|
+
```bash
|
|
95
|
+
python -m pip install --upgrade build twine
|
|
96
|
+
```
|
|
97
|
+
Then build the package:
|
|
98
|
+
```bash
|
|
99
|
+
python -m build
|
|
100
|
+
```
|
|
101
|
+
This creates a `dist/` folder with your `.whl` and `.tar.gz` files.
|
|
102
|
+
|
|
103
|
+
### 3. Upload to PyPI
|
|
104
|
+
Use `twine` to securely upload your package:
|
|
105
|
+
```bash
|
|
106
|
+
python -m twine upload dist/*
|
|
107
|
+
```
|
|
108
|
+
- **Username**: `__token__`
|
|
109
|
+
- **Password**: `[Your API Token]`
|
|
110
|
+
|
|
111
|
+
Once uploaded, anyone can install it via:
|
|
112
|
+
```bash
|
|
113
|
+
pip install commitpulse
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
*Built with ❤️ for the development ecosystem.*
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# NovaStats Package
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import urllib.request
|
|
6
|
+
import urllib.parse
|
|
7
|
+
from collections import Counter
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
|
|
10
|
+
class GitAnalyzer:
|
|
11
|
+
def __init__(self, repo_path):
|
|
12
|
+
self.repo_path = os.path.abspath(repo_path)
|
|
13
|
+
self.repo_name = os.path.basename(self.repo_path)
|
|
14
|
+
self.github_avatar_cache = {}
|
|
15
|
+
|
|
16
|
+
def _run_git(self, args):
|
|
17
|
+
try:
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
['git'] + args,
|
|
20
|
+
cwd=self.repo_path,
|
|
21
|
+
capture_output=True,
|
|
22
|
+
text=True,
|
|
23
|
+
check=True,
|
|
24
|
+
shell=True
|
|
25
|
+
)
|
|
26
|
+
return result.stdout.strip()
|
|
27
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
def is_git_repo(self):
|
|
31
|
+
return os.path.exists(os.path.join(self.repo_path, '.git'))
|
|
32
|
+
|
|
33
|
+
def _get_github_avatar(self, email):
|
|
34
|
+
if email in self.github_avatar_cache:
|
|
35
|
+
return self.github_avatar_cache[email]
|
|
36
|
+
|
|
37
|
+
# Default to Gravatar
|
|
38
|
+
email_hash = hashlib.md5(email.lower().encode('utf-8')).hexdigest()
|
|
39
|
+
avatar_url = f"https://www.gravatar.com/avatar/{email_hash}?d=identicon&s=150"
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Try to search for user by email using GitHub's public API
|
|
43
|
+
search_url = f"https://api.github.com/search/users?q={urllib.parse.quote(email)}"
|
|
44
|
+
request = urllib.request.Request(search_url)
|
|
45
|
+
request.add_header('User-Agent', 'CommitPulse-CLI-v0.1')
|
|
46
|
+
|
|
47
|
+
with urllib.request.urlopen(request, timeout=5) as response:
|
|
48
|
+
data = json.loads(response.read().decode())
|
|
49
|
+
if data.get('total_count', 0) > 0:
|
|
50
|
+
avatar_url = data['items'][0]['avatar_url']
|
|
51
|
+
except Exception:
|
|
52
|
+
# Silently fall back to Gravatar on any error (rate limit, network, etc)
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
self.github_avatar_cache[email] = avatar_url
|
|
56
|
+
return avatar_url
|
|
57
|
+
|
|
58
|
+
def get_stats(self):
|
|
59
|
+
if not self.is_git_repo():
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
print(f"Analyzing {self.repo_name}... (High Precision)")
|
|
63
|
+
|
|
64
|
+
# Get all commit dates and times for heatmap and productivity
|
|
65
|
+
# Use --all to include all branches and --no-merges to match GitHub's default contribution view
|
|
66
|
+
# or remove --no-merges if we want every single commit. GitHub counts merges if they occurred on GitHub.
|
|
67
|
+
# We'll stick to --all for full visibility.
|
|
68
|
+
commit_dates_raw = self._run_git(['log', '--all', '--format=%aI'])
|
|
69
|
+
|
|
70
|
+
heatmap_data = Counter()
|
|
71
|
+
hourly_distribution = Counter()
|
|
72
|
+
|
|
73
|
+
if commit_dates_raw:
|
|
74
|
+
dates = commit_dates_raw.split('\n')
|
|
75
|
+
for d in dates:
|
|
76
|
+
if not d: continue
|
|
77
|
+
try:
|
|
78
|
+
dt = datetime.fromisoformat(d)
|
|
79
|
+
date_str = dt.date().isoformat()
|
|
80
|
+
hour = dt.hour
|
|
81
|
+
heatmap_data[date_str] += 1
|
|
82
|
+
hourly_distribution[hour] += 1
|
|
83
|
+
except ValueError:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
total_commits = sum(heatmap_data.values())
|
|
87
|
+
|
|
88
|
+
# Get first and last commit dates
|
|
89
|
+
first_date = "N/A"
|
|
90
|
+
last_date = "N/A"
|
|
91
|
+
if heatmap_data:
|
|
92
|
+
sorted_dates = sorted(heatmap_data.keys())
|
|
93
|
+
first_date = sorted_dates[0]
|
|
94
|
+
last_date = sorted_dates[-1]
|
|
95
|
+
|
|
96
|
+
# Get contributors with high precision
|
|
97
|
+
# Format: count <tab> name <email>
|
|
98
|
+
shortlog = self._run_git(['shortlog', '-sne', '--all'])
|
|
99
|
+
contributors = []
|
|
100
|
+
if shortlog:
|
|
101
|
+
for line in shortlog.split('\n'):
|
|
102
|
+
line = line.strip()
|
|
103
|
+
if not line: continue
|
|
104
|
+
parts = line.split('\t')
|
|
105
|
+
if len(parts) == 2:
|
|
106
|
+
count = int(parts[0].strip())
|
|
107
|
+
name_email = parts[1].strip()
|
|
108
|
+
name = name_email
|
|
109
|
+
email = ""
|
|
110
|
+
if '<' in name_email:
|
|
111
|
+
name = name_email.split('<')[0].strip()
|
|
112
|
+
email = name_email.split('<')[1].split('>')[0].strip()
|
|
113
|
+
|
|
114
|
+
avatar = self._get_github_avatar(email)
|
|
115
|
+
|
|
116
|
+
contributors.append({
|
|
117
|
+
"name": name,
|
|
118
|
+
"email": email,
|
|
119
|
+
"commits": count,
|
|
120
|
+
"avatar": avatar
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
# Calculate most productive hour
|
|
124
|
+
peak_hour = "N/A"
|
|
125
|
+
if hourly_distribution:
|
|
126
|
+
peak_hour = f"{hourly_distribution.most_common(1)[0][0]}:00"
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"name": self.repo_name,
|
|
130
|
+
"path": self.repo_path,
|
|
131
|
+
"total_commits": total_commits,
|
|
132
|
+
"first_commit": first_date,
|
|
133
|
+
"last_commit": last_date,
|
|
134
|
+
"peak_hour": peak_hour,
|
|
135
|
+
"heatmap": dict(heatmap_data),
|
|
136
|
+
"hourly_distribution": dict(hourly_distribution),
|
|
137
|
+
"contributors": sorted(contributors, key=lambda x: x['commits'], reverse=True)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def scan_for_repos(root_dir, max_depth=3):
|
|
142
|
+
repos = []
|
|
143
|
+
root_dir = os.path.abspath(root_dir)
|
|
144
|
+
|
|
145
|
+
for dirpath, dirnames, filenames in os.walk(root_dir):
|
|
146
|
+
if '.git' in dirnames:
|
|
147
|
+
repos.append(dirpath)
|
|
148
|
+
dirnames.remove('.git')
|
|
149
|
+
|
|
150
|
+
depth = dirpath.replace(root_dir, '').count(os.sep)
|
|
151
|
+
if depth >= max_depth:
|
|
152
|
+
del dirnames[:]
|
|
153
|
+
|
|
154
|
+
return repos
|
|
155
|
+
@staticmethod
|
|
156
|
+
def get_git_config_user():
|
|
157
|
+
try:
|
|
158
|
+
import subprocess
|
|
159
|
+
name = subprocess.check_output(["git", "config", "user.name"]).decode().strip()
|
|
160
|
+
return name
|
|
161
|
+
except:
|
|
162
|
+
return None
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import webbrowser
|
|
5
|
+
from .analyzer import GitAnalyzer
|
|
6
|
+
from .renderer import DashboardRenderer
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
description="Commit Pulse - Premium Git Repository Analytics Dashboard"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
'path',
|
|
15
|
+
nargs='?',
|
|
16
|
+
default='.',
|
|
17
|
+
help="Path to the repository (default is current directory)"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
'--scan',
|
|
22
|
+
action='store_true',
|
|
23
|
+
help="Scan the current directory (and subdirectories) for all git repositories"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
'--no-open',
|
|
28
|
+
action='store_true',
|
|
29
|
+
help="Do not automatically open the dashboard in the browser"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
'--publish',
|
|
34
|
+
action='store_true',
|
|
35
|
+
help="Publish your project's pulse to the cloud for sharing"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
args = parser.parse_args()
|
|
39
|
+
|
|
40
|
+
all_stats = []
|
|
41
|
+
|
|
42
|
+
if args.scan:
|
|
43
|
+
print(f"Scanning for git repositories in {os.path.abspath(args.path)}...")
|
|
44
|
+
repo_paths = GitAnalyzer.scan_for_repos(args.path)
|
|
45
|
+
print(f"Found {len(repo_paths)} repositories.")
|
|
46
|
+
|
|
47
|
+
for p in repo_paths:
|
|
48
|
+
analyzer = GitAnalyzer(p)
|
|
49
|
+
stats = analyzer.get_stats()
|
|
50
|
+
if stats:
|
|
51
|
+
all_stats.append(stats)
|
|
52
|
+
else:
|
|
53
|
+
analyzer = GitAnalyzer(args.path)
|
|
54
|
+
if not analyzer.is_git_repo():
|
|
55
|
+
print(f"Error: {os.path.abspath(args.path)} is not a git repository.")
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
stats = analyzer.get_stats()
|
|
59
|
+
if stats:
|
|
60
|
+
all_stats.append(stats)
|
|
61
|
+
|
|
62
|
+
if not all_stats:
|
|
63
|
+
print("Note: No statistics could be gathered.")
|
|
64
|
+
sys.exit(0)
|
|
65
|
+
|
|
66
|
+
# Render dashboard
|
|
67
|
+
renderer = DashboardRenderer(all_stats)
|
|
68
|
+
output_path = renderer.render()
|
|
69
|
+
|
|
70
|
+
print(f"\nDashboard generated successfully!")
|
|
71
|
+
print(f"Location: {output_path}")
|
|
72
|
+
|
|
73
|
+
# Decide whether to open local HTML
|
|
74
|
+
# If publishing, we might want to prioritize the cloud link instead of popping up the local file
|
|
75
|
+
should_open_local = not args.no_open
|
|
76
|
+
if args.publish and not args.no_open:
|
|
77
|
+
# If they are publishing, maybe they only want the web link?
|
|
78
|
+
# Let's still open local unless it's a server environment, but clarify.
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
if should_open_local:
|
|
82
|
+
webbrowser.open(f"file://{output_path}")
|
|
83
|
+
|
|
84
|
+
# Cloud Publishing
|
|
85
|
+
if args.publish:
|
|
86
|
+
import requests
|
|
87
|
+
# Get username from git config
|
|
88
|
+
username = GitAnalyzer.get_git_config_user() or os.getenv("USER") or "Anonymous"
|
|
89
|
+
|
|
90
|
+
print(f"\n🚀 Publishing your Pulse to the cloud as '{username}'...")
|
|
91
|
+
|
|
92
|
+
# In a real scenario, this would be your production URL
|
|
93
|
+
# For now, we point to the local or configured cloud instance
|
|
94
|
+
cloud_url = os.getenv("COMMITPULSE_CLOUD_URL", "http://localhost:3000")
|
|
95
|
+
api_endpoint = f"{cloud_url}/api/publish"
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
# We publish the first repo in scanning mode or the target repo
|
|
99
|
+
# For simplicity, we publish the main one if multiple found
|
|
100
|
+
repo_to_publish = all_stats[0]
|
|
101
|
+
|
|
102
|
+
payload = {
|
|
103
|
+
"username": username,
|
|
104
|
+
"repoName": repo_to_publish["name"],
|
|
105
|
+
"stats": all_stats # Send all scanned repos
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
response = requests.post(api_endpoint, json=payload, timeout=10)
|
|
109
|
+
if response.status_code == 200:
|
|
110
|
+
result = response.json()
|
|
111
|
+
print(f"✨ Successfully published! Share your Pulse at: {result['url']}")
|
|
112
|
+
else:
|
|
113
|
+
print(f"❌ Failed to publish: {response.text}")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f"⚠️ Cloud sync failed: {str(e)}")
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
main()
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
class DashboardRenderer:
|
|
6
|
+
def __init__(self, stats_data):
|
|
7
|
+
self.stats_data = stats_data
|
|
8
|
+
|
|
9
|
+
def render(self, output_path="stats_dashboard.html"):
|
|
10
|
+
html_template = """
|
|
11
|
+
<!DOCTYPE html>
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<head>
|
|
14
|
+
<meta charset="UTF-8">
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
16
|
+
<title>Commit Pulse | Repository Ecosystem</title>
|
|
17
|
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
18
|
+
<style>
|
|
19
|
+
:root {
|
|
20
|
+
--bg-dark: #0d1117;
|
|
21
|
+
--card-bg: rgba(22, 27, 34, 0.7);
|
|
22
|
+
--border: rgba(48, 54, 61, 0.8);
|
|
23
|
+
--text-main: #f0f6fc;
|
|
24
|
+
--text-dim: #8b949e;
|
|
25
|
+
--accent: #238636;
|
|
26
|
+
--accent-glow: rgba(35, 134, 54, 0.3);
|
|
27
|
+
--github-blue: #58a6ff;
|
|
28
|
+
--glass-blur: blur(12px);
|
|
29
|
+
|
|
30
|
+
/* Heatmap Colors */
|
|
31
|
+
--h-0: #161b22;
|
|
32
|
+
--h-1: #0e4429;
|
|
33
|
+
--h-2: #006d32;
|
|
34
|
+
--h-3: #26a641;
|
|
35
|
+
--h-4: #39d353;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
* {
|
|
39
|
+
box-sizing: border-box;
|
|
40
|
+
transition: all 0.2s ease-in-out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
body {
|
|
44
|
+
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
45
|
+
background-color: var(--bg-dark);
|
|
46
|
+
background-image: radial-gradient(circle at 50% 10%, rgba(35, 134, 54, 0.05) 0%, transparent 50%);
|
|
47
|
+
color: var(--text-main);
|
|
48
|
+
margin: 0;
|
|
49
|
+
padding: 0;
|
|
50
|
+
overflow-x: hidden;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
header {
|
|
54
|
+
padding: 30px 60px;
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: space-between;
|
|
58
|
+
position: sticky;
|
|
59
|
+
top: 0;
|
|
60
|
+
z-index: 100;
|
|
61
|
+
background: rgba(13, 17, 23, 0.8);
|
|
62
|
+
backdrop-filter: var(--glass-blur);
|
|
63
|
+
border-bottom: 1px solid var(--border);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.header-left {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 15px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.logo-icon {
|
|
73
|
+
fill: var(--text-main);
|
|
74
|
+
filter: drop-shadow(0 0 8px rgba(255,255,255,0.2));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
h1 {
|
|
78
|
+
font-size: 24px;
|
|
79
|
+
font-weight: 700;
|
|
80
|
+
margin: 0;
|
|
81
|
+
letter-spacing: -0.5px;
|
|
82
|
+
background: linear-gradient(90deg, #fff, var(--text-dim));
|
|
83
|
+
-webkit-background-clip: text;
|
|
84
|
+
-webkit-text-fill-color: transparent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.container {
|
|
88
|
+
max-width: 1400px;
|
|
89
|
+
margin: 0 auto;
|
|
90
|
+
padding: 40px 60px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.stats-grid {
|
|
94
|
+
display: grid;
|
|
95
|
+
grid-template-columns: 1fr;
|
|
96
|
+
gap: 40px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.repo-card {
|
|
100
|
+
background: var(--card-bg);
|
|
101
|
+
border: 1px solid var(--border);
|
|
102
|
+
border-radius: 16px;
|
|
103
|
+
padding: 32px;
|
|
104
|
+
backdrop-filter: var(--glass-blur);
|
|
105
|
+
position: relative;
|
|
106
|
+
overflow: hidden;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.repo-card::before {
|
|
110
|
+
content: '';
|
|
111
|
+
position: absolute;
|
|
112
|
+
top: 0; left: 0; width: 4px; height: 100%;
|
|
113
|
+
background: var(--accent);
|
|
114
|
+
opacity: 0;
|
|
115
|
+
transition: 0.3s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.repo-card:hover {
|
|
119
|
+
border-color: var(--github-blue);
|
|
120
|
+
transform: translateY(-4px);
|
|
121
|
+
box-shadow: 0 12px 24px rgba(0,0,0,0.3);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.repo-card:hover::before {
|
|
125
|
+
opacity: 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.repo-title-row {
|
|
129
|
+
display: flex;
|
|
130
|
+
justify-content: space-between;
|
|
131
|
+
align-items: flex-start;
|
|
132
|
+
margin-bottom: 24px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.repo-name-group {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: 12px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.repo-name {
|
|
142
|
+
font-size: 28px;
|
|
143
|
+
font-weight: 700;
|
|
144
|
+
color: var(--text-main);
|
|
145
|
+
text-decoration: none;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.repo-meta-badges {
|
|
149
|
+
display: flex;
|
|
150
|
+
gap: 12px;
|
|
151
|
+
margin-bottom: 32px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.badge {
|
|
155
|
+
background: rgba(48, 54, 61, 0.4);
|
|
156
|
+
border: 1px solid var(--border);
|
|
157
|
+
color: var(--text-dim);
|
|
158
|
+
padding: 4px 12px;
|
|
159
|
+
border-radius: 20px;
|
|
160
|
+
font-size: 13px;
|
|
161
|
+
font-weight: 500;
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
gap: 6px;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.badge.highlight {
|
|
168
|
+
border-color: var(--accent);
|
|
169
|
+
color: var(--accent);
|
|
170
|
+
background: rgba(35, 134, 54, 0.1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.dashboard-row {
|
|
174
|
+
display: grid;
|
|
175
|
+
grid-template-columns: 1fr;
|
|
176
|
+
gap: 32px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* HEATMAP STYLES */
|
|
180
|
+
.heatmap-container {
|
|
181
|
+
background: rgba(0,0,0,0.2);
|
|
182
|
+
padding: 32px;
|
|
183
|
+
border-radius: 12px;
|
|
184
|
+
border: 1px solid var(--border);
|
|
185
|
+
width: 100%;
|
|
186
|
+
overflow: hidden;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.heatmap-wrapper {
|
|
190
|
+
overflow-x: auto;
|
|
191
|
+
display: flex;
|
|
192
|
+
gap: 16px;
|
|
193
|
+
padding-bottom: 12px;
|
|
194
|
+
scrollbar-width: thin;
|
|
195
|
+
scrollbar-color: var(--border) transparent;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.heatmap-wrapper::-webkit-scrollbar {
|
|
199
|
+
height: 6px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.heatmap-wrapper::-webkit-scrollbar-track {
|
|
203
|
+
background: transparent;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.heatmap-wrapper::-webkit-scrollbar-thumb {
|
|
207
|
+
background: var(--border);
|
|
208
|
+
border-radius: 10px;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.heatmap-wrapper::-webkit-scrollbar-thumb:hover {
|
|
212
|
+
background: var(--text-dim);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.section-label {
|
|
216
|
+
font-size: 14px;
|
|
217
|
+
font-weight: 600;
|
|
218
|
+
text-transform: uppercase;
|
|
219
|
+
color: var(--text-dim);
|
|
220
|
+
letter-spacing: 1px;
|
|
221
|
+
margin-bottom: 20px;
|
|
222
|
+
display: flex;
|
|
223
|
+
justify-content: space-between;
|
|
224
|
+
align-items: center;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.heatmap-month-group {
|
|
228
|
+
display: flex;
|
|
229
|
+
flex-direction: column;
|
|
230
|
+
gap: 8px;
|
|
231
|
+
flex-shrink: 0; /* Don't squish month groups */
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.month-name {
|
|
235
|
+
font-size: 11px;
|
|
236
|
+
font-weight: 700;
|
|
237
|
+
color: var(--github-blue); /* Subtle blue for headers */
|
|
238
|
+
text-transform: uppercase;
|
|
239
|
+
letter-spacing: 0.5px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.heatmap-grid {
|
|
243
|
+
display: flex;
|
|
244
|
+
flex-flow: column wrap;
|
|
245
|
+
height: 122px; /* (14px * 7) + (4px * 6) = 122px */
|
|
246
|
+
gap: 4px;
|
|
247
|
+
align-content: flex-start;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.heatmap-cell {
|
|
251
|
+
width: 14px;
|
|
252
|
+
height: 14px;
|
|
253
|
+
border-radius: 2px;
|
|
254
|
+
background-color: var(--h-0);
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
flex-shrink: 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.heatmap-cell.empty {
|
|
260
|
+
background-color: transparent;
|
|
261
|
+
cursor: default;
|
|
262
|
+
pointer-events: none;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.heatmap-cell:hover {
|
|
266
|
+
outline: 2px solid var(--text-main);
|
|
267
|
+
z-index: 10;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.intensity-1 { background-color: var(--h-1); }
|
|
271
|
+
.intensity-2 { background-color: var(--h-2); }
|
|
272
|
+
.intensity-3 { background-color: var(--h-3); }
|
|
273
|
+
.intensity-4 { background-color: var(--h-4); }
|
|
274
|
+
|
|
275
|
+
.legend {
|
|
276
|
+
display: flex;
|
|
277
|
+
align-items: center;
|
|
278
|
+
gap: 6px;
|
|
279
|
+
font-size: 12px;
|
|
280
|
+
color: var(--text-dim);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.legend-box {
|
|
284
|
+
width: 10px;
|
|
285
|
+
height: 10px;
|
|
286
|
+
border-radius: 2px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* CONTRIBUTOR STYLES */
|
|
290
|
+
.contributor-panel {
|
|
291
|
+
background: rgba(0,0,0,0.2);
|
|
292
|
+
padding: 24px;
|
|
293
|
+
border-radius: 12px;
|
|
294
|
+
border: 1px solid var(--border);
|
|
295
|
+
margin-top: 24px;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.contributor-list {
|
|
299
|
+
display: flex;
|
|
300
|
+
flex-wrap: wrap;
|
|
301
|
+
gap: 24px;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.contributor-item {
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
gap: 12px;
|
|
308
|
+
padding: 8px;
|
|
309
|
+
border-radius: 8px;
|
|
310
|
+
background: rgba(255,255,255,0.02);
|
|
311
|
+
min-width: 200px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.contributor-info {
|
|
315
|
+
display: flex;
|
|
316
|
+
align-items: center;
|
|
317
|
+
gap: 12px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.avatar {
|
|
321
|
+
width: 40px;
|
|
322
|
+
height: 40px;
|
|
323
|
+
border-radius: 50%;
|
|
324
|
+
border: 2px solid var(--border);
|
|
325
|
+
padding: 2px;
|
|
326
|
+
background: #0d1117;
|
|
327
|
+
object-fit: cover;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.contributor-name {
|
|
331
|
+
font-weight: 600;
|
|
332
|
+
font-size: 15px;
|
|
333
|
+
color: var(--text-main);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.commit-pill {
|
|
337
|
+
background: var(--accent);
|
|
338
|
+
color: #fff;
|
|
339
|
+
padding: 4px 12px;
|
|
340
|
+
border-radius: 6px;
|
|
341
|
+
font-weight: 600;
|
|
342
|
+
font-size: 12px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
footer {
|
|
346
|
+
text-align: center;
|
|
347
|
+
padding: 60px;
|
|
348
|
+
color: var(--text-dim);
|
|
349
|
+
font-size: 14px;
|
|
350
|
+
opacity: 0.6;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@keyframes fadeIn {
|
|
354
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
355
|
+
to { opacity: 1; transform: translateY(0); }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.repo-card {
|
|
359
|
+
animation: fadeIn 0.8s ease-out forwards;
|
|
360
|
+
}
|
|
361
|
+
</style>
|
|
362
|
+
</head>
|
|
363
|
+
<body>
|
|
364
|
+
<header>
|
|
365
|
+
<div class="header-left">
|
|
366
|
+
<svg height="32" viewBox="0 0 16 16" width="32" class="logo-icon"><path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.27-.82 2.15 0 3.07 1.87 3.75 3.65 3.95-.29.25-.54.73-.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path></svg>
|
|
367
|
+
<h1>Commit Pulse</h1>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="badge highlight">
|
|
370
|
+
<span>Repository Ecosystem</span>
|
|
371
|
+
</div>
|
|
372
|
+
</header>
|
|
373
|
+
|
|
374
|
+
<div class="stats-grid" id="stats-grid"></div>
|
|
375
|
+
|
|
376
|
+
<footer>
|
|
377
|
+
© 2026 Commit Pulse • High-Fidelity Ecosystem Analytics
|
|
378
|
+
</footer>
|
|
379
|
+
|
|
380
|
+
<script>
|
|
381
|
+
const data = __STATS_DATA__;
|
|
382
|
+
|
|
383
|
+
function getIntensity(count) {
|
|
384
|
+
if (count === 0) return '';
|
|
385
|
+
if (count < 3) return 'intensity-1';
|
|
386
|
+
if (count < 6) return 'intensity-2';
|
|
387
|
+
if (count < 10) return 'intensity-3';
|
|
388
|
+
return 'intensity-4';
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function createHeatmap(repo) {
|
|
392
|
+
const wrapper = document.createElement('div');
|
|
393
|
+
wrapper.className = 'heatmap-wrapper';
|
|
394
|
+
|
|
395
|
+
const start = new Date(repo.first_commit);
|
|
396
|
+
const end = new Date(repo.last_commit);
|
|
397
|
+
|
|
398
|
+
// Start from the first day of the first commit month
|
|
399
|
+
const current = new Date(start.getFullYear(), start.getMonth(), 1);
|
|
400
|
+
// End at the last day of the last commit month
|
|
401
|
+
const lastDay = new Date(end.getFullYear(), end.getMonth() + 1, 0);
|
|
402
|
+
|
|
403
|
+
const months = {};
|
|
404
|
+
|
|
405
|
+
while (current <= lastDay) {
|
|
406
|
+
const monthKey = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, '0')}`;
|
|
407
|
+
if (!months[monthKey]) {
|
|
408
|
+
months[monthKey] = {
|
|
409
|
+
name: current.toLocaleString('default', { month: 'short' }),
|
|
410
|
+
year: current.getFullYear(),
|
|
411
|
+
days: []
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const dateKey = current.toISOString().split('T')[0];
|
|
416
|
+
const count = repo.heatmap[dateKey] || 0;
|
|
417
|
+
|
|
418
|
+
months[monthKey].days.push({
|
|
419
|
+
date: dateKey,
|
|
420
|
+
readable: current.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }),
|
|
421
|
+
count: count
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
current.setDate(current.getDate() + 1);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
Object.values(months).forEach(m => {
|
|
428
|
+
const monthGroup = document.createElement('div');
|
|
429
|
+
monthGroup.className = 'heatmap-month-group';
|
|
430
|
+
|
|
431
|
+
const monthLabel = document.createElement('div');
|
|
432
|
+
monthLabel.className = 'month-name';
|
|
433
|
+
monthLabel.innerText = `${m.name} ${m.year}`;
|
|
434
|
+
monthGroup.appendChild(monthLabel);
|
|
435
|
+
|
|
436
|
+
const grid = document.createElement('div');
|
|
437
|
+
grid.className = 'heatmap-grid';
|
|
438
|
+
|
|
439
|
+
m.days.forEach(day => {
|
|
440
|
+
const cell = document.createElement('div');
|
|
441
|
+
cell.className = `heatmap-cell ${getIntensity(day.count)}`;
|
|
442
|
+
cell.title = `${day.count} commits on ${day.readable}`;
|
|
443
|
+
grid.appendChild(cell);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Pad the end of the month to complete the column (total cells must be multiple of 7)
|
|
447
|
+
const totalCells = m.days.length;
|
|
448
|
+
const remaining = (7 - (totalCells % 7)) % 7;
|
|
449
|
+
for (let i = 0; i < remaining; i++) {
|
|
450
|
+
const empty = document.createElement('div');
|
|
451
|
+
empty.className = 'heatmap-cell empty';
|
|
452
|
+
grid.appendChild(empty);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
monthGroup.appendChild(grid);
|
|
456
|
+
wrapper.appendChild(monthGroup);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
return wrapper;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function render() {
|
|
463
|
+
const grid = document.getElementById('stats-grid');
|
|
464
|
+
data.forEach((repo, index) => {
|
|
465
|
+
const card = document.createElement('div');
|
|
466
|
+
card.className = 'repo-card';
|
|
467
|
+
card.style.animationDelay = `${index * 0.15}s`;
|
|
468
|
+
|
|
469
|
+
card.innerHTML = `
|
|
470
|
+
<div class="repo-title-row">
|
|
471
|
+
<div class="repo-name-group">
|
|
472
|
+
<svg height="24" viewBox="0 0 16 16" width="24" fill="var(--text-dim)"><path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.27-.82 2.15 0 3.07 1.87 3.75 3.65 3.95-.29.25-.54.73-.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path></svg>
|
|
473
|
+
<span class="repo-name">${repo.name}</span>
|
|
474
|
+
</div>
|
|
475
|
+
<div class="commit-pill">${repo.total_commits} Total Commits</div>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<div class="repo-meta-badges">
|
|
479
|
+
<div class="badge">
|
|
480
|
+
<span style="opacity: 0.5">Timeline:</span>
|
|
481
|
+
${repo.first_commit} to ${repo.last_commit}
|
|
482
|
+
</div>
|
|
483
|
+
<div class="badge highlight">
|
|
484
|
+
<span style="opacity: 0.5">Peak Hour:</span>
|
|
485
|
+
${repo.peak_hour}
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<div class="dashboard-row">
|
|
490
|
+
<div class="heatmap-container">
|
|
491
|
+
<div class="section-label">
|
|
492
|
+
<span>Commit Contributions (Full History)</span>
|
|
493
|
+
<div class="legend">
|
|
494
|
+
<span>Less</span>
|
|
495
|
+
<div class="legend-box" style="background: var(--h-0)"></div>
|
|
496
|
+
<div class="legend-box intensity-1"></div>
|
|
497
|
+
<div class="legend-box intensity-2"></div>
|
|
498
|
+
<div class="legend-box intensity-3"></div>
|
|
499
|
+
<div class="legend-box intensity-4"></div>
|
|
500
|
+
<span>More</span>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
<div class="heatmap-wrapper" id="heatmap-${index}"></div>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
|
|
507
|
+
<div class="contributor-panel">
|
|
508
|
+
<span class="section-label">Top Engineering Impact</span>
|
|
509
|
+
<div class="contributor-list">
|
|
510
|
+
${repo.contributors.map(c => `
|
|
511
|
+
<div class="contributor-item">
|
|
512
|
+
<div class="contributor-info">
|
|
513
|
+
<img src="${c.avatar}" class="avatar" onerror="this.src='https://www.gravatar.com/avatar/0?d=mp'">
|
|
514
|
+
<span class="contributor-name">${c.name}</span>
|
|
515
|
+
</div>
|
|
516
|
+
<div class="commit-pill" style="background: rgba(255,255,255,0.05); color: var(--text-dim); border: 1px solid var(--border)">
|
|
517
|
+
${c.commits}
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
`).join('')}
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
`;
|
|
524
|
+
grid.appendChild(card);
|
|
525
|
+
document.getElementById(`heatmap-${index}`).appendChild(createHeatmap(repo));
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
render();
|
|
529
|
+
</script>
|
|
530
|
+
</body>
|
|
531
|
+
</html>
|
|
532
|
+
""".replace("__STATS_DATA__", json.dumps(self.stats_data))
|
|
533
|
+
|
|
534
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
535
|
+
f.write(html_template)
|
|
536
|
+
|
|
537
|
+
return os.path.abspath(output_path)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: commitpulse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Premium Git repository analytics and dashboard generator
|
|
5
|
+
Author: Antigravity AI
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests>=2.31.0
|
|
9
|
+
|
|
10
|
+
# Commit Pulse 🚀
|
|
11
|
+
|
|
12
|
+
Commit Pulse is a powerful, locally-first CLI tool that generates high-fidelity engineering dashboards for your Git repositories. It analyzes your development ecosystem, visualizes commit intensity heatmaps, and identifies top contributors using real GitHub profiles.
|
|
13
|
+
|
|
14
|
+
## ✨ Key Features
|
|
15
|
+
- **Zero-PAT Analysis**: Analyzes local `.git` metadata directly. No Personal Access Tokens or cloud APIs required for core stats.
|
|
16
|
+
- **Accurate Project Timelines**: Dynamic contribution grids that span from the repository's inception to the **last official commit**.
|
|
17
|
+
- **GitHub Profile Integration**: Automatically fetches actual contributor avatars via the public GitHub Search API.
|
|
18
|
+
- **Master Scan**: A recursive drive scanner that aggregates stats from every project on your machine into a single interactive dashboard.
|
|
19
|
+
- **Premium Aesthetics**: Glassmorphic UI with dark mode, smooth animations, and official GitHub iconography.
|
|
20
|
+
|
|
21
|
+
## 📦 Installation
|
|
22
|
+
|
|
23
|
+
### 1. Local (Developer Mode)
|
|
24
|
+
If you have the source code locally:
|
|
25
|
+
```bash
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. Direct from GitHub
|
|
30
|
+
Anyone can install your tool directly if the repo is public:
|
|
31
|
+
```bash
|
|
32
|
+
pip install git+https://github.com/YOUR_USERNAME/commitpulse.git
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 3. Global Command
|
|
36
|
+
Once installed, the `commitpulse` command is available everywhere in your terminal.
|
|
37
|
+
|
|
38
|
+
## 🚀 Usage
|
|
39
|
+
|
|
40
|
+
### Analyze the current repository
|
|
41
|
+
```bash
|
|
42
|
+
commitpulse
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Analyze all repositories on your computer (Master Scan)
|
|
46
|
+
```bash
|
|
47
|
+
commitpulse --scan
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Specify a target directory
|
|
51
|
+
```bash
|
|
52
|
+
commitpulse /path/to/projects
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 🛠️ Options
|
|
56
|
+
- `--scan`: Crawls subdirectories to find and aggregate all Git repositories.
|
|
57
|
+
- `--no-open`: Generates the dashboard without automatically opening it in the browser.
|
|
58
|
+
|
|
59
|
+
## 🚀 Deployment & Sharing
|
|
60
|
+
|
|
61
|
+
Commit Pulse offers two ways to view and share your results:
|
|
62
|
+
|
|
63
|
+
### 1. Local-First (Default)
|
|
64
|
+
Run the command to generate a self-contained, interactive HTML dashboard on your machine. Perfect for private analysis.
|
|
65
|
+
```bash
|
|
66
|
+
commitpulse --scan
|
|
67
|
+
```
|
|
68
|
+
*Output: `stats_dashboard.html`*
|
|
69
|
+
|
|
70
|
+
### 2. Commit Pulse Cloud (Global Sharing)
|
|
71
|
+
Ready to show the world? Use the `--publish` flag to host your dashboard on the cloud and get a unique, shareable URL.
|
|
72
|
+
```bash
|
|
73
|
+
commitpulse --publish
|
|
74
|
+
```
|
|
75
|
+
*Output: `https://commitpulse.app/v/your-unique-id`*
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 🛠️ Setting up your own Cloud Instance (Optional)
|
|
80
|
+
|
|
81
|
+
If you are hosting your own version of **Commit Pulse Cloud**:
|
|
82
|
+
|
|
83
|
+
1. **Neon DB**:
|
|
84
|
+
- Go to [Neon.tech](https://neon.tech) and create a free project.
|
|
85
|
+
- Copy your **Connection String** (e.g., `postgresql://user:pass@ep-cool-beach-123.us-east-2.aws.neon.tech/neondb?sslmode=require`).
|
|
86
|
+
2. **Environment Setup**:
|
|
87
|
+
- In the `commitpulse-cloud` directory, rename `.env.example` to `.env`.
|
|
88
|
+
- Paste your connection string into `DATABASE_URL`.
|
|
89
|
+
3. **Vercel Deployment**:
|
|
90
|
+
- Deploy the `commitpulse-cloud` folder to Vercel.
|
|
91
|
+
- Add the `DATABASE_URL` to your Vercel Environment Variables.
|
|
92
|
+
|
|
93
|
+
## 🌍 Distribution & PyPI Hosting
|
|
94
|
+
|
|
95
|
+
To share **Commit Pulse** with the global developer community via PyPI:
|
|
96
|
+
|
|
97
|
+
### 1. Prepare your Account
|
|
98
|
+
- Register at [pypi.org](https://pypi.org/).
|
|
99
|
+
- Generate an API Token in your account settings.
|
|
100
|
+
|
|
101
|
+
### 2. Build the Distribution
|
|
102
|
+
Ensure you have the latest build tools:
|
|
103
|
+
```bash
|
|
104
|
+
python -m pip install --upgrade build twine
|
|
105
|
+
```
|
|
106
|
+
Then build the package:
|
|
107
|
+
```bash
|
|
108
|
+
python -m build
|
|
109
|
+
```
|
|
110
|
+
This creates a `dist/` folder with your `.whl` and `.tar.gz` files.
|
|
111
|
+
|
|
112
|
+
### 3. Upload to PyPI
|
|
113
|
+
Use `twine` to securely upload your package:
|
|
114
|
+
```bash
|
|
115
|
+
python -m twine upload dist/*
|
|
116
|
+
```
|
|
117
|
+
- **Username**: `__token__`
|
|
118
|
+
- **Password**: `[Your API Token]`
|
|
119
|
+
|
|
120
|
+
Once uploaded, anyone can install it via:
|
|
121
|
+
```bash
|
|
122
|
+
pip install commitpulse
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
*Built with ❤️ for the development ecosystem.*
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
commitpulse/__init__.py
|
|
4
|
+
commitpulse/analyzer.py
|
|
5
|
+
commitpulse/main.py
|
|
6
|
+
commitpulse/renderer.py
|
|
7
|
+
commitpulse.egg-info/PKG-INFO
|
|
8
|
+
commitpulse.egg-info/SOURCES.txt
|
|
9
|
+
commitpulse.egg-info/dependency_links.txt
|
|
10
|
+
commitpulse.egg-info/entry_points.txt
|
|
11
|
+
commitpulse.egg-info/requires.txt
|
|
12
|
+
commitpulse.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.31.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
commitpulse
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "commitpulse"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Premium Git repository analytics and dashboard generator"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Antigravity AI" }
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.7"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"requests>=2.31.0"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
commitpulse = "commitpulse.main:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["."]
|
|
23
|
+
include = ["commitpulse*"]
|