Last active
October 11, 2025 06:34
-
-
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 | |
| # 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