[READ-ONLY] Mirror of https://github.com/mrgnw/scripts.
0

Configure Feed

Select the types of activity you want to include in your feed.

scripts / gitllm
9.2 kB 261 lines
1#!/usr/bin/env -S uv run --script 2# /// script 3# requires-python = ">=3.12" 4# dependencies = [ 5# "requests", 6# "aiohttp", 7# "rich" 8# ] 9# /// 10import os 11import requests 12import asyncio 13import aiohttp 14from pathlib import Path 15import sys 16from fnmatch import fnmatch 17from rich.console import Console 18from rich.panel import Panel 19from rich.progress import Progress, TextColumn, BarColumn, TimeRemainingColumn 20 21# Configuration 22DEFAULT_BRANCH = 'main' 23GITHUB_TOKEN = os.getenv('GITHUB_TOKEN') 24OUTPUT_FILE = 'repo_files.md' 25HEADERS = ( 26 {'Authorization': f'bearer {GITHUB_TOKEN}', 'Content-Type': 'application/json'} if GITHUB_TOKEN else {} 27) 28 29# Initialize rich console 30console = Console() 31 32# Svelte project file patterns 33SVELTE_PATTERNS = [ 34 'package.json', 35 'svelte.config.js', 36 'vite.config.js', 37 'vite.config.ts', 38 'src/routes/**/*.svelte', 39 'src/routes/**/*.js', 40 'src/routes/**/*.ts', 41 'src/lib/**/*.svelte', 42 'src/lib/**/*.js', 43 'src/lib/**/*.ts', 44 'src/app.html', 45 'src/app.d.ts', 46] 47 48 49async def fetch_repo_files(repo, branch='main'): 50 """Fetch all files matching patterns from a repository.""" 51 owner, name = repo.split('/') 52 53 # Check repository and get default branch if needed 54 with console.status("[bold green]Checking repository...", spinner="dots"): 55 url = f"https://api.github.com/repos/{owner}/{name}" 56 response = requests.get(url, headers=HEADERS) 57 response.raise_for_status() 58 repo_info = response.json() 59 60 if branch == 'main' and repo_info['default_branch'] != 'main': 61 branch = repo_info['default_branch'] 62 console.print(f"[yellow]Using default branch:[/] [bold cyan]{branch}[/]") 63 64 # Get repository contents using Git Trees API 65 with console.status("[bold green]Fetching repository structure...", spinner="dots"): 66 tree_url = f"https://api.github.com/repos/{owner}/{name}/git/trees/{branch}?recursive=1" 67 response = requests.get(tree_url, headers=HEADERS) 68 response.raise_for_status() 69 tree_data = response.json() 70 71 if tree_data.get('truncated', False): 72 console.print("[yellow]Warning: Repository tree is truncated. Some files may be missing.[/]") 73 74 # Filter files based on patterns 75 matching_files = [] 76 for item in tree_data.get('tree', []): 77 if item['type'] == 'blob' and any(fnmatch(item['path'], pattern) for pattern in SVELTE_PATTERNS): 78 matching_files.append({'path': item['path']}) 79 80 console.print(f"Found [bold green]{len(matching_files)}[/] matching files.") 81 return matching_files 82 83 84async def fetch_file_contents(repo, files, branch): 85 """Fetch file contents asynchronously with progress tracking.""" 86 owner, name = repo.split('/') 87 results = [] 88 89 with Progress( 90 TextColumn("[bold blue]{task.description}"), 91 BarColumn(), 92 TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), 93 TimeRemainingColumn(), 94 ) as progress: 95 overall_task = progress.add_task(f"[cyan]Downloading files...", total=len(files)) 96 97 async with aiohttp.ClientSession() as session: 98 # Process in batches to avoid rate limits 99 batch_size = 5 100 for i in range(0, len(files), batch_size): 101 batch = files[i:i+batch_size] 102 batch_tasks = [] 103 104 for file in batch: 105 url = f"https://raw.githubusercontent.com/{owner}/{name}/{branch}/{file['path']}" 106 task = asyncio.create_task(fetch_single_file(session, url, file['path'])) 107 batch_tasks.append(task) 108 109 # Wait for batch to complete 110 batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True) 111 112 for j, result in enumerate(batch_results): 113 file_path = batch[j]['path'] 114 if isinstance(result, Exception): 115 console.print(f"[red]Error fetching {file_path}: {result}[/]") 116 else: 117 results.append({'path': file_path, 'content': result}) 118 119 progress.update(overall_task, advance=len(batch)) 120 121 return results 122 123 124async def fetch_single_file(session, url, path): 125 """Fetch a single file's content.""" 126 async with session.get(url) as response: 127 if response.status != 200: 128 raise ValueError(f"Failed to fetch file: {response.status}") 129 return await response.text() 130 131 132def generate_tree_structure(files): 133 """Generate a tree structure for the output markdown.""" 134 # Build directory tree dictionary 135 path_dict = {} 136 for file in files: 137 parts = file['path'].split('/') 138 current = path_dict 139 for i, part in enumerate(parts): 140 if i == len(parts) - 1: # File 141 if 'files' not in current: 142 current['files'] = [] 143 current['files'].append(part) 144 else: # Directory 145 if part not in current: 146 current[part] = {} 147 current = current[part] 148 149 # Build markdown tree representation 150 lines = [] 151 152 def build_tree(node, prefix=""): 153 items = sorted([(k, v) for k, v in node.items() if k != "files"]) 154 files = node.get("files", []) 155 156 # Process directories 157 for i, (name, contents) in enumerate(items): 158 is_last = (i == len(items) - 1 and not files) 159 if is_last: 160 lines.append(f"{prefix}└── 📁 {name}/") 161 build_tree(contents, prefix + " ") 162 else: 163 lines.append(f"{prefix}├── 📁 {name}/") 164 build_tree(contents, prefix + "") 165 166 # Process files 167 for i, file in enumerate(sorted(files)): 168 is_last = (i == len(files) - 1) 169 ext = file.split('.')[-1] if '.' in file else '' 170 171 icon = "📄" 172 if ext in ['js', 'ts']: 173 icon = "🟨" 174 elif ext == 'json': 175 icon = "🔧" 176 elif ext == 'svelte': 177 icon = "🔥" 178 179 if is_last: 180 lines.append(f"{prefix}└── {icon} {file}") 181 else: 182 lines.append(f"{prefix}├── {icon} {file}") 183 184 build_tree(path_dict) 185 return "\n".join(lines) 186 187 188async def main(): 189 # Parse command line arguments 190 if len(sys.argv) < 2: 191 console.print("[bold red]Error:[/] Please provide a repository name (owner/repo) or URL") 192 sys.exit(1) 193 194 # Parse repo from argument (handle URLs too) 195 repo_arg = sys.argv[1] 196 if repo_arg.startswith("https://"): 197 parts = repo_arg.strip('/').split('/') 198 if len(parts) >= 4 and parts[2] in ['github.com', 'www.github.com']: 199 repo = f"{parts[-2]}/{parts[-1]}" 200 else: 201 console.print("[bold red]Error:[/] Invalid GitHub URL format") 202 sys.exit(1) 203 else: 204 repo = repo_arg 205 206 # Get output file path (optional second argument) 207 output_file = sys.argv[2] if len(sys.argv) > 2 else OUTPUT_FILE 208 209 # Get branch (optional --branch argument) 210 branch = DEFAULT_BRANCH 211 if "--branch" in sys.argv and sys.argv.index("--branch") + 1 < len(sys.argv): 212 branch_idx = sys.argv.index("--branch") + 1 213 branch = sys.argv[branch_idx] 214 215 console.print(Panel.fit( 216 f"[bold cyan]Svelte Repository Explorer[/]\n[green]{repo}[/] (branch: [yellow]{branch}[/])", 217 border_style="cyan" 218 )) 219 220 try: 221 # Fetch matching files 222 files = await fetch_repo_files(repo, branch) 223 if not files: 224 console.print("[yellow]No Svelte project files found in repository.[/]") 225 return 226 227 # Sort files for consistent output 228 files.sort(key=lambda f: f['path']) 229 230 # Fetch file contents 231 file_contents = await fetch_file_contents(repo, files, branch) 232 console.print(f"Successfully fetched [bold green]{len(file_contents)}/{len(files)}[/] files") 233 234 # Generate tree structure 235 tree_md = generate_tree_structure(files) 236 237 # Generate markdown content 238 repo_name = repo.split('/')[1] 239 md_content = f'# {repo_name} Project Structure\n\n' 240 md_content += "## File Tree\n\n" 241 md_content += tree_md 242 md_content += "\n\n## File Contents\n\n" 243 244 # Add file contents 245 for file in file_contents: 246 file_path = file['path'] 247 content = file['content'] 248 ext = file_path.split('.')[-1] if '.' in file_path else '' 249 md_content += f"### {file_path}\n\n```{ext}\n{content}\n```\n\n" 250 251 # Write to file 252 Path(output_file).write_text(md_content, encoding='utf-8') 253 console.print(f"[bold green]Generated:[/] {output_file}") 254 255 except Exception as e: 256 console.print(f"[bold red]Error:[/] {e}") 257 sys.exit(1) 258 259 260if __name__ == '__main__': 261 asyncio.run(main())