Skip to content

Instantly share code, notes, and snippets.

@brahimmachkouri
Last active October 11, 2025 06:34
Show Gist options
  • Save brahimmachkouri/5a1c1dbfb729ec0f19337cedb3b2996d to your computer and use it in GitHub Desktop.
Save brahimmachkouri/5a1c1dbfb729ec0f19337cedb3b2996d to your computer and use it in GitHub Desktop.
Création d'un document Markdown contenant l'arborescence et le contenu des fichiers d'un projet de développement d'application
#!/usr/bin/env python3
# BM 20251011
import os
import tempfile
import subprocess
import shutil
import argparse
import zipfile
# Répertoires susceptibles d'être exclus par défaut
DEFAULT_EXCLUDE_DIRS = {".git", "venv", "__pycache__", "node_modules", ".cache", ".build", ".vscode", ".DS_Store"}
# Extensions de fichiers binaires courantes à ignorer
BINARY_EXTENSIONS = {
# Images
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp', '.tiff', '.psd',
# Vidéos
'.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.mpg', '.mpeg',
# Audio
'.mp3', '.wav', '.flac', '.aac', '.ogg', '.wma', '.m4a', '.opus',
# Archives
'.zip', '.tar', '.gz', '.bz2', '.7z', '.rar', '.xz',
# Exécutables et bibliothèques
'.exe', '.dll', '.so', '.dylib', '.bin', '.o', '.a',
# Documents binaires
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
# Bases de données
'.db', '.sqlite', '.sqlite3',
# Autres
'.pyc', '.class', '.jar', '.war'
}
def clone_repo(source):
"""Clone un repository Git dans un répertoire temporaire."""
temp_dir = tempfile.mkdtemp()
if source.endswith('.zip'):
with zipfile.ZipFile(source, 'r') as zip_ref:
zip_ref.extractall(temp_dir)
else:
subprocess.run(["git", "clone", source, temp_dir], check=True, capture_output=True)
return temp_dir
def is_binary_file(filepath):
"""
Vérifie si un fichier est binaire en vérifiant:
1. Son extension
2. La présence de bytes nuls dans les premiers octets
"""
# Vérification par extension
_, ext = os.path.splitext(filepath)
if ext.lower() in BINARY_EXTENSIONS:
return True
# Vérification du contenu (recherche de bytes nuls)
try:
with open(filepath, 'rb') as f:
chunk = f.read(8192) # Lit les premiers 8KB
if b'\x00' in chunk: # Présence de byte nul = fichier binaire
return True
except Exception as e:
print(f"Impossible de lire {filepath}: {e}")
return True # En cas d'erreur, on considère le fichier comme binaire par précaution
return False
def is_text_file(filepath):
"""
Vérifie si un fichier est un fichier texte.
Retourne True si le fichier n'est pas binaire.
"""
return not is_binary_file(filepath)
def generate_tree(directory, exclude_set, prefix=""):
"""Génère une représentation arborescente de la structure du répertoire."""
tree = ""
try:
entries = sorted(os.listdir(directory))
except FileNotFoundError:
return ""
for idx, entry in enumerate(entries):
path = os.path.join(directory, entry)
connector = "└── " if idx == len(entries) - 1 else "├── "
if entry in exclude_set:
continue
if os.path.isdir(path):
tree += f"{prefix}{connector}{entry}/\n"
extension = " " if idx == len(entries) - 1 else "│ "
tree += generate_tree(path, exclude_set, prefix + extension)
else:
tree += f"{prefix}{connector}{entry}\n"
return tree
def flatten_repo_to_md(directory, output_md, exclude_set):
"""Crée un fichier Markdown unique avec la structure et le contenu des fichiers texte."""
intro = ""
structure = "## Structure du projet\n\n```\n"
structure += generate_tree(directory, exclude_set)
structure += "```\n\n---\n\n## Contenu complet des fichiers\n\n"
contents = ""
processed_files = set()
skipped_binary_files = []
for root, dirs, files in os.walk(directory):
dirs[:] = [d for d in dirs if d not in exclude_set]
for file in sorted(files):
if file in exclude_set:
continue
filepath = os.path.join(root, file)
rel_path = os.path.relpath(filepath, directory)
if rel_path in processed_files:
continue
processed_files.add(rel_path)
# Vérification si le fichier est binaire
if is_binary_file(filepath):
skipped_binary_files.append(rel_path)
continue
contents += f"### {file} (`/{rel_path}`)\n"
contents += "```\n"
try:
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
contents += f.read()
except Exception as e:
contents += f"*Impossible de lire le contenu du fichier : {e}*"
contents += "\n```\n\n"
# Ajout d'une note sur les fichiers binaires ignorés
if skipped_binary_files:
contents += "---\n\n### Fichiers binaires ignorés\n\n"
contents += f"*{len(skipped_binary_files)} fichier(s) binaire(s) ont été ignorés :*\n\n"
for binary_file in skipped_binary_files[:20]: # Limite à 20 pour ne pas surcharger
contents += f"- `{binary_file}`\n"
if len(skipped_binary_files) > 20:
contents += f"\n*... et {len(skipped_binary_files) - 20} autre(s) fichier(s).*\n"
with open(output_md, "w", encoding="utf-8") as md:
md.write(intro + structure + contents)
def main():
"""Fonction principale du script."""
parser = argparse.ArgumentParser(
description="Aplatit un repository (local ou Git) en un seul fichier Markdown, en incluant uniquement les fichiers texte."
)
parser.add_argument("source", help="Chemin local ou URL GitHub du repository")
parser.add_argument("output", help="Nom du fichier Markdown de sortie")
parser.add_argument(
"-e", "--exclude", nargs="*", default=[], help="Fichiers ou répertoires à exclure de l'analyse"
)
args = parser.parse_args()
exclude_set = DEFAULT_EXCLUDE_DIRS.union(set(args.exclude))
is_git_url = args.source.startswith("http://") or args.source.startswith("https://")
is_zip_file = args.source.endswith(".zip")
temp_dir = None
if is_git_url:
print(f"Clonage du repository depuis {args.source}...")
try:
temp_dir = clone_repo(args.source)
directory = temp_dir
except subprocess.CalledProcessError as e:
print(f"Erreur lors du clonage du repository : {e.stderr.decode('utf-8')}")
return
elif is_zip_file:
print(f"Extraction du fichier zip depuis {args.source}...")
try:
temp_dir = clone_repo(args.source)
directory = temp_dir
except Exception as e:
print(f"Erreur lors de l'extraction du fichier zip : {e}")
return
else:
directory = args.source
if not os.path.isdir(directory):
print(f"Erreur : Le chemin local '{directory}' n'est pas un répertoire valide.")
return
try:
print(f"Génération du fichier Markdown à partir de '{directory}'...")
flatten_repo_to_md(directory, args.output, exclude_set)
print(f"Opération terminée. Fichier Markdown généré : {args.output}")
finally:
if temp_dir:
print("Nettoyage des fichiers temporaires...")
shutil.rmtree(temp_dir)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment