Skip to content

Commit 567d978

Browse files
committed
helper
1 parent 947e076 commit 567d978

1 file changed

Lines changed: 129 additions & 0 deletions

File tree

‎merge_repo.py‎

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env python3
2+
"""
3+
merge_repo.py — Merge Python source files from the 'src' subfolder into a single file.
4+
5+
Usage:
6+
python merge_repo.py [--root <repo_root>] [--out <merged_file>] [--folder src]
7+
8+
Defaults:
9+
--root .
10+
--out merged_repo.py
11+
--folder src
12+
13+
Behavior:
14+
- Recursively collects *.py files under the given subfolder (relative to --root).
15+
- Preserves per-file content verbatim (opened as UTF-8 with replacement for undecodable bytes).
16+
- Inserts a clear header block before each file containing:
17+
* Original relative path within the repository
18+
* A best-effort import path (dot-separated) relative to the repo root
19+
* Hints extracted from env.sh if present (e.g., PYTHONPATH changes)
20+
21+
Notes:
22+
- File order is stable and sorted (case-insensitive) by relative path.
23+
- __init__.py files are included (they can document package structure).
24+
- If duplicate module names occur, they are still included in distinct sections.
25+
- The merged file starts with a table of contents to aid navigation.
26+
"""
27+
28+
import os, sys, argparse
29+
30+
def read_env_sh(repo_root: str) -> str:
31+
for base, dirs, files in os.walk(repo_root):
32+
if 'env.sh' in files:
33+
p = os.path.join(base, 'env.sh')
34+
try:
35+
with open(p, 'r', encoding='utf-8', errors='replace') as f:
36+
return f.read()
37+
except Exception:
38+
return ''
39+
return ''
40+
41+
def guess_import_path(rel_path: str) -> str:
42+
if rel_path.endswith('.py'):
43+
rel_path = rel_path[:-3]
44+
rel_path = rel_path.replace('\\', '/')
45+
parts = [p for p in rel_path.split('/') if p not in ('.', '')]
46+
return '.'.join(parts)
47+
48+
def collect_files(repo_root: str, folder: str):
49+
wanted = []
50+
base = os.path.join(repo_root, folder)
51+
if not os.path.isdir(base):
52+
return wanted
53+
for root, dirs, files in os.walk(base):
54+
dirs[:] = [d for d in dirs if d not in ('__pycache__', '.git', '.mypy_cache', '.pytest_cache', '.venv', 'venv')]
55+
for fn in files:
56+
if fn.endswith('.py'):
57+
full = os.path.join(root, fn)
58+
rel = os.path.relpath(full, repo_root)
59+
wanted.append(rel)
60+
wanted.sort(key=lambda s: s.lower())
61+
return wanted
62+
63+
def main():
64+
ap = argparse.ArgumentParser()
65+
ap.add_argument('--root', default='.', help='Repository root (default: .)')
66+
ap.add_argument('--out', default='merged_repo.py', help='Output merged file path')
67+
ap.add_argument('--folder', default='src', help='Subfolder to include (default: src)')
68+
args = ap.parse_args()
69+
70+
repo_root = os.path.abspath(args.root)
71+
env_text = read_env_sh(repo_root)
72+
files = collect_files(repo_root, args.folder)
73+
74+
header_lines = []
75+
header_lines.append('#' * 80)
76+
header_lines.append('# MERGED REPOSITORY FILE (auto-generated by merge_repo.py)')
77+
header_lines.append('#')
78+
header_lines.append('# Included subfolder: ' + args.folder)
79+
header_lines.append('# Repository root: ' + repo_root)
80+
header_lines.append('# Total files: ' + str(len(files)))
81+
header_lines.append('#')
82+
header_lines.append('# Table of Contents (relative path -> import path):')
83+
for rel in files:
84+
header_lines.append('# - {rel} -> {imp}'.format(rel=rel, imp=guess_import_path(rel)))
85+
header_lines.append('#')
86+
if env_text.strip():
87+
header_lines.append('# env.sh (excerpt, first 60 lines):')
88+
lines = env_text.splitlines()
89+
for line in lines[:60]:
90+
header_lines.append('# ' + line)
91+
if len(lines) > 60:
92+
header_lines.append('# ... (truncated)')
93+
else:
94+
header_lines.append('# No env.sh found or it was empty/unreadable.')
95+
header_lines.append('#' * 80)
96+
header_lines.append('')
97+
merged_header = '\n'.join(header_lines)
98+
99+
os.makedirs(os.path.dirname(os.path.abspath(args.out)) or '.', exist_ok=True)
100+
with open(args.out, 'w', encoding='utf-8', errors='replace') as out:
101+
out.write(merged_header + '\n\n')
102+
for rel in files:
103+
imp = guess_import_path(rel)
104+
section_header = []
105+
section_header.append('\n' + '#' * 80)
106+
section_header.append('# BEGIN FILE: {rel}'.format(rel=rel))
107+
section_header.append('# Suggested import: {imp}'.format(imp=imp))
108+
if env_text.strip():
109+
section_header.append('# env.sh hints: the environment may export PYTHONPATH or other variables')
110+
section_header.append('# that affect how modules are imported. See the env.sh excerpt above.')
111+
section_header.append('# Additional context:')
112+
section_header.append('# - This block is a verbatim copy of the original file.')
113+
section_header.append('# - If this file defines __all__ or relies on package-relative imports, the order in')
114+
section_header.append('# this merged file does not simulate runtime import side-effects; it is for reading.')
115+
section_header.append('# - Use the relative path to locate the original source in the repo.')
116+
section_header.append('#' * 80 + '\n')
117+
out.write('\n'.join(section_header))
118+
119+
full = os.path.join(repo_root, rel)
120+
try:
121+
with open(full, 'r', encoding='utf-8', errors='replace') as f:
122+
content = f.read()
123+
except Exception as e:
124+
content = '# ERROR reading file: {e}\n'.format(e=e)
125+
out.write(content)
126+
out.write('\n\n# END FILE: {rel}\n'.format(rel=rel))
127+
128+
if __name__ == '__main__':
129+
main()

0 commit comments

Comments
 (0)