262 lines
9.8 KiB
Python
Executable file
262 lines
9.8 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
|
|
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)
|