Compare commits

..

3 Commits

12 changed files with 1303 additions and 59 deletions
+75
View File
@@ -80,6 +80,9 @@ $ git-velocity serve --port 8080
- GitHub App authentication
- Environment variable support
### 🔑 Required GitHub Token Permissions
See [Token Permissions](#-github-token-permissions) for detailed requirements.
## 🚀 Quick Start
### Installation
@@ -268,6 +271,78 @@ Git Velocity includes **95 hardcoded achievements** across 20 categories with mu
| ✂️ Comment Trimmer | Removed 50 outdated comment lines |
| 💀 Dead Code Hunter | Removed 500 outdated comment lines |
## 🔑 GitHub Token Permissions
Git Velocity requires specific GitHub API permissions to fetch repository data. Below are the required permissions for each authentication method.
### Personal Access Token (Classic)
When creating a classic Personal Access Token, select the following scopes:
| Scope | Required | Description |
|-------|----------|-------------|
| `repo` | ✅ Yes | Full access to private repositories (includes commits, PRs, issues) |
| `read:org` | ⚠️ If using org patterns | Required when using `pattern: "*"` to list organization repositories |
> **Note**: For public repositories only, the `public_repo` scope is sufficient instead of full `repo` access.
### Fine-Grained Personal Access Token (Recommended)
Fine-grained tokens provide more granular control. Configure the following permissions:
**Repository Permissions:**
| Permission | Access Level | Description |
|------------|--------------|-------------|
| Contents | Read | Access commit data and file contents |
| Pull requests | Read | Access PR data, reviews, and comments |
| Issues | Read | Access issue data and comments |
| Metadata | Read | Basic repository information (automatically included) |
**Account Permissions:**
| Permission | Access Level | Description |
|------------|--------------|-------------|
| Email addresses | Read | Access public email for user deduplication |
### GitHub App Authentication
When using GitHub App authentication, configure the following permissions:
**Repository Permissions:**
| Permission | Access Level | Description |
|------------|--------------|-------------|
| Contents | Read | Fetch commits and diffs |
| Pull requests | Read | Fetch PRs, reviews, and review comments |
| Issues | Read | Fetch issues and issue comments |
| Metadata | Read | Repository metadata (required) |
**Organization Permissions (if using org patterns):**
| Permission | Access Level | Description |
|------------|--------------|-------------|
| Members | Read | List organization repositories |
**Account Permissions:**
| Permission | Access Level | Description |
|------------|--------------|-------------|
| Email addresses | Read | User profile deduplication |
### GitHub Actions (GITHUB_TOKEN)
When running in GitHub Actions, the default `GITHUB_TOKEN` has sufficient permissions for repositories in the same organization/account. For cross-organization access, use a PAT or GitHub App.
### API Endpoints Used
Git Velocity uses the following GitHub REST API endpoints:
| Endpoint | Purpose |
|----------|---------|
| `GET /orgs/{org}/repos` | List repositories by organization |
| `GET /repos/{owner}/{repo}/commits` | Fetch commit history |
| `GET /repos/{owner}/{repo}/commits/{sha}` | Fetch commit details with diff |
| `GET /repos/{owner}/{repo}/pulls` | List pull requests |
| `GET /repos/{owner}/{repo}/pulls/{number}/reviews` | Fetch PR reviews |
| `GET /repos/{owner}/{repo}/issues` | List issues |
| `GET /users/{username}` | Fetch user profile information |
## ⚙️ Configuration
### Full Configuration Reference
+819
View File
@@ -0,0 +1,819 @@
<!doctype html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>How Scoring Works - Git Velocity</title>
<meta
name="description"
content="Learn how Git Velocity calculates scores, generates leaderboards, and awards achievements based on your GitHub activity."
/>
<meta name="keywords" content="git velocity, scoring, calculations, leaderboard, achievements, github metrics" />
<meta name="author" content="Lukasz Raczylo" />
<meta property="og:title" content="How Scoring Works - Git Velocity" />
<meta property="og:description" content="Understand the scoring system, point calculations, and achievement criteria in Git Velocity." />
<meta property="og:type" content="website" />
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class'
}
</script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
<style>
body { font-family: "Inter", sans-serif; }
code, pre { font-family: "JetBrains Mono", monospace; }
.theme-transition {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in-up { animation: fadeInUp 0.6s ease-out; }
.glass {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.dark .glass {
background: rgba(17, 24, 39, 0.7);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.gradient-text {
background: linear-gradient(135deg, #f472b6 0%, #c084fc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.dark .gradient-text {
background: linear-gradient(135deg, #f9a8d4 0%, #d8b4fe 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.shadow-modern { box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.1); }
.dark .shadow-modern { box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.4); }
html { scroll-behavior: smooth; }
</style>
<script>
if (localStorage.theme === "dark" || (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
</script>
</head>
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 theme-transition">
<!-- Navigation -->
<nav class="fixed w-full glass shadow-modern z-50 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="flex justify-between h-16 items-center">
<a href="index.html" class="flex items-center hover:opacity-80 transition-opacity duration-300">
<img src="git-velocity-logo.png" alt="Git Velocity" class="h-8 w-auto" />
</a>
<div class="hidden md:flex items-center space-x-1">
<a href="index.html" class="px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg font-medium transition-colors">Home</a>
<a href="#scoring" class="px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg font-medium transition-colors">Scoring</a>
<a href="#leaderboard" class="px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg font-medium transition-colors">Leaderboard</a>
<a href="#achievements" class="px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg font-medium transition-colors">Achievements</a>
<a href="#data-sources" class="px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg font-medium transition-colors">Data Sources</a>
</div>
<div class="flex items-center space-x-2">
<button id="theme-toggle" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 p-2 rounded-lg transition-colors" aria-label="Toggle theme">
<i class="fas fa-moon dark:hidden text-lg"></i>
<i class="fas fa-sun hidden dark:inline text-lg"></i>
</button>
<a href="https://github.com/lukaszraczylo/git-velocity" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 p-2 rounded-lg transition-colors" aria-label="View on GitHub">
<i class="fab fa-github text-lg"></i>
</a>
<button id="mobile-menu-toggle" class="md:hidden text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 p-2 rounded-lg transition-colors" aria-label="Toggle menu">
<i class="fas fa-bars text-lg" id="menu-open-icon"></i>
<i class="fas fa-times text-lg hidden" id="menu-close-icon"></i>
</button>
</div>
</div>
</div>
<div id="mobile-menu" class="hidden md:hidden border-t border-gray-200 dark:border-gray-700">
<div class="px-4 py-3 space-y-1 bg-white dark:bg-gray-800">
<a href="index.html" class="block px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg font-medium">Home</a>
<a href="#scoring" class="block px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg font-medium">Scoring</a>
<a href="#leaderboard" class="block px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg font-medium">Leaderboard</a>
<a href="#achievements" class="block px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg font-medium">Achievements</a>
<a href="#data-sources" class="block px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg font-medium">Data Sources</a>
</div>
</div>
</nav>
<!-- Hero Section -->
<section class="relative pt-24 sm:pt-32 pb-12 sm:pb-16 overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-pink-50 via-purple-50 to-indigo-50 dark:from-gray-900 dark:via-pink-900/20 dark:to-purple-900/20 theme-transition"></div>
<div class="relative max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center">
<h1 class="text-3xl sm:text-4xl md:text-5xl font-bold text-gray-900 dark:text-gray-100 mb-4 sm:mb-6 leading-tight animate-fade-in-up">
How <span class="gradient-text">Scoring</span> Works
</h1>
<p class="text-base sm:text-lg md:text-xl text-gray-600 dark:text-gray-300 mb-8 max-w-2xl mx-auto leading-relaxed px-4 animate-fade-in-up" style="animation-delay: 0.1s;">
Understanding the point system, leaderboard rankings, and achievement criteria that power Git Velocity.
</p>
</div>
</div>
</section>
<!-- Overview Section -->
<section class="py-12 bg-white dark:bg-gray-900 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="max-w-4xl mx-auto">
<div class="glass p-6 rounded-xl mb-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center">
<i class="fas fa-info-circle mr-3 text-blue-500"></i>
Overview
</h2>
<p class="text-gray-600 dark:text-gray-400 mb-4">
Git Velocity calculates developer contributions by analyzing GitHub activity across configured repositories.
The scoring system is designed to encourage well-rounded contributions including code commits, pull requests,
code reviews, and collaboration.
</p>
<div class="grid sm:grid-cols-3 gap-4 mt-6">
<div class="text-center p-4 bg-pink-50 dark:bg-pink-900/20 rounded-lg">
<i class="fas fa-calculator text-pink-500 text-2xl mb-2"></i>
<h3 class="font-medium text-gray-900 dark:text-gray-100">Point-Based</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Activities earn configurable points</p>
</div>
<div class="text-center p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<i class="fas fa-layer-group text-purple-500 text-2xl mb-2"></i>
<h3 class="font-medium text-gray-900 dark:text-gray-100">Aggregated</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Combined across all repositories</p>
</div>
<div class="text-center p-4 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg">
<i class="fas fa-trophy text-indigo-500 text-2xl mb-2"></i>
<h3 class="font-medium text-gray-900 dark:text-gray-100">Achievement-Driven</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">Unlock badges for milestones</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Scoring Section -->
<section id="scoring" class="py-12 sm:py-16 bg-gray-50 dark:bg-gray-800 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Point Calculations</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">How each activity contributes to your score</p>
</div>
<div class="max-w-4xl mx-auto space-y-6">
<!-- Score Formula -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-function mr-2 text-pink-500"></i>
Score Formula
</h3>
<div class="bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto mb-4">
<pre class="text-sm"><code>Total Score = Commits + Line Changes + PRs + Reviews + Comments + Response Bonus + Out of Hours
Where:
Commits = commit_count × 10 points
Line Changes = (lines_added × 0.1) + (lines_deleted × 0.05) points
PRs = (PRs_opened × 25) + (PRs_merged × 50) points
Reviews = reviews_given × 30 points
Comments = review_comments × 5 points
Response = bonus for fast review response (0-50 points)
Out of Hours = commits outside 9am-5pm × 2 points</code></pre>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400">
<i class="fas fa-info-circle mr-1"></i>
All point values are configurable in your <code class="text-pink-600 dark:text-pink-400">.git-velocity.yaml</code> file.
</p>
</div>
<!-- Default Point Values -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-coins mr-2 text-yellow-500"></i>
Default Point Values
</h3>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="text-left py-3 text-gray-600 dark:text-gray-400">Activity</th>
<th class="text-left py-3 text-gray-600 dark:text-gray-400">Points</th>
<th class="text-left py-3 text-gray-600 dark:text-gray-400">Description</th>
</tr>
</thead>
<tbody class="text-gray-700 dark:text-gray-300">
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-3"><i class="fas fa-code-commit text-pink-500 mr-2"></i>Commit</td>
<td class="py-3 font-mono text-pink-600 dark:text-pink-400">10</td>
<td class="py-3">Per commit pushed</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-3"><i class="fas fa-flask text-green-500 mr-2"></i>Commit with Tests</td>
<td class="py-3 font-mono text-pink-600 dark:text-pink-400">15</td>
<td class="py-3">Commit that includes test files</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-3"><i class="fas fa-plus text-blue-500 mr-2"></i>Lines Added</td>
<td class="py-3 font-mono text-pink-600 dark:text-pink-400">0.1</td>
<td class="py-3">Per meaningful line added</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-3"><i class="fas fa-minus text-red-500 mr-2"></i>Lines Deleted</td>
<td class="py-3 font-mono text-pink-600 dark:text-pink-400">0.05</td>
<td class="py-3">Per meaningful line removed</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-3"><i class="fas fa-code-pull-request text-purple-500 mr-2"></i>PR Opened</td>
<td class="py-3 font-mono text-pink-600 dark:text-pink-400">25</td>
<td class="py-3">Per pull request created</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-3"><i class="fas fa-code-merge text-indigo-500 mr-2"></i>PR Merged</td>
<td class="py-3 font-mono text-pink-600 dark:text-pink-400">50</td>
<td class="py-3">Per pull request merged</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-3"><i class="fas fa-eye text-cyan-500 mr-2"></i>PR Reviewed</td>
<td class="py-3 font-mono text-pink-600 dark:text-pink-400">30</td>
<td class="py-3">Per PR review submitted</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-3"><i class="fas fa-comment text-orange-500 mr-2"></i>Review Comment</td>
<td class="py-3 font-mono text-pink-600 dark:text-pink-400">5</td>
<td class="py-3">Per comment on PR reviews</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-3"><i class="fas fa-bolt text-yellow-500 mr-2"></i>Fast Review (&lt;1h)</td>
<td class="py-3 font-mono text-pink-600 dark:text-pink-400">50</td>
<td class="py-3">Bonus for average response under 1 hour</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-3"><i class="fas fa-stopwatch text-yellow-500 mr-2"></i>Fast Review (&lt;4h)</td>
<td class="py-3 font-mono text-pink-600 dark:text-pink-400">25</td>
<td class="py-3">Bonus for average response under 4 hours</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-3"><i class="fas fa-clock text-yellow-500 mr-2"></i>Fast Review (&lt;24h)</td>
<td class="py-3 font-mono text-pink-600 dark:text-pink-400">10</td>
<td class="py-3">Bonus for average response under 24 hours</td>
</tr>
<tr>
<td class="py-3"><i class="fas fa-moon text-gray-500 mr-2"></i>Out of Hours</td>
<td class="py-3 font-mono text-pink-600 dark:text-pink-400">2</td>
<td class="py-3">Per commit outside 9am-5pm</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Meaningful Lines -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-filter mr-2 text-green-500"></i>
Meaningful Lines
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
By default, Git Velocity uses <strong>meaningful lines</strong> instead of raw line counts.
This filters out noise and rewards actual code contributions:
</p>
<div class="grid sm:grid-cols-2 gap-4">
<div class="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h4 class="font-medium text-green-700 dark:text-green-400 mb-2">
<i class="fas fa-check mr-2"></i>Counted as Meaningful
</h4>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li>Actual code logic</li>
<li>Function definitions</li>
<li>Variable declarations</li>
<li>Import statements</li>
</ul>
</div>
<div class="p-4 bg-red-50 dark:bg-red-900/20 rounded-lg">
<h4 class="font-medium text-red-700 dark:text-red-400 mb-2">
<i class="fas fa-times mr-2"></i>Filtered Out
</h4>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li>Empty lines / whitespace</li>
<li>Single-line comments</li>
<li>Multi-line comment blocks</li>
<li>Documentation strings</li>
</ul>
</div>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-4">
<i class="fas fa-cog mr-1"></i>
Disable with <code class="text-pink-600 dark:text-pink-400">use_meaningful_lines: false</code> in config to use raw line counts.
</p>
</div>
</div>
</div>
</section>
<!-- Leaderboard Section -->
<section id="leaderboard" class="py-12 sm:py-16 bg-white dark:bg-gray-900 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Leaderboard Rankings</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">How positions are determined</p>
</div>
<div class="max-w-4xl mx-auto space-y-6">
<!-- Ranking Process -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-list-ol mr-2 text-purple-500"></i>
Ranking Process
</h3>
<ol class="space-y-4">
<li class="flex items-start gap-3">
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-pink-100 dark:bg-pink-900/30 flex items-center justify-center text-pink-600 dark:text-pink-400 font-bold">1</span>
<div>
<h4 class="font-medium text-gray-900 dark:text-gray-100">Aggregate Across Repos</h4>
<p class="text-sm text-gray-600 dark:text-gray-400">Metrics from all configured repositories are combined per contributor</p>
</div>
</li>
<li class="flex items-start gap-3">
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center text-purple-600 dark:text-purple-400 font-bold">2</span>
<div>
<h4 class="font-medium text-gray-900 dark:text-gray-100">Calculate Total Score</h4>
<p class="text-sm text-gray-600 dark:text-gray-400">Apply point values to each activity type and sum the breakdown</p>
</div>
</li>
<li class="flex items-start gap-3">
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 font-bold">3</span>
<div>
<h4 class="font-medium text-gray-900 dark:text-gray-100">Sort by Score</h4>
<p class="text-sm text-gray-600 dark:text-gray-400">Contributors are sorted in descending order by total score</p>
</div>
</li>
<li class="flex items-start gap-3">
<span class="flex-shrink-0 w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 font-bold">4</span>
<div>
<h4 class="font-medium text-gray-900 dark:text-gray-100">Assign Ranks & Percentiles</h4>
<p class="text-sm text-gray-600 dark:text-gray-400">Each contributor receives a rank (1st, 2nd...) and percentile position</p>
</div>
</li>
</ol>
</div>
<!-- Top Categories -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-medal mr-2 text-yellow-500"></i>
Top Achievers
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
Git Velocity tracks top performers in each category:
</p>
<div class="grid sm:grid-cols-2 gap-4">
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div class="flex items-center gap-2 mb-2">
<i class="fas fa-trophy text-yellow-500"></i>
<span class="font-medium text-gray-900 dark:text-gray-100">Overall Leader</span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400">Highest total score</p>
</div>
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div class="flex items-center gap-2 mb-2">
<i class="fas fa-code-commit text-pink-500"></i>
<span class="font-medium text-gray-900 dark:text-gray-100">Top Committer</span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400">Most commits</p>
</div>
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div class="flex items-center gap-2 mb-2">
<i class="fas fa-eye text-purple-500"></i>
<span class="font-medium text-gray-900 dark:text-gray-100">Top Reviewer</span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400">Most reviews given</p>
</div>
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div class="flex items-center gap-2 mb-2">
<i class="fas fa-code-pull-request text-indigo-500"></i>
<span class="font-medium text-gray-900 dark:text-gray-100">Top PR Author</span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400">Most PRs opened</p>
</div>
</div>
</div>
<!-- Team Scoring -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-users mr-2 text-blue-500"></i>
Team Scoring
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
When teams are configured, Git Velocity calculates team metrics:
</p>
<ul class="space-y-2 text-gray-600 dark:text-gray-400">
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Total Team Score:</strong> Sum of all member scores</li>
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Average Score:</strong> Total score / number of members</li>
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Member Breakdown:</strong> Individual scores and achievements per team member</li>
</ul>
</div>
</div>
</div>
</section>
<!-- Achievements Section -->
<section id="achievements" class="py-12 sm:py-16 bg-gray-50 dark:bg-gray-800 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Achievement System</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">95 achievements across 22 categories with tiered progression</p>
</div>
<div class="max-w-4xl mx-auto space-y-6">
<!-- Achievement Categories -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-trophy mr-2 text-yellow-500"></i>
Achievement Categories
</h3>
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Commits -->
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">
<i class="fas fa-code-commit text-pink-500 mr-2"></i>Commits
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Tiers: 1, 10, 50, 100, 500, 1000</p>
<div class="text-xs text-gray-600 dark:text-gray-400">
First Steps → Getting Started → Contributor → Committed → Code Machine → Code Warrior
</div>
</div>
<!-- PRs Opened -->
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">
<i class="fas fa-code-pull-request text-purple-500 mr-2"></i>PRs Opened
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Tiers: 1, 10, 25, 50, 100, 250</p>
<div class="text-xs text-gray-600 dark:text-gray-400">
PR Pioneer → PR Regular → PR Pro → Merge Master → PR Champion → PR Legend
</div>
</div>
<!-- Reviews -->
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">
<i class="fas fa-eye text-indigo-500 mr-2"></i>Reviews Given
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Tiers: 1, 10, 25, 50, 100, 250</p>
<div class="text-xs text-gray-600 dark:text-gray-400">
First Review → Reviewer → Review Regular → Review Expert → Review Guru → Review Master
</div>
</div>
<!-- Review Comments -->
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">
<i class="fas fa-comment text-blue-500 mr-2"></i>Review Comments
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Tiers: 10, 50, 100, 250, 500</p>
<div class="text-xs text-gray-600 dark:text-gray-400">
Commentator → Feedback Giver → Code Critic → Feedback Expert → Comment Champion
</div>
</div>
<!-- Lines Added -->
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">
<i class="fas fa-plus text-green-500 mr-2"></i>Lines Added
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Tiers: 100, 1K, 5K, 10K, 50K</p>
<div class="text-xs text-gray-600 dark:text-gray-400">
First Hundred → Thousand Lines → Five Thousand → Ten Thousand → Code Mountain
</div>
</div>
<!-- Lines Deleted -->
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">
<i class="fas fa-minus text-red-500 mr-2"></i>Lines Deleted
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Tiers: 100, 500, 1K, 5K, 10K</p>
<div class="text-xs text-gray-600 dark:text-gray-400">
Tidying Up → Spring Cleaning → Code Cleaner → Refactoring Hero → Deletion Master
</div>
</div>
<!-- Response Time -->
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">
<i class="fas fa-bolt text-yellow-500 mr-2"></i>Response Time
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Tiers: &lt;24h, &lt;4h, &lt;1h</p>
<div class="text-xs text-gray-600 dark:text-gray-400">
Same Day Reviewer → Quick Responder → Speed Demon
</div>
</div>
<!-- Streaks -->
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">
<i class="fas fa-fire text-orange-500 mr-2"></i>Contribution Streaks
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Tiers: 3, 7, 14, 30 days</p>
<div class="text-xs text-gray-600 dark:text-gray-400">
Getting Rolling → Week Warrior → Two Week Streak → Month Master
</div>
</div>
<!-- Activity Patterns -->
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<h4 class="font-medium text-gray-900 dark:text-gray-100 mb-2">
<i class="fas fa-clock text-cyan-500 mr-2"></i>Activity Patterns
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">Early Bird, Night Owl, Weekend Warrior</p>
<div class="text-xs text-gray-600 dark:text-gray-400">
Commits at different times of day unlock special badges
</div>
</div>
</div>
</div>
<!-- Achievement Conditions -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-unlock mr-2 text-green-500"></i>
How Achievements Are Earned
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
Each achievement has a <strong>condition type</strong> and <strong>threshold</strong>.
When your metrics meet or exceed the threshold, the achievement is unlocked.
</p>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="text-left py-2 text-gray-600 dark:text-gray-400">Condition Type</th>
<th class="text-left py-2 text-gray-600 dark:text-gray-400">Metric Checked</th>
<th class="text-left py-2 text-gray-600 dark:text-gray-400">Comparison</th>
</tr>
</thead>
<tbody class="text-gray-700 dark:text-gray-300">
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2 font-mono text-xs">commit_count</td>
<td class="py-2">Total commits</td>
<td class="py-2">≥ threshold</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2 font-mono text-xs">pr_opened_count</td>
<td class="py-2">PRs opened</td>
<td class="py-2">≥ threshold</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2 font-mono text-xs">review_count</td>
<td class="py-2">Reviews given</td>
<td class="py-2">≥ threshold</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2 font-mono text-xs">avg_review_time_hours</td>
<td class="py-2">Average review response</td>
<td class="py-2">≤ threshold (lower is better)</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2 font-mono text-xs">longest_streak</td>
<td class="py-2">Consecutive active days</td>
<td class="py-2">≥ threshold</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2 font-mono text-xs">perfect_prs</td>
<td class="py-2">PRs with no changes requested</td>
<td class="py-2">≥ threshold</td>
</tr>
<tr>
<td class="py-2 font-mono text-xs">repo_count</td>
<td class="py-2">Repositories contributed to</td>
<td class="py-2">≥ threshold</td>
</tr>
</tbody>
</table>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-4">
<i class="fas fa-shield-halved mr-1"></i>
Achievement definitions are hardcoded and cannot be customized to prevent manipulation.
</p>
</div>
<!-- Tiered Progression -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-layer-group mr-2 text-purple-500"></i>
Tiered Progression
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
Most achievements have multiple tiers. As you progress, you unlock higher tiers:
</p>
<div class="flex flex-wrap gap-2 mb-4">
<span class="px-3 py-1 bg-gray-200 dark:bg-gray-700 rounded-full text-sm">Tier 1: 1</span>
<span class="px-3 py-1 bg-gray-300 dark:bg-gray-600 rounded-full text-sm">Tier 2: 10</span>
<span class="px-3 py-1 bg-green-200 dark:bg-green-900/50 rounded-full text-sm">Tier 3: 25</span>
<span class="px-3 py-1 bg-blue-200 dark:bg-blue-900/50 rounded-full text-sm">Tier 4: 50</span>
<span class="px-3 py-1 bg-purple-200 dark:bg-purple-900/50 rounded-full text-sm">Tier 5: 100</span>
<span class="px-3 py-1 bg-pink-200 dark:bg-pink-900/50 rounded-full text-sm">Tier 6: 250</span>
<span class="px-3 py-1 bg-orange-200 dark:bg-orange-900/50 rounded-full text-sm">Tier 7: 500</span>
<span class="px-3 py-1 bg-yellow-200 dark:bg-yellow-900/50 rounded-full text-sm font-medium">Tier 8+: 1000+</span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400">
The leaderboard shows only the highest tier achieved per category for each contributor.
</p>
</div>
</div>
</div>
</section>
<!-- Data Sources Section -->
<section id="data-sources" class="py-12 sm:py-16 bg-white dark:bg-gray-900 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Data Sources</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Where the metrics come from</p>
</div>
<div class="max-w-4xl mx-auto space-y-6">
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fab fa-github mr-2 text-gray-700 dark:text-gray-300"></i>
GitHub API Data
</h3>
<div class="grid sm:grid-cols-2 gap-6">
<div>
<h4 class="font-medium text-gray-800 dark:text-gray-200 mb-3">Commits</h4>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li><i class="fas fa-check text-green-500 mr-2"></i>SHA, message, timestamp</li>
<li><i class="fas fa-check text-green-500 mr-2"></i>Author (login, name, email)</li>
<li><i class="fas fa-check text-green-500 mr-2"></i>Additions, deletions, files changed</li>
<li><i class="fas fa-check text-green-500 mr-2"></i>Patch/diff for line analysis</li>
</ul>
</div>
<div>
<h4 class="font-medium text-gray-800 dark:text-gray-200 mb-3">Pull Requests</h4>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li><i class="fas fa-check text-green-500 mr-2"></i>State (open, merged, closed)</li>
<li><i class="fas fa-check text-green-500 mr-2"></i>Author and timestamps</li>
<li><i class="fas fa-check text-green-500 mr-2"></i>Size (additions, deletions)</li>
<li><i class="fas fa-check text-green-500 mr-2"></i>Comments count</li>
</ul>
</div>
<div>
<h4 class="font-medium text-gray-800 dark:text-gray-200 mb-3">Reviews</h4>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li><i class="fas fa-check text-green-500 mr-2"></i>Review state (approved, changes requested)</li>
<li><i class="fas fa-check text-green-500 mr-2"></i>Reviewer login</li>
<li><i class="fas fa-check text-green-500 mr-2"></i>Submission timestamp</li>
<li><i class="fas fa-check text-green-500 mr-2"></i>Comment count</li>
</ul>
</div>
<div>
<h4 class="font-medium text-gray-800 dark:text-gray-200 mb-3">User Profiles</h4>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li><i class="fas fa-check text-green-500 mr-2"></i>GitHub login (username)</li>
<li><i class="fas fa-check text-green-500 mr-2"></i>Display name</li>
<li><i class="fas fa-check text-green-500 mr-2"></i>Avatar URL</li>
<li><i class="fas fa-check text-green-500 mr-2"></i>Public email (for deduplication)</li>
</ul>
</div>
</div>
</div>
<!-- Calculated Metrics -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-calculator mr-2 text-blue-500"></i>
Derived Metrics
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
These metrics are calculated from raw data:
</p>
<div class="grid sm:grid-cols-2 gap-4 text-sm">
<div class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<strong class="text-gray-900 dark:text-gray-100">Meaningful Lines</strong>
<p class="text-gray-500 dark:text-gray-400">Parsed from commit diffs, filtering comments/whitespace</p>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<strong class="text-gray-900 dark:text-gray-100">Average Review Time</strong>
<p class="text-gray-500 dark:text-gray-400">Time between PR creation and first review</p>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<strong class="text-gray-900 dark:text-gray-100">Contribution Streaks</strong>
<p class="text-gray-500 dark:text-gray-400">Consecutive days with activity</p>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<strong class="text-gray-900 dark:text-gray-100">Perfect PRs</strong>
<p class="text-gray-500 dark:text-gray-400">PRs merged without "changes requested" reviews</p>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<strong class="text-gray-900 dark:text-gray-100">Out of Hours</strong>
<p class="text-gray-500 dark:text-gray-400">Commits outside 9am-5pm based on commit timestamp</p>
</div>
<div class="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<strong class="text-gray-900 dark:text-gray-100">Unique Reviewees</strong>
<p class="text-gray-500 dark:text-gray-400">Count of distinct PR authors reviewed</p>
</div>
</div>
</div>
<!-- Bot Filtering -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-robot mr-2 text-red-500"></i>
Bot Filtering
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
By default, bot activity is excluded from metrics. The following patterns are automatically filtered:
</p>
<div class="flex flex-wrap gap-2">
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm">*[bot]</code>
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm">dependabot*</code>
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm">renovate*</code>
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm">github-actions*</code>
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm">codecov*</code>
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm">snyk*</code>
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm">greenkeeper*</code>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-4">
<i class="fas fa-cog mr-1"></i>
Enable with <code class="text-pink-600 dark:text-pink-400">include_bots: true</code> or add custom patterns with <code class="text-pink-600 dark:text-pink-400">additional_bot_patterns</code>.
</p>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="py-16 sm:py-20 bg-gradient-to-br from-pink-500 via-purple-500 to-indigo-500 text-white">
<div class="max-w-4xl mx-auto px-4 sm:px-6 text-center">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold mb-4">Ready to Track Your Velocity?</h2>
<p class="text-lg sm:text-xl opacity-90 mb-8 max-w-2xl mx-auto">Now that you understand how scoring works, start analyzing your repositories.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a href="index.html#installation" class="bg-white text-purple-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-105">
<i class="fas fa-download mr-2"></i>Get Started
</a>
<a href="index.html#configuration" class="bg-transparent border-2 border-white text-white px-8 py-3 rounded-lg font-semibold hover:bg-white/10 transition-all duration-300">
<i class="fas fa-cog mr-2"></i>Configure Points
</a>
</div>
</div>
</section>
<!-- Footer -->
<footer class="py-8 bg-gray-100 dark:bg-gray-800 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="flex flex-col sm:flex-row justify-between items-center gap-4">
<div class="flex items-center">
<a href="index.html"><img src="git-velocity-logo.png" alt="Git Velocity" class="h-6 w-auto" /></a>
</div>
<div class="flex items-center gap-6">
<a href="https://github.com/lukaszraczylo/git-velocity" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">
<i class="fab fa-github text-xl"></i>
</a>
<a href="index.html" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 text-sm">
Home
</a>
<a href="index.html#configuration" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 text-sm">
Configuration
</a>
</div>
<p class="text-gray-500 dark:text-gray-400 text-sm">MIT License</p>
</div>
</div>
</footer>
<script>
// Theme toggle
document.getElementById('theme-toggle').addEventListener('click', function() {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.theme = 'light';
} else {
document.documentElement.classList.add('dark');
localStorage.theme = 'dark';
}
});
// Mobile menu toggle
document.getElementById('mobile-menu-toggle').addEventListener('click', function() {
const menu = document.getElementById('mobile-menu');
const openIcon = document.getElementById('menu-open-icon');
const closeIcon = document.getElementById('menu-close-icon');
menu.classList.toggle('hidden');
openIcon.classList.toggle('hidden');
closeIcon.classList.toggle('hidden');
});
// Close mobile menu when clicking a link
document.querySelectorAll('#mobile-menu a').forEach(link => {
link.addEventListener('click', () => {
document.getElementById('mobile-menu').classList.add('hidden');
document.getElementById('menu-open-icon').classList.remove('hidden');
document.getElementById('menu-close-icon').classList.add('hidden');
});
});
</script>
</body>
</html>
+175 -22
View File
@@ -112,35 +112,39 @@
<a href="#" class="flex items-center hover:opacity-80 transition-opacity duration-300">
<img src="git-velocity-logo.png" alt="Git Velocity" class="h-8 w-auto" />
</a>
<div class="hidden md:flex space-x-6">
<a href="#features" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Features</a>
<a href="#achievements" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Achievements</a>
<a href="#installation" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Installation</a>
<a href="#github-action" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">GitHub Action</a>
<a href="#configuration" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 font-medium">Configuration</a>
<div class="hidden lg:flex items-center space-x-1">
<a href="#features" class="px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg font-medium transition-colors">Features</a>
<a href="#achievements" class="px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg font-medium transition-colors">Achievements</a>
<a href="#installation" class="px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg font-medium transition-colors">Install</a>
<a href="#github-action" class="px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg font-medium transition-colors">GitHub Action</a>
<a href="#permissions" class="px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg font-medium transition-colors">Permissions</a>
<a href="#configuration" class="px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg font-medium transition-colors">Config</a>
<a href="calculations.html" class="px-3 py-2 text-sm text-pink-600 dark:text-pink-400 hover:text-pink-700 dark:hover:text-pink-300 hover:bg-pink-50 dark:hover:bg-pink-900/20 rounded-lg font-medium transition-colors">Scoring</a>
</div>
<div class="flex items-center space-x-4">
<button id="theme-toggle" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Toggle theme">
<i class="fas fa-moon dark:hidden text-xl"></i>
<i class="fas fa-sun hidden dark:inline text-xl"></i>
<div class="flex items-center space-x-2">
<button id="theme-toggle" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 p-2 rounded-lg transition-colors" aria-label="Toggle theme">
<i class="fas fa-moon dark:hidden text-lg"></i>
<i class="fas fa-sun hidden dark:inline text-lg"></i>
</button>
<a href="https://github.com/lukaszraczylo/git-velocity" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="View on GitHub">
<i class="fab fa-github text-xl"></i>
<a href="https://github.com/lukaszraczylo/git-velocity" target="_blank" class="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 p-2 rounded-lg transition-colors" aria-label="View on GitHub">
<i class="fab fa-github text-lg"></i>
</a>
<button id="mobile-menu-toggle" class="md:hidden text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 p-2 min-w-[44px] min-h-[44px] flex items-center justify-center" aria-label="Toggle menu">
<i class="fas fa-bars text-xl" id="menu-open-icon"></i>
<i class="fas fa-times text-xl hidden" id="menu-close-icon"></i>
<button id="mobile-menu-toggle" class="lg:hidden text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 p-2 rounded-lg transition-colors" aria-label="Toggle menu">
<i class="fas fa-bars text-lg" id="menu-open-icon"></i>
<i class="fas fa-times text-lg hidden" id="menu-close-icon"></i>
</button>
</div>
</div>
</div>
<div id="mobile-menu" class="hidden md:hidden border-t border-gray-200 dark:border-gray-700">
<div id="mobile-menu" class="hidden lg:hidden border-t border-gray-200 dark:border-gray-700">
<div class="px-4 py-3 space-y-1 bg-white dark:bg-gray-800">
<a href="#features" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Features</a>
<a href="#achievements" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Achievements</a>
<a href="#installation" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Installation</a>
<a href="#github-action" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">GitHub Action</a>
<a href="#configuration" class="block px-3 py-3 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded font-medium">Configuration</a>
<a href="#features" class="block px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg font-medium">Features</a>
<a href="#achievements" class="block px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg font-medium">Achievements</a>
<a href="#installation" class="block px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg font-medium">Installation</a>
<a href="#github-action" class="block px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg font-medium">GitHub Action</a>
<a href="#permissions" class="block px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg font-medium">Permissions</a>
<a href="#configuration" class="block px-3 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg font-medium">Configuration</a>
<a href="calculations.html" class="block px-3 py-2 text-pink-600 dark:text-pink-400 hover:text-pink-700 dark:hover:text-pink-300 hover:bg-pink-50 dark:hover:bg-pink-900/20 rounded-lg font-medium">How Scoring Works</a>
</div>
</div>
</nav>
@@ -613,8 +617,157 @@
</div>
</section>
<!-- Token Permissions Section -->
<section id="permissions" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">GitHub Token Permissions</h2>
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 px-4">Required permissions for each authentication method</p>
</div>
<div class="max-w-4xl mx-auto space-y-6">
<!-- Classic PAT -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-key mr-2 text-pink-500"></i>
Personal Access Token (Classic)
</h3>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="text-left py-2 text-gray-600 dark:text-gray-400">Scope</th>
<th class="text-left py-2 text-gray-600 dark:text-gray-400">Required</th>
<th class="text-left py-2 text-gray-600 dark:text-gray-400">Description</th>
</tr>
</thead>
<tbody class="text-gray-700 dark:text-gray-300">
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2"><code class="text-pink-600 dark:text-pink-400">repo</code></td>
<td class="py-2"><i class="fas fa-check text-green-500"></i> Yes</td>
<td class="py-2">Full access to private repositories</td>
</tr>
<tr>
<td class="py-2"><code class="text-pink-600 dark:text-pink-400">read:org</code></td>
<td class="py-2"><i class="fas fa-exclamation-triangle text-yellow-500"></i> If using patterns</td>
<td class="py-2">Required for <code>pattern: "*"</code> org listing</td>
</tr>
</tbody>
</table>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-3">
<i class="fas fa-info-circle mr-1"></i>
For public repos only, <code class="text-pink-600 dark:text-pink-400">public_repo</code> scope is sufficient.
</p>
</div>
<!-- Fine-Grained PAT -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-sliders mr-2 text-purple-500"></i>
Fine-Grained Token (Recommended)
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">More secure with granular control over permissions.</p>
<div class="grid sm:grid-cols-2 gap-4">
<div>
<h4 class="font-medium text-gray-800 dark:text-gray-200 mb-2">Repository Permissions</h4>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Contents:</strong> Read</li>
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Pull requests:</strong> Read</li>
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Issues:</strong> Read</li>
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Metadata:</strong> Read (auto)</li>
</ul>
</div>
<div>
<h4 class="font-medium text-gray-800 dark:text-gray-200 mb-2">Account Permissions</h4>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Email addresses:</strong> Read</li>
</ul>
</div>
</div>
</div>
<!-- GitHub App -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fab fa-github mr-2 text-indigo-500"></i>
GitHub App Authentication
</h3>
<div class="grid sm:grid-cols-2 gap-4">
<div>
<h4 class="font-medium text-gray-800 dark:text-gray-200 mb-2">Repository Permissions</h4>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Contents:</strong> Read</li>
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Pull requests:</strong> Read</li>
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Issues:</strong> Read</li>
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Metadata:</strong> Read</li>
</ul>
</div>
<div>
<h4 class="font-medium text-gray-800 dark:text-gray-200 mb-2">Organization Permissions</h4>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li><i class="fas fa-exclamation-triangle text-yellow-500 mr-2"></i><strong>Members:</strong> Read (if using patterns)</li>
</ul>
<h4 class="font-medium text-gray-800 dark:text-gray-200 mb-2 mt-3">Account Permissions</h4>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li><i class="fas fa-check text-green-500 mr-2"></i><strong>Email addresses:</strong> Read</li>
</ul>
</div>
</div>
</div>
<!-- API Endpoints -->
<div class="glass p-6 rounded-xl">
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-4 flex items-center text-xl">
<i class="fas fa-plug mr-2 text-green-500"></i>
API Endpoints Used
</h3>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-gray-200 dark:border-gray-700">
<th class="text-left py-2 text-gray-600 dark:text-gray-400">Endpoint</th>
<th class="text-left py-2 text-gray-600 dark:text-gray-400">Purpose</th>
</tr>
</thead>
<tbody class="text-gray-700 dark:text-gray-300">
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2"><code class="text-xs">GET /orgs/{org}/repos</code></td>
<td class="py-2">List organization repositories</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2"><code class="text-xs">GET /repos/{owner}/{repo}/commits</code></td>
<td class="py-2">Fetch commit history</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2"><code class="text-xs">GET /repos/{owner}/{repo}/commits/{sha}</code></td>
<td class="py-2">Fetch commit details with diff</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2"><code class="text-xs">GET /repos/{owner}/{repo}/pulls</code></td>
<td class="py-2">List pull requests</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2"><code class="text-xs">GET /repos/{owner}/{repo}/pulls/{n}/reviews</code></td>
<td class="py-2">Fetch PR reviews</td>
</tr>
<tr class="border-b border-gray-100 dark:border-gray-800">
<td class="py-2"><code class="text-xs">GET /repos/{owner}/{repo}/issues</code></td>
<td class="py-2">List issues</td>
</tr>
<tr>
<td class="py-2"><code class="text-xs">GET /users/{username}</code></td>
<td class="py-2">Fetch user profile</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</section>
<!-- Configuration Section -->
<section id="configuration" class="py-12 sm:py-16 md:py-20 bg-white dark:bg-gray-900 theme-transition">
<section id="configuration" class="py-12 sm:py-16 md:py-20 bg-gray-50 dark:bg-gray-800 theme-transition">
<div class="max-w-6xl mx-auto px-4 sm:px-6">
<div class="text-center mb-8 sm:mb-12">
<h2 class="text-2xl sm:text-3xl md:text-4xl font-bold text-gray-900 dark:text-gray-100 mb-3 sm:mb-4">Configuration</h2>
+1
View File
@@ -562,6 +562,7 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
return &models.GlobalMetrics{
Period: period,
Repositories: repositories,
Contributors: contributors,
Teams: teams,
TotalContributors: len(contributors),
TotalCommits: totalCommits,
+6 -5
View File
@@ -125,11 +125,12 @@ type TeamMetrics struct {
// GlobalMetrics holds metrics aggregated across all repositories
type GlobalMetrics struct {
Period Period `json:"period"`
Repositories []RepositoryMetrics `json:"repositories"`
Teams []TeamMetrics `json:"teams"`
Leaderboard []LeaderboardEntry `json:"leaderboard"`
TopAchievers map[string]string `json:"top_achievers"` // category -> login
Period Period `json:"period"`
Repositories []RepositoryMetrics `json:"repositories"`
Contributors []ContributorMetrics `json:"contributors"` // Aggregated across all repos
Teams []TeamMetrics `json:"teams"`
Leaderboard []LeaderboardEntry `json:"leaderboard"`
TopAchievers map[string]string `json:"top_achievers"` // category -> login
// Summary stats
TotalContributors int `json:"total_contributors"`
+1
View File
@@ -118,6 +118,7 @@ func (c *Calculator) Calculate(metrics *models.GlobalMetrics) *models.GlobalMetr
// Update the metrics
metrics.Leaderboard = leaderboard
metrics.TopAchievers = topAchievers
metrics.Contributors = contributors // Update global contributors with scored data
// Calculate per-repository scores (based on repo-specific metrics, not global)
for i := range metrics.Repositories {
+102
View File
@@ -98,6 +98,108 @@ func TestCalculator_BasicScoring(t *testing.T) {
assert.Equal(t, 840, entry.Score)
}
func TestCalculator_GlobalContributorsPopulatedWithScores(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
cfg.Scoring.Points = config.PointsConfig{
Commit: 10,
PROpened: 25,
PRMerged: 50,
PRReviewed: 30,
ReviewComment: 5,
LinesAdded: 0.1,
LinesDeleted: 0.05,
}
calc := NewCalculator(cfg)
// Contributor appears in multiple repos with different stats
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo1",
Contributors: []models.ContributorMetrics{
{
Login: "alice",
Name: "Alice",
CommitCount: 50,
PRsOpened: 5,
PRsMerged: 3,
},
{
Login: "bob",
Name: "Bob",
CommitCount: 20,
PRsOpened: 2,
},
},
},
{
FullName: "owner/repo2",
Contributors: []models.ContributorMetrics{
{
Login: "alice",
Name: "Alice",
CommitCount: 30, // Additional commits in second repo
PRsOpened: 3,
PRsMerged: 2,
},
},
},
},
}
result := calc.Calculate(metrics)
// Verify metrics.Contributors is populated
require.NotEmpty(t, result.Contributors, "metrics.Contributors should be populated")
require.Len(t, result.Contributors, 2, "Should have 2 unique contributors")
// Find alice in Contributors
var alice *models.ContributorMetrics
for i := range result.Contributors {
if result.Contributors[i].Login == "alice" {
alice = &result.Contributors[i]
break
}
}
require.NotNil(t, alice, "Alice should be in Contributors")
// Verify alice has AGGREGATED stats
assert.Equal(t, 80, alice.CommitCount, "Alice should have aggregated commits (50+30)")
assert.Equal(t, 8, alice.PRsOpened, "Alice should have aggregated PRs opened (5+3)")
assert.Equal(t, 5, alice.PRsMerged, "Alice should have aggregated PRs merged (3+2)")
// Verify alice has a calculated score with breakdown
assert.Greater(t, alice.Score.Total, 0, "Alice should have a calculated score")
assert.Greater(t, alice.Score.Breakdown.Commits, 0, "Score breakdown should have commits")
assert.Greater(t, alice.Score.Breakdown.PRs, 0, "Score breakdown should have PRs")
// Verify score calculation:
// Commits: 80 * 10 = 800
// PRs: 8 * 25 + 5 * 50 = 200 + 250 = 450
// Total: 800 + 450 = 1250
assert.Equal(t, 800, alice.Score.Breakdown.Commits, "Commit points should be 80 * 10 = 800")
assert.Equal(t, 450, alice.Score.Breakdown.PRs, "PR points should be 8*25 + 5*50 = 450")
assert.Equal(t, 1250, alice.Score.Total, "Total score should be 1250")
// Verify rank is assigned
assert.Equal(t, 1, alice.Score.Rank, "Alice should be rank 1 (highest scorer)")
// Verify bob also has scores
var bob *models.ContributorMetrics
for i := range result.Contributors {
if result.Contributors[i].Login == "bob" {
bob = &result.Contributors[i]
break
}
}
require.NotNil(t, bob, "Bob should be in Contributors")
assert.Greater(t, bob.Score.Total, 0, "Bob should have a calculated score")
assert.Equal(t, 2, bob.Score.Rank, "Bob should be rank 2")
}
func TestCalculator_FastReviewBonus(t *testing.T) {
t.Parallel()
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -8,7 +8,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script type="module" crossorigin src="./assets/index-R3eb927Q.js"></script>
<script type="module" crossorigin src="./assets/index-LBN7XWrH.js"></script>
<link rel="modulepreload" crossorigin href="./assets/chart-Bcjh2pZL.js">
<link rel="stylesheet" crossorigin href="./assets/index-8XjWwD9J.css">
</head>
+4 -12
View File
@@ -106,23 +106,15 @@ func (g *Generator) generateDataFiles(metrics *models.GlobalMetrics) error {
}
}
// Per-contributor data
contributorsSeen := make(map[string]bool)
// Per-contributor data (use aggregated global contributors, not per-repo)
contributorDir := filepath.Join(dataDir, "contributors")
if err := os.MkdirAll(contributorDir, 0750); err != nil {
return err
}
for _, repo := range metrics.Repositories {
for _, contributor := range repo.Contributors {
if contributorsSeen[contributor.Login] {
continue
}
contributorsSeen[contributor.Login] = true
if err := writeJSON(filepath.Join(contributorDir, contributor.Login+".json"), contributor); err != nil {
return err
}
for _, contributor := range metrics.Contributors {
if err := writeJSON(filepath.Join(contributorDir, contributor.Login+".json"), contributor); err != nil {
return err
}
}
+116 -16
View File
@@ -254,17 +254,14 @@ func TestGenerator_GenerateContributorJSON(t *testing.T) {
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
// Generator now uses global Contributors, not per-repo Contributors
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
Contributors: []models.ContributorMetrics{
{
Contributors: []models.ContributorMetrics{
{
Login: "john-doe",
Name: "John Doe",
CommitCount: 50,
PRsOpened: 10,
},
},
Login: "john-doe",
Name: "John Doe",
CommitCount: 50,
PRsOpened: 10,
},
},
}
@@ -287,33 +284,43 @@ func TestGenerator_GenerateContributorJSON(t *testing.T) {
assert.Equal(t, 10, result.PRsOpened)
}
func TestGenerator_ContributorDeduplication(t *testing.T) {
func TestGenerator_UsesGlobalContributorsNotPerRepo(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
// Same contributor in multiple repos
// Same contributor in multiple repos with different per-repo stats
// But GlobalMetrics.Contributors should have AGGREGATED stats
metrics := &models.GlobalMetrics{
// Per-repo data (used for repository-specific pages)
Repositories: []models.RepositoryMetrics{
{
Owner: "org",
Name: "repo1",
Contributors: []models.ContributorMetrics{
{Login: "user1", CommitCount: 50},
{Login: "user1", CommitCount: 50, PRsOpened: 5},
},
},
{
Owner: "org",
Name: "repo2",
Contributors: []models.ContributorMetrics{
{Login: "user1", CommitCount: 75}, // Same user, different count
{Login: "user1", CommitCount: 75, PRsOpened: 10}, // Same user, different count
},
},
},
// Global aggregated data (this is what the generator should use for contributor files)
Contributors: []models.ContributorMetrics{
{Login: "user1", CommitCount: 125, PRsOpened: 15}, // Sum: 50+75=125, 5+10=15
},
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Should only have one contributor file (first one seen)
// Contributor file should have AGGREGATED data from GlobalMetrics.Contributors
contributorPath := filepath.Join(tempDir, "data", "contributors", "user1.json")
data, err := os.ReadFile(contributorPath)
require.NoError(t, err)
@@ -322,8 +329,84 @@ func TestGenerator_ContributorDeduplication(t *testing.T) {
err = json.Unmarshal(data, &result)
require.NoError(t, err)
// Should be the first one (50 commits)
assert.Equal(t, 50, result.CommitCount)
// Should be the aggregated count (125 commits, 15 PRs), NOT 50 or 75
assert.Equal(t, 125, result.CommitCount, "Should use aggregated commits from GlobalMetrics.Contributors")
assert.Equal(t, 15, result.PRsOpened, "Should use aggregated PRs from GlobalMetrics.Contributors")
}
func TestGenerator_MultipleContributorsAcrossRepos(t *testing.T) {
tempDir := t.TempDir()
cfg := config.DefaultConfig()
gen, err := NewGenerator(tempDir, cfg)
require.NoError(t, err)
// Multiple contributors across multiple repos with aggregated global data
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
Owner: "org",
Name: "repo1",
Contributors: []models.ContributorMetrics{
{Login: "alice", CommitCount: 100, LinesAdded: 5000},
{Login: "bob", CommitCount: 50, LinesAdded: 2000},
},
},
{
Owner: "org",
Name: "repo2",
Contributors: []models.ContributorMetrics{
{Login: "alice", CommitCount: 50, LinesAdded: 3000},
{Login: "charlie", CommitCount: 75, LinesAdded: 4000},
},
},
},
// Aggregated global contributors
Contributors: []models.ContributorMetrics{
{Login: "alice", CommitCount: 150, LinesAdded: 8000}, // 100+50, 5000+3000
{Login: "bob", CommitCount: 50, LinesAdded: 2000}, // Only in repo1
{Login: "charlie", CommitCount: 75, LinesAdded: 4000}, // Only in repo2
},
}
err = gen.Generate(metrics)
require.NoError(t, err)
// Verify alice has aggregated data
alicePath := filepath.Join(tempDir, "data", "contributors", "alice.json")
aliceData, err := os.ReadFile(alicePath)
require.NoError(t, err)
var aliceResult models.ContributorMetrics
err = json.Unmarshal(aliceData, &aliceResult)
require.NoError(t, err)
assert.Equal(t, 150, aliceResult.CommitCount, "Alice should have aggregated commits")
assert.Equal(t, 8000, aliceResult.LinesAdded, "Alice should have aggregated lines added")
// Verify bob exists with his data
bobPath := filepath.Join(tempDir, "data", "contributors", "bob.json")
bobData, err := os.ReadFile(bobPath)
require.NoError(t, err)
var bobResult models.ContributorMetrics
err = json.Unmarshal(bobData, &bobResult)
require.NoError(t, err)
assert.Equal(t, 50, bobResult.CommitCount)
assert.Equal(t, 2000, bobResult.LinesAdded)
// Verify charlie exists with his data
charliePath := filepath.Join(tempDir, "data", "contributors", "charlie.json")
charlieData, err := os.ReadFile(charliePath)
require.NoError(t, err)
var charlieResult models.ContributorMetrics
err = json.Unmarshal(charlieData, &charlieResult)
require.NoError(t, err)
assert.Equal(t, 75, charlieResult.CommitCount)
assert.Equal(t, 4000, charlieResult.LinesAdded)
}
func TestGenerator_NoTeamsDoesNotCreateTeamDir(t *testing.T) {
@@ -466,6 +549,12 @@ func TestGenerator_GenerateWithFullMetrics(t *testing.T) {
},
},
},
// Global aggregated contributors (used for individual contributor files)
Contributors: []models.ContributorMetrics{
{Login: "alice", Name: "Alice", CommitCount: 150}, // 100+50 aggregated
{Login: "bob", Name: "Bob", CommitCount: 200}, // Only in repo1
{Login: "charlie", Name: "Charlie", CommitCount: 150}, // Only in repo2
},
Teams: []models.TeamMetrics{
{
Name: "Core Team",
@@ -499,4 +588,15 @@ func TestGenerator_GenerateWithFullMetrics(t *testing.T) {
_, err := os.Stat(path)
assert.NoError(t, err, "Expected file to exist: %s", path)
}
// Verify alice's file has aggregated data (150 commits, not 100 from first repo)
alicePath := filepath.Join(tempDir, "data", "contributors", "alice.json")
aliceData, err := os.ReadFile(alicePath)
require.NoError(t, err)
var aliceResult models.ContributorMetrics
err = json.Unmarshal(aliceData, &aliceResult)
require.NoError(t, err)
assert.Equal(t, 150, aliceResult.CommitCount, "Alice should have aggregated commits from global Contributors")
}
+2 -2
View File
@@ -226,10 +226,10 @@ const progressItems = computed(() => {
})
}
// Sort by progress (closest to completion first)
// Sort by progress descending (closest to next tier first - highest % complete)
results.sort((a, b) => b.progress - a.progress)
return results.slice(0, props.maxDisplay)
return results
})
// Get count of remaining achievements (all unearned across all types)