Skip to content

Instantly share code, notes, and snippets.

@brahimmachkouri
Last active January 20, 2026 14:55
Show Gist options
  • Select an option

  • Save brahimmachkouri/5a1c1dbfb729ec0f19337cedb3b2996d to your computer and use it in GitHub Desktop.

Select an option

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
# Aplatit un repository en un seul fichier Markdown
# BM 20260120
import os
import tempfile
import subprocess
import shutil
import argparse
import zipfile
from pathlib import Path
DEFAULT_EXCLUDE = {".git", "venv", ".venv", "__pycache__", "node_modules", ".cache", ".build", ".vscode", ".DS_Store"}
BINARY_EXTENSIONS = frozenset({
# 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 is_binary(filepath: Path) -> bool:
"""Vérifie si un fichier est binaire (extension ou contenu)."""
if filepath.suffix.lower() in BINARY_EXTENSIONS:
return True
try:
with open(filepath, 'rb') as f:
return b'\x00' in f.read(8192)
except (OSError, IOError):
return True
def clone_or_extract(source: str) -> str:
"""Clone un repo Git ou extrait une archive zip dans un répertoire temporaire."""
temp_dir = tempfile.mkdtemp()
try:
if source.endswith('.zip'):
with zipfile.ZipFile(source, 'r') as zf:
zf.extractall(temp_dir)
else:
subprocess.run(
["git", "clone", "--depth=1", source, temp_dir],
check=True, capture_output=True
)
return temp_dir
except Exception:
shutil.rmtree(temp_dir, ignore_errors=True)
raise
def generate_tree(directory: Path, exclude: set, prefix: str = "") -> list:
"""Génère l'arborescence du répertoire (retourne une liste de lignes)."""
lines = []
try:
entries = sorted(e for e in directory.iterdir() if e.name not in exclude)
except OSError:
return lines
for idx, entry in enumerate(entries):
is_last = idx == len(entries) - 1
connector = "└── " if is_last else "├── "
if entry.is_dir():
lines.append(f"{prefix}{connector}{entry.name}/")
extension = " " if is_last else "│ "
lines.extend(generate_tree(entry, exclude, prefix + extension))
else:
lines.append(f"{prefix}{connector}{entry.name}")
return lines
def flatten_repo(directory: Path, output: Path, exclude: set):
"""Crée le fichier Markdown avec structure et contenu."""
tree_lines = generate_tree(directory, exclude)
binary_files = []
content_parts = []
for filepath in sorted(directory.rglob('*')):
if not filepath.is_file():
continue
# Vérifie si un parent est exclu
if any(part in exclude for part in filepath.relative_to(directory).parts):
continue
rel_path = filepath.relative_to(directory)
if is_binary(filepath):
binary_files.append(str(rel_path))
continue
try:
text = filepath.read_text(encoding='utf-8', errors='replace')
except OSError as e:
text = f"*Impossible de lire le fichier : {e}*"
content_parts.append(f"### {filepath.name} (`/{rel_path}`)\n```\n{text}\n```\n")
# Construction du fichier final
with open(output, 'w', encoding='utf-8') as f:
f.write("## Structure du projet\n\n```\n")
f.write('\n'.join(tree_lines))
f.write("\n```\n\n---\n\n## Contenu complet des fichiers\n\n")
f.write('\n'.join(content_parts))
if binary_files:
f.write(f"\n---\n\n### Fichiers binaires ignorés\n\n")
f.write(f"*{len(binary_files)} fichier(s) binaire(s) ignoré(s) :*\n\n")
for bf in binary_files[:20]:
f.write(f"- `{bf}`\n")
if len(binary_files) > 20:
f.write(f"\n*... et {len(binary_files) - 20} autre(s).*\n")
def main():
parser = argparse.ArgumentParser(
description="Aplatit un repository en un fichier Markdown (fichiers texte uniquement)."
)
parser.add_argument("source", help="Chemin local ou URL Git du repository")
parser.add_argument("output", help="Fichier Markdown de sortie")
parser.add_argument(
"-e", "--exclude",
action="append",
default=[],
metavar="NAME",
help="Fichier/répertoire à exclure (répétable: -e dir1 -e dir2)"
)
args = parser.parse_args()
exclude = DEFAULT_EXCLUDE | set(args.exclude)
source = args.source
temp_dir = None
is_remote = source.startswith(("http://", "https://"))
is_zip = source.endswith(".zip")
try:
if is_remote or is_zip:
action = "Clonage" if is_remote else "Extraction"
print(f"{action} depuis {source}...")
temp_dir = clone_or_extract(source)
directory = Path(temp_dir)
else:
directory = Path(source)
if not directory.is_dir():
print(f"Erreur : '{source}' n'est pas un répertoire valide.")
return 1
print(f"Génération de {args.output}...")
flatten_repo(directory, Path(args.output), exclude)
print(f"Terminé : {args.output}")
return 0
except subprocess.CalledProcessError as e:
print(f"Erreur Git : {e.stderr.decode('utf-8', errors='replace')}")
return 1
except zipfile.BadZipFile:
print(f"Erreur : '{source}' n'est pas un fichier zip valide.")
return 1
except Exception as e:
print(f"Erreur : {e}")
return 1
finally:
if temp_dir:
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
raise SystemExit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment