|
#!/usr/bin/env python3 |
|
import os |
|
import json |
|
import subprocess |
|
import argparse |
|
from pathlib import Path |
|
import fnmatch |
|
|
|
class TreeClone: |
|
def __init__(self): |
|
self.default_exclude_dirs = {'.git', '__pycache__', 'node_modules', '.DS_Store', '.vscode'} |
|
self.noclone_filename = '.noclone' |
|
|
|
def parse_noclone_file(self, noclone_path): |
|
"""Parse .noclone file and return patterns to exclude.""" |
|
patterns = [] |
|
|
|
try: |
|
with open(noclone_path, 'r') as f: |
|
for line in f: |
|
line = line.strip() |
|
# Skip empty lines and comments |
|
if line and not line.startswith('#'): |
|
patterns.append(line) |
|
except FileNotFoundError: |
|
pass |
|
|
|
return patterns |
|
|
|
def collect_noclone_patterns(self, root_path): |
|
"""Collect all .noclone patterns from root and subdirectories.""" |
|
all_patterns = {} |
|
root = Path(root_path) |
|
|
|
# Find all .noclone files in the directory tree |
|
for noclone_file in root.rglob(self.noclone_filename): |
|
# Get patterns from this .noclone file |
|
patterns = self.parse_noclone_file(noclone_file) |
|
if patterns: |
|
# Store patterns with their directory context |
|
relative_dir = noclone_file.parent.relative_to(root) |
|
all_patterns[str(relative_dir)] = patterns |
|
|
|
return all_patterns |
|
|
|
def should_exclude_item(self, item_path, root_path, noclone_patterns): |
|
"""Check if an item should be excluded based on .noclone patterns.""" |
|
# Check default exclusions |
|
if any(excluded in item_path.parts for excluded in self.default_exclude_dirs): |
|
return True |
|
|
|
root = Path(root_path) |
|
relative_path = item_path.relative_to(root) |
|
|
|
# Check patterns from all applicable .noclone files |
|
for pattern_dir, patterns in noclone_patterns.items(): |
|
pattern_path = Path(pattern_dir) |
|
|
|
# Check if this pattern directory applies to our item |
|
try: |
|
# If the item is in this directory or a subdirectory |
|
if pattern_path == Path('.') or pattern_path in relative_path.parents or pattern_path == relative_path.parent: |
|
# Test each pattern |
|
for pattern in patterns: |
|
# Support both file names and paths |
|
if fnmatch.fnmatch(item_path.name, pattern) or fnmatch.fnmatch(str(relative_path), pattern): |
|
return True |
|
except ValueError: |
|
# relative_path calculation failed, skip this pattern |
|
continue |
|
|
|
return False |
|
|
|
def capture_with_tree(self, directory, output_file): |
|
"""Capture using tree command if available.""" |
|
try: |
|
result = subprocess.run( |
|
['tree', '-J', directory], |
|
capture_output=True, |
|
text=True, |
|
check=True |
|
) |
|
structure = json.loads(result.stdout) |
|
|
|
with open(output_file, 'w') as f: |
|
json.dump(structure, f, indent=2) |
|
|
|
return True |
|
except (subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError): |
|
return False |
|
|
|
def capture_manual(self, directory, output_file, include_files=True, max_depth=None): |
|
"""Manual capture without tree command, respecting .noclone files.""" |
|
root = Path(directory) |
|
|
|
# Collect all .noclone patterns |
|
noclone_patterns = self.collect_noclone_patterns(root) |
|
|
|
print(f"Found .noclone patterns: {noclone_patterns}") |
|
|
|
def scan_directory(path, current_depth=0): |
|
if max_depth is not None and current_depth > max_depth: |
|
return [] |
|
|
|
items = [] |
|
try: |
|
for item in sorted(path.iterdir()): |
|
# Check if item should be excluded |
|
if self.should_exclude_item(item, root, noclone_patterns): |
|
print(f"Excluding: {item.relative_to(root)}") |
|
continue |
|
|
|
if item.is_dir(): |
|
children = scan_directory(item, current_depth + 1) |
|
items.append({ |
|
'name': item.name, |
|
'type': 'directory', |
|
'children': children |
|
}) |
|
elif include_files: |
|
items.append({ |
|
'name': item.name, |
|
'type': 'file' |
|
}) |
|
except PermissionError: |
|
pass |
|
|
|
return items |
|
|
|
structure = { |
|
'root': root.name, |
|
'noclone_patterns': noclone_patterns, |
|
'structure': scan_directory(root) |
|
} |
|
|
|
with open(output_file, 'w') as f: |
|
json.dump(structure, f, indent=2) |
|
|
|
def recreate_structure(self, structure_file, target_directory, create_files=True): |
|
"""Recreate directory structure from JSON.""" |
|
with open(structure_file, 'r') as f: |
|
structure = json.load(f) |
|
|
|
target = Path(target_directory) |
|
|
|
def create_items(items, current_path): |
|
for item in items: |
|
item_path = current_path / item['name'] |
|
|
|
if item['type'] == 'directory': |
|
item_path.mkdir(parents=True, exist_ok=True) |
|
print(f"π {item_path}") |
|
|
|
if 'children' in item: |
|
create_items(item['children'], item_path) |
|
|
|
elif item['type'] == 'file' and create_files: |
|
item_path.touch() |
|
print(f"π {item_path}") |
|
|
|
root_path = target / structure['root'] |
|
root_path.mkdir(parents=True, exist_ok=True) |
|
|
|
# Recreate .noclone files if they were captured |
|
if 'noclone_patterns' in structure: |
|
for pattern_dir, patterns in structure['noclone_patterns'].items(): |
|
noclone_path = root_path / pattern_dir / self.noclone_filename |
|
noclone_path.parent.mkdir(parents=True, exist_ok=True) |
|
|
|
with open(noclone_path, 'w') as f: |
|
f.write("# .noclone file - patterns to exclude from cloning\n") |
|
for pattern in patterns: |
|
f.write(f"{pattern}\n") |
|
|
|
print(f"π Created .noclone: {noclone_path}") |
|
|
|
if 'structure' in structure: |
|
create_items(structure['structure'], root_path) |
|
|
|
return root_path |
|
|
|
def create_sample_noclone(self, directory): |
|
"""Create a sample .noclone file with common patterns.""" |
|
noclone_path = Path(directory) / self.noclone_filename |
|
|
|
sample_content = """# .noclone file - patterns to exclude from TreeClone |
|
# Lines starting with # are comments |
|
# Supports glob patterns like *.log, temp*, etc. |
|
|
|
# Common files to exclude |
|
*.log |
|
*.tmp |
|
*.cache |
|
*.pid |
|
.env |
|
.env.local |
|
|
|
# Directories to exclude |
|
temp/ |
|
logs/ |
|
cache/ |
|
build/ |
|
dist/ |
|
coverage/ |
|
|
|
# IDE and editor files |
|
.vscode/ |
|
.idea/ |
|
*.swp |
|
*.swo |
|
*~ |
|
|
|
# OS files |
|
.DS_Store |
|
Thumbs.db |
|
desktop.ini |
|
|
|
# Add your custom patterns below: |
|
""" |
|
|
|
with open(noclone_path, 'w') as f: |
|
f.write(sample_content) |
|
|
|
print(f"Created sample .noclone file at: {noclone_path}") |
|
return noclone_path |
|
|
|
def main(): |
|
parser = argparse.ArgumentParser(description='TreeClone - Directory structure cloning tool') |
|
subparsers = parser.add_subparsers(dest='command', help='Available commands') |
|
|
|
# Capture command |
|
capture_parser = subparsers.add_parser('capture', help='Capture directory structure') |
|
capture_parser.add_argument('directory', help='Directory to capture') |
|
capture_parser.add_argument('-o', '--output', default='structure.json', help='Output file') |
|
capture_parser.add_argument('--no-files', action='store_true', help='Only capture directories') |
|
capture_parser.add_argument('--max-depth', type=int, help='Maximum depth') |
|
|
|
# Recreate command |
|
recreate_parser = subparsers.add_parser('recreate', help='Recreate directory structure') |
|
recreate_parser.add_argument('structure_file', help='JSON structure file') |
|
recreate_parser.add_argument('target_directory', help='Target directory') |
|
recreate_parser.add_argument('--no-files', action='store_true', help='Only create directories') |
|
|
|
# Init command for creating sample .noclone |
|
init_parser = subparsers.add_parser('init', help='Create sample .noclone file') |
|
init_parser.add_argument('directory', nargs='?', default='.', help='Directory to create .noclone in') |
|
|
|
args = parser.parse_args() |
|
|
|
tool = TreeClone() |
|
|
|
if args.command == 'capture': |
|
print(f"Capturing structure of {args.directory}...") |
|
|
|
# Always use manual capture to respect .noclone files |
|
print("Using manual capture to respect .noclone files...") |
|
tool.capture_manual( |
|
args.directory, |
|
args.output, |
|
include_files=not args.no_files, |
|
max_depth=args.max_depth |
|
) |
|
|
|
print(f"Structure saved to {args.output}") |
|
|
|
elif args.command == 'recreate': |
|
print(f"Recreating structure from {args.structure_file}...") |
|
created_path = tool.recreate_structure( |
|
args.structure_file, |
|
args.target_directory, |
|
create_files=not args.no_files |
|
) |
|
print(f"Structure recreated at: {created_path}") |
|
|
|
elif args.command == 'init': |
|
print(f"Creating sample .noclone file in {args.directory}...") |
|
tool.create_sample_noclone(args.directory) |
|
print("Edit the .noclone file to customize exclusion patterns.") |
|
|
|
else: |
|
parser.print_help() |
|
|
|
if __name__ == '__main__': |
|
main() |