Design updates
- gameplay draft - core architecture (Python core)
This commit is contained in:
145
tools/filetree.py
Normal file
145
tools/filetree.py
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create an ASCII file tree while respecting .gitignore when possible.
|
||||
|
||||
Usage:
|
||||
python tools/filetree.py [output_path]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Dict, Iterable, List
|
||||
|
||||
|
||||
def find_repo_root(start: str) -> str:
|
||||
cur = os.path.abspath(start)
|
||||
while True:
|
||||
if os.path.isdir(os.path.join(cur, ".git")):
|
||||
return cur
|
||||
parent = os.path.dirname(cur)
|
||||
if parent == cur:
|
||||
return os.path.abspath(start)
|
||||
cur = parent
|
||||
|
||||
|
||||
def git_available() -> bool:
|
||||
try:
|
||||
subprocess.run(["git", "--version"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def list_files_via_git(root: str) -> List[str]:
|
||||
result = subprocess.run(
|
||||
["git", "-C", root, "ls-files", "--others", "--cached", "--exclude-standard"],
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
)
|
||||
files = [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
||||
return files
|
||||
|
||||
|
||||
def parse_gitignore(root: str) -> List[str]:
|
||||
path = os.path.join(root, ".gitignore")
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
patterns: List[str] = []
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
for raw in f:
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
patterns.append(line)
|
||||
return patterns
|
||||
|
||||
|
||||
def is_ignored(path_rel: str, is_dir: bool, patterns: List[str]) -> bool:
|
||||
# Minimal .gitignore matching: supports *, ?, **, leading /, trailing /
|
||||
path_rel = path_rel.replace(os.sep, "/")
|
||||
ignored = False
|
||||
for pat in patterns:
|
||||
negate = pat.startswith("!")
|
||||
if negate:
|
||||
pat = pat[1:]
|
||||
if pat.startswith("/"):
|
||||
pat = pat[1:]
|
||||
if pat.endswith("/") and not is_dir:
|
||||
continue
|
||||
pat = pat.rstrip("/")
|
||||
if fnmatch.fnmatch(path_rel, pat) or fnmatch.fnmatch(os.path.basename(path_rel), pat):
|
||||
ignored = not negate
|
||||
return ignored
|
||||
|
||||
|
||||
def list_files_with_fallback(root: str) -> List[str]:
|
||||
patterns = parse_gitignore(root)
|
||||
files: List[str] = []
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
rel_dir = os.path.relpath(dirpath, root)
|
||||
if rel_dir == ".":
|
||||
rel_dir = ""
|
||||
# Prune ignored directories early
|
||||
pruned = []
|
||||
for d in dirnames:
|
||||
rel_path = os.path.join(rel_dir, d) if rel_dir else d
|
||||
if is_ignored(rel_path, True, patterns):
|
||||
continue
|
||||
pruned.append(d)
|
||||
dirnames[:] = pruned
|
||||
for name in filenames:
|
||||
rel_path = os.path.join(rel_dir, name) if rel_dir else name
|
||||
if is_ignored(rel_path, False, patterns):
|
||||
continue
|
||||
files.append(rel_path.replace(os.sep, "/"))
|
||||
return files
|
||||
|
||||
|
||||
def build_tree(paths: Iterable[str]) -> Dict[str, dict]:
|
||||
root: Dict[str, dict] = {}
|
||||
for path in paths:
|
||||
parts = [p for p in path.split("/") if p]
|
||||
node = root
|
||||
for part in parts:
|
||||
node = node.setdefault(part, {})
|
||||
return root
|
||||
|
||||
|
||||
def render_tree(node: Dict[str, dict], prefix: str = "") -> List[str]:
|
||||
lines: List[str] = []
|
||||
entries = sorted(node.keys())
|
||||
for i, name in enumerate(entries):
|
||||
is_last = i == len(entries) - 1
|
||||
connector = "\\-- " if is_last else "|-- "
|
||||
lines.append(f"{prefix}{connector}{name}")
|
||||
child = node[name]
|
||||
if child:
|
||||
extension = " " if is_last else "| "
|
||||
lines.extend(render_tree(child, prefix + extension))
|
||||
return lines
|
||||
|
||||
|
||||
def main() -> int:
|
||||
out_path = sys.argv[1] if len(sys.argv) > 1 else "FILETREE.txt"
|
||||
root = find_repo_root(os.getcwd())
|
||||
if os.path.isdir(os.path.join(root, ".git")) and git_available():
|
||||
files = list_files_via_git(root)
|
||||
else:
|
||||
files = list_files_with_fallback(root)
|
||||
|
||||
tree = build_tree(files)
|
||||
lines = ["."]
|
||||
lines.extend(render_tree(tree))
|
||||
out_abs = os.path.join(root, out_path)
|
||||
with open(out_abs, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user