Skip to content

Instantly share code, notes, and snippets.

@Godefroy
Last active February 6, 2026 17:53
Show Gist options
  • Select an option

  • Save Godefroy/d383f91cceced741ccc4d86843b66acb to your computer and use it in GitHub Desktop.

Select an option

Save Godefroy/d383f91cceced741ccc4d86843b66acb to your computer and use it in GitHub Desktop.
GitHub Stats Generator - Visualize your commits and lines of code over time

GitHub Stats Generator

Script Python pour générer des statistiques visuelles de vos contributions GitHub sur plusieurs années.

📊 Le script génère un graphique avec deux courbes : nombre de commits par mois et lignes ajoutées/supprimées par mois.

Fonctionnalités

  • Commits par mois : visualisation du nombre de commits
  • Lignes de code : additions et suppressions par mois
  • Multi-organisations : inclut les contributions sur tous les repos (personnels et organisations)
  • Configurable : période, utilisateur, fichier de sortie

Prérequis

  1. GitHub CLI installé et authentifié :

    # Installation (macOS)
    brew install gh
    
    # Authentification
    gh auth login
  2. matplotlib :

    pip install matplotlib

Usage

# Usage de base (utilisateur connecté, 3 dernières années)
python github_stats.py

# Spécifier un utilisateur
python github_stats.py --user octocat

# Analyser les 5 dernières années
python github_stats.py --years 5

# Spécifier le fichier de sortie
python github_stats.py --output mes_stats.png

# Mode rapide (sans les stats de lignes - beaucoup plus rapide)
python github_stats.py --no-lines

# Combiner les options
python github_stats.py -u octocat -y 2 -o octocat_stats.png

Options

Option Court Description Défaut
--user -u Nom d'utilisateur GitHub Utilisateur connecté
--years -y Nombre d'années à analyser 3
--output -o Fichier PNG de sortie github_stats.png
--no-lines Ne pas récupérer les stats de lignes False

Exemple de sortie

Utilisateur: Godefroy
Période: 2023-02-01 → aujourd'hui (3 ans)

Récupération des commits...
  1624 commits trouvés

Récupération des lignes de code...
  Page 17/17...

==================================================
RÉSUMÉ
==================================================
Total commits:      1,624
Lignes ajoutées:    +1,004,503
Lignes supprimées:  -432,573
Net:                +571,930 lignes

Top 5 mois (commits):
  2025-09: 233 commits
  2025-08: 132 commits
  2025-11: 131 commits
  2025-12: 113 commits
  2025-10: 94 commits
==================================================

Graphique sauvegardé: github_stats.png

Comment ça marche

Récupération des commits

Le script utilise l'API GraphQL de GitHub pour récupérer le calendrier de contributions :

{
  user(login: "USERNAME") {
    contributionsCollection(from: "...", to: "...") {
      contributionCalendar {
        weeks {
          contributionDays {
            contributionCount
            date
          }
        }
      }
    }
  }
}

L'API limite les requêtes à 1 an maximum, donc le script fait plusieurs requêtes pour couvrir la période demandée.

Récupération des lignes de code

Pour les statistiques de lignes ajoutées/supprimées, le script :

  1. Utilise l'API Search pour trouver tous les commits de l'utilisateur :

    GET /search/commits?q=author:USERNAME+author-date:>DATE
    
  2. Pour chaque commit, récupère les stats détaillées :

    GET /repos/OWNER/REPO/commits/SHA
    

    → Retourne stats.additions et stats.deletions

  3. Agrège les données par mois

Cette approche permet de capturer les contributions sur tous les repos (personnels, organisations, forks).

Notes

  • La récupération des stats de lignes peut prendre plusieurs minutes selon le nombre de commits
  • L'API GitHub a des limites de rate (5000 requêtes/heure), le script gère automatiquement la pagination
  • Utilisez --no-lines pour un aperçu rapide des commits uniquement

License

MIT

#!/usr/bin/env python3
"""
GitHub Stats - Statistiques de commits et lignes de code sur plusieurs années.
Usage:
python github_stats.py [--user USERNAME] [--years N] [--output FILE]
Requires:
- GitHub CLI (gh) authenticated
- matplotlib: pip install matplotlib
"""
import subprocess
import json
import argparse
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Optional
try:
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
except ImportError:
print("matplotlib non installé. Installez-le avec: pip install matplotlib")
exit(1)
def run_gh(args: list[str]) -> tuple[str, str]:
"""Exécute une commande gh et retourne stdout, stderr."""
result = subprocess.run(['gh'] + args, capture_output=True, text=True)
return result.stdout, result.stderr
def get_current_user() -> str:
"""Récupère le nom d'utilisateur GitHub connecté."""
output, _ = run_gh(['api', 'user', '--jq', '.login'])
return output.strip()
def get_commits_by_month(username: str, since_date: str) -> dict[str, int]:
"""
Récupère le nombre de commits par mois via l'API GraphQL.
L'API limite à 1 an par requête, donc on fait plusieurs requêtes.
"""
commits_by_month = defaultdict(int)
start = datetime.strptime(since_date, "%Y-%m-%d")
end = datetime.now()
current = start
while current < end:
year_end = min(current + timedelta(days=365), end)
query = f'''
{{
user(login: "{username}") {{
contributionsCollection(from: "{current.strftime("%Y-%m-%d")}T00:00:00Z", to: "{year_end.strftime("%Y-%m-%d")}T00:00:00Z") {{
contributionCalendar {{
weeks {{
contributionDays {{
contributionCount
date
}}
}}
}}
}}
}}
}}
'''
output, err = run_gh(['api', 'graphql', '-f', f'query={query}'])
if output.strip():
try:
data = json.loads(output)
weeks = data['data']['user']['contributionsCollection']['contributionCalendar']['weeks']
for week in weeks:
for day in week['contributionDays']:
month = day['date'][:7]
commits_by_month[month] += day['contributionCount']
except (json.JSONDecodeError, KeyError) as e:
print(f"Erreur parsing GraphQL: {e}")
current = year_end
return dict(commits_by_month)
def get_lines_by_month(username: str, since_date: str) -> dict[str, dict[str, int]]:
"""
Récupère les lignes ajoutées/supprimées par mois via l'API Search.
"""
lines_by_month = defaultdict(lambda: {'additions': 0, 'deletions': 0})
# Calculer le nombre de pages nécessaires
count_output, _ = run_gh([
'api',
f'search/commits?q=author:{username}+author-date:>{since_date}&per_page=1',
'--jq', '.total_count'
])
try:
total_commits = int(count_output.strip())
except ValueError:
total_commits = 0
pages = min((total_commits // 100) + 1, 10) # Max 10 pages (1000 commits) par sécurité
print(f"Récupération des stats de lignes ({total_commits} commits, {pages} pages)...")
for page in range(1, pages + 1):
print(f" Page {page}/{pages}...", end='\r')
output, _ = run_gh([
'api',
f'search/commits?q=author:{username}+author-date:>{since_date}&per_page=100&page={page}&sort=author-date',
'--jq', '.items[] | {sha, repo: .repository.full_name, date: .commit.author.date}'
])
if not output.strip():
break
for line in output.strip().split('\n'):
if not line:
continue
try:
item = json.loads(line)
sha = item['sha']
repo = item['repo']
date = item['date']
month = date[:7]
# Récupérer les stats du commit
stats_output, _ = run_gh([
'api',
f'repos/{repo}/commits/{sha}',
'--jq', '{additions: .stats.additions, deletions: .stats.deletions}'
])
if stats_output.strip():
stats = json.loads(stats_output)
lines_by_month[month]['additions'] += stats.get('additions', 0) or 0
lines_by_month[month]['deletions'] += stats.get('deletions', 0) or 0
except (json.JSONDecodeError, KeyError):
pass
print()
return dict(lines_by_month)
def generate_chart(
commits: dict[str, int],
lines: dict[str, dict[str, int]],
username: str,
output_file: str
) -> None:
"""Génère les graphiques et les sauvegarde."""
# Préparer les données
months = sorted(set(list(commits.keys()) + list(lines.keys())))
if not months:
print("Aucune donnée à afficher")
return
dates = [datetime.strptime(m, "%Y-%m") for m in months]
commit_counts = [commits.get(m, 0) for m in months]
additions = [lines.get(m, {}).get('additions', 0) for m in months]
deletions = [lines.get(m, {}).get('deletions', 0) for m in months]
# Créer la figure
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# Graphique 1: Commits par mois
ax1.bar(dates, commit_counts, width=25, color='#2196F3', alpha=0.8, edgecolor='#1565C0')
ax1.set_ylabel('Nombre de commits', fontsize=12)
ax1.set_title(f'Statistiques GitHub de {username}', fontsize=14, fontweight='bold')
ax1.grid(axis='y', alpha=0.3)
if commit_counts:
ax1.set_ylim(0, max(commit_counts) * 1.1)
# Annotations pour les mois importants
threshold = max(commit_counts) * 0.6 if commit_counts else 0
for date, count in zip(dates, commit_counts):
if count > threshold:
ax1.annotate(str(count), (date, count), textcoords="offset points",
xytext=(0, 5), ha='center', fontsize=8)
# Graphique 2: Lignes ajoutées/supprimées
ax2.bar(dates, additions, width=20, label='Lignes ajoutées',
color='#4CAF50', alpha=0.8, edgecolor='#2E7D32')
ax2.bar(dates, [-d for d in deletions], width=20, label='Lignes supprimées',
color='#F44336', alpha=0.8, edgecolor='#C62828')
ax2.set_ylabel('Lignes de code', fontsize=12)
ax2.set_xlabel('Mois', fontsize=12)
ax2.legend(loc='upper left')
ax2.grid(axis='y', alpha=0.3)
ax2.axhline(y=0, color='black', linewidth=0.5)
# Format axe X
ax2.xaxis.set_major_locator(mdates.MonthLocator(interval=3))
ax2.xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
plt.xticks(rotation=45, ha='right')
# Totaux
total_commits = sum(commit_counts)
total_additions = sum(additions)
total_deletions = sum(deletions)
textstr = f'Total: {total_commits:,} commits | +{total_additions:,} / -{total_deletions:,} lignes'
fig.text(0.5, 0.02, textstr, ha='center', fontsize=11,
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
plt.tight_layout()
plt.subplots_adjust(bottom=0.1)
plt.savefig(output_file, dpi=150, bbox_inches='tight')
print(f"Graphique sauvegardé: {output_file}")
def print_summary(commits: dict[str, int], lines: dict[str, dict[str, int]]) -> None:
"""Affiche un résumé des statistiques."""
total_commits = sum(commits.values())
total_additions = sum(l.get('additions', 0) for l in lines.values())
total_deletions = sum(l.get('deletions', 0) for l in lines.values())
print("\n" + "=" * 50)
print("RÉSUMÉ")
print("=" * 50)
print(f"Total commits: {total_commits:,}")
print(f"Lignes ajoutées: +{total_additions:,}")
print(f"Lignes supprimées: -{total_deletions:,}")
print(f"Net: {total_additions - total_deletions:+,} lignes")
if commits:
print("\nTop 5 mois (commits):")
top_months = sorted(commits.items(), key=lambda x: x[1], reverse=True)[:5]
for month, count in top_months:
print(f" {month}: {count} commits")
print("=" * 50)
def main():
parser = argparse.ArgumentParser(
description='Génère des statistiques GitHub (commits et lignes de code)'
)
parser.add_argument(
'--user', '-u',
help='Nom d\'utilisateur GitHub (par défaut: utilisateur connecté)'
)
parser.add_argument(
'--years', '-y',
type=int,
default=3,
help='Nombre d\'années à analyser (par défaut: 3)'
)
parser.add_argument(
'--output', '-o',
default='github_stats.png',
help='Fichier de sortie pour le graphique (par défaut: github_stats.png)'
)
parser.add_argument(
'--no-lines',
action='store_true',
help='Ne pas récupérer les stats de lignes (plus rapide)'
)
args = parser.parse_args()
# Vérifier l'authentification
status_output, _ = run_gh(['auth', 'status'])
if 'Logged in' not in status_output:
print("Erreur: Vous devez être connecté à GitHub CLI (gh auth login)")
exit(1)
# Récupérer l'utilisateur
username = args.user or get_current_user()
print(f"Utilisateur: {username}")
# Calculer la date de début
since_date = (datetime.now() - timedelta(days=365 * args.years)).strftime("%Y-%m-%d")
print(f"Période: {since_date} → aujourd'hui ({args.years} ans)")
# Récupérer les commits
print("\nRécupération des commits...")
commits = get_commits_by_month(username, since_date)
print(f" {sum(commits.values())} commits trouvés")
# Récupérer les lignes
lines = {}
if not args.no_lines:
print("\nRécupération des lignes de code...")
lines = get_lines_by_month(username, since_date)
# Afficher le résumé
print_summary(commits, lines)
# Générer le graphique
print("\nGénération du graphique...")
generate_chart(commits, lines, username, args.output)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment