#!/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 import argparse 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(folder_path: str): """Fonction principale pour analyser l'historique Git dans un dossier donné""" files_to_track = ['livre.org', 'intrigues.org', 'personnages.org'] if not os.path.isdir(folder_path): print(f"Erreur: le dossier {folder_path} n'existe pas.", file=sys.stderr) sys.exit(1) # Sauver le cwd courant et se placer dans le bon dossier pour les commandes git original_cwd = os.getcwd() os.chdir(folder_path) try: # 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 = [] for filename in files_to_track: if not os.path.exists(filename): print(f"Attention: Le fichier {filename} n'existe pas dans {folder_path}, il sera ignoré", file=sys.stderr) continue print(f"Analyse de {filename}...") history = get_file_history(filename) if not history: print(f"Aucun historique trouvé pour {filename}") continue for i, commit in enumerate(history): commit_hash = commit['hash'] commit_date = commit['date'] commit_author = commit['author'] commit_message = commit['message'] if i < len(history) - 1: prev_hash = history[i + 1]['hash'] changes = get_changes_between_commits(commit_hash, prev_hash, filename) else: 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) 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) writer.writerow([ 'Date', 'Fichier', 'Commit Hash', 'Auteur', 'Message', 'Lignes Ajoutées', 'Lignes Supprimées', 'Changement Net', 'Détails Ajouts', 'Détails Suppressions' ]) for change in all_changes: added_details = "; ".join([ format_line_for_csv(item['line'], 50) for item in change['changes']['added_lines'][:10] ]) 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] ]) 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") finally: os.chdir(original_cwd) if __name__ == '__main__': parser = argparse.ArgumentParser(description="Analyse historique git pour livre.org, intrigues.org et personnages.org") parser.add_argument('folder', nargs='?', default='.', help="Chemin du dossier contenant les fichiers org (défaut: dossier courant)") args = parser.parse_args() analyze_git_history(args.folder)