book-generator-orgmode/analyze_git_history.py
2025-10-30 18:49:31 +01:00

272 lines
9.3 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Script pour analyser l'historique Git des fichiers livre.org, intrigues.org et personnages.org
Génère un JSON détaillé des changements et un CSV récapitulatif
"""
import subprocess
import json
import csv
from datetime import datetime
from collections import defaultdict
import os
import sys
from typing import Dict, List, Tuple, Any
def run_git_command(args: List[str]) -> str:
"""Exécute une commande git et retourne la sortie"""
try:
result = subprocess.run(
['git'] + args,
capture_output=True,
text=True,
check=True
)
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Erreur lors de l'exécution de la commande git: {e}", file=sys.stderr)
sys.exit(1)
def get_file_history(filename: str) -> List[Dict[str, str]]:
"""Récupère l'historique Git d'un fichier"""
cmd = ['log', '--follow', '--pretty=format:%H|%an|%ae|%ad|%s', '--date=iso-strict', '--', filename]
output = run_git_command(cmd)
history = []
for line in output.strip().split('\n'):
if not line:
continue
parts = line.split('|', 4)
if len(parts) >= 5:
history.append({
'hash': parts[0],
'author': parts[1],
'email': parts[2],
'date': parts[3],
'message': parts[4]
})
return history
def get_file_content_at_commit(hash: str, filename: str) -> str:
"""Récupère le contenu d'un fichier à un commit donné"""
try:
result = subprocess.run(
['git', 'show', f'{hash}:{filename}'],
capture_output=True,
text=True,
check=True
)
return result.stdout
except subprocess.CalledProcessError:
return ""
def parse_diff(diff_output: str) -> Dict[str, Any]:
"""Parse la sortie de git diff pour extraire les changements"""
changes = {
'added_lines': [],
'removed_lines': [],
'modified_sections': []
}
current_file = None
current_block_start = None
in_header = True
for line in diff_output.split('\n'):
# Sauter la première ligne de diff
if line.startswith('diff --git'):
in_header = True
continue
if line.startswith('+++'):
in_header = False
continue
if line.startswith('---'):
continue
if in_header:
continue
# Nouveau bloc de changement
if line.startswith('@@'):
parts = line.split('@@')
if len(parts) >= 3:
# Extraire les numéros de ligne
line_info = parts[1].strip()
current_block_start = line_info
continue
# Ligne ajoutée
if line.startswith('+') and not line.startswith('+++'):
clean_line = line[1:] # Enlever le + au début
line_num = current_block_start if current_block_start else 'unknown'
changes['added_lines'].append({
'line': clean_line,
'context': current_block_start
})
# Ligne supprimée
elif line.startswith('-') and not line.startswith('---'):
clean_line = line[1:] # Enlever le - au début
changes['removed_lines'].append({
'line': clean_line,
'context': current_block_start
})
# Ligne inchangée (context)
elif line.startswith(' '):
continue
return changes
def get_changes_between_commits(commit_hash: str, prev_hash: str, filename: str) -> Dict[str, Any]:
"""Compare deux commits et retourne les changements"""
# Récupérer le diff
diff_cmd = ['diff', prev_hash, commit_hash, '--', filename]
diff_output = run_git_command(diff_cmd)
if not diff_output.strip():
return None
# Parser le diff
changes = parse_diff(diff_output)
# Ajouter des statistiques
changes['stats'] = {
'added_count': len(changes['added_lines']),
'removed_count': len(changes['removed_lines']),
'net_change': len(changes['added_lines']) - len(changes['removed_lines'])
}
return changes
def format_line_for_csv(line_content: str, max_length: int = 100) -> str:
"""Formate une ligne pour le CSV en limitant sa longueur"""
if len(line_content) > max_length:
return line_content[:max_length] + '...'
return line_content
def analyze_git_history():
"""Fonction principale pour analyser l'historique Git"""
files_to_track = ['livre.org', 'intrigues.org', 'personnages.org']
# Vérifier que nous sommes dans un dépôt Git
try:
run_git_command(['rev-parse', '--git-dir'])
except subprocess.CalledProcessError:
print("Erreur: Ce répertoire n'est pas un dépôt Git", file=sys.stderr)
sys.exit(1)
all_changes = []
# Pour chaque fichier
for filename in files_to_track:
if not os.path.exists(filename):
print(f"Attention: Le fichier {filename} n'existe pas, il sera ignoré", file=sys.stderr)
continue
print(f"Analyse de {filename}...")
# Récupérer l'historique
history = get_file_history(filename)
if not history:
print(f"Aucun historique trouvé pour {filename}")
continue
# Analyser chaque commit
for i, commit in enumerate(history):
commit_hash = commit['hash']
commit_date = commit['date']
commit_author = commit['author']
commit_message = commit['message']
# Récupérer les changements par rapport au commit précédent
if i < len(history) - 1:
prev_hash = history[i + 1]['hash']
changes = get_changes_between_commits(commit_hash, prev_hash, filename)
else:
# Premier commit, récupérer le contenu initial
content = get_file_content_at_commit(commit_hash, filename)
changes = {
'added_lines': [{'line': line, 'context': 'initial'} for line in content.split('\n') if line.strip()],
'removed_lines': [],
'modified_sections': [],
'stats': {
'added_count': len(content.split('\n')),
'removed_count': 0,
'net_change': len(content.split('\n'))
}
}
if changes and changes['stats']['added_count'] + changes['stats']['removed_count'] > 0:
change_entry = {
'filename': filename,
'date': commit_date,
'commit_hash': commit_hash,
'author': commit_author,
'message': commit_message,
'changes': changes
}
all_changes.append(change_entry)
# Trier par date
all_changes.sort(key=lambda x: x['date'])
# Générer le JSON
print("Génération du fichier JSON...")
with open('git_changes_history.json', 'w', encoding='utf-8') as f:
json.dump(all_changes, f, ensure_ascii=False, indent=2)
# Générer le CSV
print("Génération du fichier CSV...")
with open('git_changes_history.csv', 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile)
# En-têtes
writer.writerow([
'Date', 'Fichier', 'Commit Hash', 'Auteur', 'Message',
'Lignes Ajoutées', 'Lignes Supprimées', 'Changement Net',
'Détails Ajouts', 'Détails Suppressions'
])
# Données
for change in all_changes:
# Préparer les détails des ajouts et suppressions
added_details = "; ".join([
format_line_for_csv(item['line'], 50)
for item in change['changes']['added_lines'][:10] # Limiter à 10 exemples
])
if len(change['changes']['added_lines']) > 10:
added_details += f"... (+{len(change['changes']['added_lines']) - 10} autres)"
removed_details = "; ".join([
format_line_for_csv(item['line'], 50)
for item in change['changes']['removed_lines'][:10] # Limiter à 10 exemples
])
if len(change['changes']['removed_lines']) > 10:
removed_details += f"... (+{len(change['changes']['removed_lines']) - 10} autres)"
stats = change['changes']['stats']
writer.writerow([
change['date'],
change['filename'],
change['commit_hash'],
change['author'],
change['message'],
stats['added_count'],
stats['removed_count'],
stats['net_change'],
added_details,
removed_details
])
print(f"Analyse terminée: {len(all_changes)} changements enregistrés")
print("Fichiers générés: git_changes_history.json et git_changes_history.csv")
if __name__ == '__main__':
analyze_git_history()