Last active
January 20, 2026 14:55
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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