Cybrkyd's git repositories

GitGen • commit: 0c017c0

commit 0c017c08961d17f7ef544d5f797e0bc00bc93c162792e5cf722850f31d6c1894
author cybrkyd <noreply@cybrkyd.com> 2025-12-28 18:23:06 +0000
committer cybrkyd <noreply@cybrkyd.com> 2025-12-28 18:23:06 +0000

Commit Message

Initial commit

📊 Diffstat

gitgen.py 687
1 files changed, 687 insertions(+), 0 deletions(-)

Diff

commit 0c017c08961d17f7ef544d5f797e0bc00bc93c162792e5cf722850f31d6c1894
Author: cybrkyd <noreply@cybrkyd.com>
Date: Sun Dec 28 18:23:06 2025 +0000
Initial commit
diff --git a/gitgen.py b/gitgen.py
new file mode 100644
index 0000000..29d0821
--- /dev/null
+++ b/gitgen.py
@@ -0,0 +1,687 @@
+ #!/usr/bin/env python3
+ """
+ GitGen - a static git repository website generator
+ """
+
+ import os
+ import sys
+ import subprocess
+ import datetime
+ import html
+ from pathlib import Path
+ from typing import List, Dict, Optional, DefaultDict
+ from collections import defaultdict
+ import markdown
+ import re
+ from concurrent.futures import ThreadPoolExecutor
+
+ class GitRepoScanner:
+ """Scans and processes Git repositories"""
+
+ def __init__(self, base_path: str = "/home/cybr/Work/cybrkyd-git"):
+ self.base_path = Path(base_path).resolve()
+ self.repos = []
+ self.commit_cache: Dict[str, Dict] = {}
+ self.tree_cache: Dict[Path, List[Dict]] = {}
+ self.commit_history_cache: Dict[Path, List[Dict]] = {}
+
+ def run_git_command(self, repo_path: Path, args: List[str]) -> Optional[str]:
+ try:
+ result = subprocess.run(
+ ['git'] + args,
+ cwd=repo_path,
+ capture_output=True,
+ text=True,
+ check=True
+ )
+ return result.stdout
+ except Exception:
+ return None
+
+ def find_git_repos(self) -> List[Dict]:
+ self.repos = []
+ for item in self.base_path.iterdir():
+ if item.is_dir() and (item / '.git').is_dir():
+ info = self.get_repo_info(item)
+ if info:
+ self.repos.append(info)
+ self.repos.sort(key=lambda x: x['name'].lower())
+ return self.repos
+
+ def get_repo_info(self, repo_path: Path) -> Optional[Dict]:
+ try:
+ name = repo_path.name
+ description = ""
+ desc_file = repo_path / '.git' / 'description'
+ if desc_file.exists():
+ for enc in ('utf-8', 'latin-1'):
+ try:
+ description = desc_file.read_text(encoding=enc).strip()
+ break
+ except Exception:
+ pass
+
+ remote = self.run_git_command(repo_path, ['config', '--get', 'remote.origin.url'])
+ owner = self.run_git_command(repo_path, ['config', '--get', 'user.name'])
+
+ if not owner and remote:
+ parts = remote.split('/')
+ if len(parts) >= 2:
+ owner = parts[-2].replace('.git', '').split(':')[-1]
+
+ latest = self.run_git_command(repo_path, [
+ 'log', '-1', '--pretty=format:%H|%an|%ae|%ad|%B|||', '--date=iso'
+ ])
+
+ commit_info = {}
+ if latest:
+ header = latest.split('|||', 1)[0].split('|', 4)
+ if len(header) == 5:
+ commit_info = {
+ 'hash': header[0],
+ 'author': header[1],
+ 'email': header[2],
+ 'date': header[3],
+ 'message': header[4].strip()
+ }
+
+ branch = self.run_git_command(repo_path, ['branch', '--show-current']) or 'main'
+
+ return {
+ 'name': name,
+ 'path': repo_path,
+ 'relative_path': repo_path.relative_to(self.base_path),
+ 'description': description,
+ 'owner': owner.strip() if owner else 'Unknown',
+ 'remote_url': remote.strip() if remote else None,
+ 'latest_commit': commit_info,
+ 'branch': branch.strip(),
+ 'readme_content': self.get_readme_content(repo_path),
+ 'name_escaped': html.escape(name),
+ 'owner_escaped': html.escape(owner.strip() if owner else 'Unknown'),
+ 'description_escaped': html.escape(description),
+ }
+ except Exception as e:
+ print(f"Error processing {repo_path}: {e}")
+ return None
+
+ def get_readme_content(self, repo_path: Path) -> Optional[str]:
+ for name in ['README.md', 'README.txt', 'README', 'readme.md']:
+ path = repo_path / name
+ if path.exists():
+ for enc in ('utf-8', 'latin-1'):
+ try:
+ content = path.read_text(encoding=enc)
+ return markdown.markdown(content, extensions=["tables"]) if name.endswith('.md') else f"<pre>{html.escape(content)}</pre>"
+ except Exception:
+ pass
+ return None
+
+ def get_working_tree(self, repo_path: Path) -> List[Dict]:
+ if repo_path in self.tree_cache:
+ return self.tree_cache[repo_path]
+
+ tree = self.run_git_command(repo_path, ['ls-tree', '-r', '--name-only', 'HEAD'])
+ if not tree:
+ self.tree_cache[repo_path] = []
+ return []
+
+ files = tree.strip().split('\n')
+
+ log = self.run_git_command(repo_path, [
+ 'log', '-1', '--name-only',
+ '--pretty=format:%ad|%an',
+ '--date=format:%Y-%m-%d'
+ ])
+
+ file_meta = {}
+ if log:
+ lines = log.splitlines()
+ if lines:
+ meta = lines[0].split('|', 1)
+ for f in lines[1:]:
+ file_meta[f] = meta
+
+ result = []
+ for f in files:
+ meta = file_meta.get(f, ['', ''])
+ result.append({
+ 'path': f,
+ 'name': os.path.basename(f),
+ 'directory': os.path.dirname(f) or '.',
+ 'last_modified': meta[0],
+ 'last_author': meta[1],
+ 'path_escaped': html.escape(f),
+ })
+
+ result.sort(key=lambda x: (x['directory'], x['name']))
+ self.tree_cache[repo_path] = result
+ return result
+
+ def get_commit_history(self, repo_path: Path) -> List[Dict]:
+ if repo_path in self.commit_history_cache:
+ return self.commit_history_cache[repo_path]
+
+ log = self.run_git_command(repo_path, [
+ 'log', '--max-count=25',
+ '--pretty=format:%H|%an|%ae|%ad|%s|%P',
+ '--date=iso'
+ ])
+ if not log:
+ self.commit_history_cache[repo_path] = []
+ return []
+
+ commits = []
+ for line in log.strip().split('\n'):
+ parts = line.split('|', 5)
+ if len(parts) == 6:
+ commits.append({
+ 'hash': parts[0],
+ 'short_hash': parts[0][:7],
+ 'author': parts[1],
+ 'email': parts[2],
+ 'date': parts[3],
+ 'message': parts[4],
+ 'parent_hash': parts[5],
+ 'author_escaped': html.escape(parts[1]),
+ 'email_escaped': html.escape(parts[2]),
+ 'message_escaped': html.escape(parts[4]).replace('\n', '<br>'),
+ })
+
+ self.commit_history_cache[repo_path] = commits
+ return commits
+
+ def get_commit_details(self, repo_path: Path, commit_hash: str) -> Dict:
+ if commit_hash in self.commit_cache:
+ return self.commit_cache[commit_hash]
+
+ header = self.run_git_command(repo_path, [
+ 'log', '-1', commit_hash,
+ '--pretty=format:%H|%an|%ae|%ad|%B|||',
+ '--date=iso'
+ ])
+ if not header:
+ return {}
+
+ head = header.split('|||', 1)[0].split('|', 4)
+ parents = self.run_git_command(repo_path, ['log', '-1', commit_hash, '--pretty=format:%P']) or ""
+ files = self.run_git_command(repo_path, [
+ 'log', '-1', '--name-status', '--pretty=', commit_hash
+ ]) or ""
+
+ diff = self.run_git_command(
+ repo_path,
+ ['diff', '--no-color', parents.split()[0], commit_hash]
+ if parents else ['show', '--no-color', commit_hash]
+ ) or ""
+
+ details = {
+ 'hash': head[0],
+ 'short_hash': head[0][:7],
+ 'author': head[1],
+ 'email': head[2],
+ 'date': head[3],
+ 'message': head[4].strip(),
+ 'parent_hash': parents.strip(),
+ 'files': [],
+ 'diff': diff,
+ 'author_escaped': html.escape(head[1]),
+ 'email_escaped': html.escape(head[2]),
+ 'message_escaped': html.escape(head[4].strip()).replace('\n', '<br>'),
+ }
+
+ for line in files.splitlines():
+ if '\t' in line:
+ s, p = line.split('\t', 1)
+ details['files'].append({'status': s, 'path': p, 'path_escaped': html.escape(p)})
+
+ self.commit_cache[commit_hash] = details
+ return details
+
+ class HTMLGenerator:
+ def __init__(self, scanner: GitRepoScanner, output_dir: str = "git-website"):
+ self.scanner = scanner
+ self.output_dir = Path(output_dir)
+ self.output_dir.mkdir(exist_ok=True)
+
+ def generate_index(self, repos: List[Dict]) -> str:
+ """Generate main index page with table layout"""
+ html_fragments = [
+ """<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Git Repositories</title>
+ <link rel="stylesheet" href="main.css">
+ </head>
+ <body>
+ <div class="header">
+ <h1><a href="/" title="gitgen"> <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 21 21" width="38" height="38" fill="none" stroke="#222222" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M4.13 1.101v12.56m11.972-6.28a2.993 3.14 0 1 0 0-6.28 2.993 3.14 0 0 0 0 6.28M4.13 19.94a2.993 3.14 0 1 0 0-6.28 2.993 3.14 0 0 0 0 6.28" style="stroke-width:2.04357"/><path d="M13.109 4.241a8.978 9.42 0 0 0-8.978 9.42" style="stroke-width:2.04357"/><path stroke="#0f0" d="M16.172 13.915v6m3-3h-6"/><path stroke="red" d="M19.135 11.43h-6"/></svg></a> Cybrkyd's git repositories</h1>
+ <p>Listing {} repositories</p>
+ </div>
+
+ <div class="repo-list">
+ <table class="repo-table">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Description</th>
+ <th>Owner</th>
+ <th>Last Commit</th>
+ </tr>
+ </thead>
+ <tbody>""".format(len(repos))
+ ]
+
+ for repo in repos:
+ last_commit = repo.get('latest_commit', {})
+ last_commit_date = last_commit.get('date', 'N/A') if last_commit else 'N/A'
+ html_fragments.append(f"""
+ <tr>
+ <td class="repo-name">
+ <a href="{repo['name']}/index.html" class="repo-link">
+ {repo['name_escaped']}
+ </a>
+ </td>
+ <td class="repo-description">{repo['description_escaped']}</td>
+ <td class="repo-owner">{repo['owner_escaped']}</td>
+ <td class="repo-last-commit">{last_commit_date}</td>
+ </tr>
+ """)
+
+ html_fragments.append("""
+ </tbody>
+ </table>
+ </div>
+
+ <div class="footer">
+ <p>Static Git Repository Browser • Generated on {}</p>
+ </div>
+ </body>
+ </html>""".format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
+
+ return "".join(html_fragments)
+
+ def generate_repo_overview_page(self, repo: Dict) -> str:
+ """Generate repository overview page (README only)"""
+ html_fragments = [
+ f"""<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{repo['name_escaped']} - Repository Overview</title>
+ <link rel="stylesheet" href="../main.css">
+ </head>
+ <body>
+ <div class="header">
+ <h1><a href="../index.html" title="gitgen"> <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 21 21" width="38" height="38" fill="none" stroke="#222222" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M4.13 1.101v12.56m11.972-6.28a2.993 3.14 0 1 0 0-6.28 2.993 3.14 0 0 0 0 6.28M4.13 19.94a2.993 3.14 0 1 0 0-6.28 2.993 3.14 0 0 0 0 6.28" style="stroke-width:2.04357"/><path d="M13.109 4.241a8.978 9.42 0 0 0-8.978 9.42" style="stroke-width:2.04357"/><path stroke="#0f0" d="M16.172 13.915v6m3-3h-6"/><path stroke="red" d="M19.135 11.43h-6"/></svg></a> Cybrkyd's git repositories</h1>
+ <p>{repo['name_escaped']}</p>
+ </div>
+
+ <div class="breadcrumb">
+ <a href="../index.html">← Back to All Repositories</a>
+ </div>
+
+ <div class="navigation">
+ <a href="index.html" class="nav-btn active">README</a>
+ <a href="files.html" class="nav-btn secondary">Files</a>
+ <a href="commits.html" class="nav-btn secondary">Commits</a>
+ </div>
+
+ <div class="repo-ov">
+ <div class="meta-item">
+ <strong>Branch: </strong>
+ <span class="branch-badge">{html.escape(repo.get('branch', 'main'))}</span>
+ <strong>Last commit: </strong>
+ <span>{repo.get('latest_commit', {}).get('date', 'N/A')}</span>
+ <strong>Clone: </strong>
+ <code>git clone {html.escape(repo['remote_url'])}</code>
+ </div>
+ </div>
+
+ <div class="readme-content">
+ {repo['readme_content'] if repo.get('readme_content') else '<p><em>No README file found in this repository.</em></p>'}
+ </div>
+
+ <div class="footer">
+ <p>Repository: {repo['name_escaped']} • Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
+ </div>
+ </body>
+ </html>"""
+ ]
+ return "".join(html_fragments)
+
+ def generate_files_page(self, repo: Dict, files: List[Dict]) -> str:
+ """Generate files page"""
+ files_by_dir = defaultdict(list)
+ for file_info in files:
+ files_by_dir[file_info['directory']].append(file_info)
+
+ sorted_dirs = sorted(files_by_dir.keys())
+
+ html_fragments = [
+ f"""<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{repo['name_escaped']} - Files</title>
+ <link rel="stylesheet" href="../main.css">
+ </head>
+ <body>
+ <div class="header">
+ <h1><a href="../index.html" title="gitgen"> <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 21 21" width="38" height="38" fill="none" stroke="#222222" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M4.13 1.101v12.56m11.972-6.28a2.993 3.14 0 1 0 0-6.28 2.993 3.14 0 0 0 0 6.28M4.13 19.94a2.993 3.14 0 1 0 0-6.28 2.993 3.14 0 0 0 0 6.28" style="stroke-width:2.04357"/><path d="M13.109 4.241a8.978 9.42 0 0 0-8.978 9.42" style="stroke-width:2.04357"/><path stroke="#0f0" d="M16.172 13.915v6m3-3h-6"/><path stroke="red" d="M19.135 11.43h-6"/></svg></a> Cybrkyd's git repositories</h1>
+ <p>{repo['name_escaped']} - {len(files):,} files</p>
+ </div>
+
+ <div class="breadcrumb">
+ <a href="../index.html">← All Repositories</a> •
+ <a href="index.html">← {repo['name_escaped']} Overview</a>
+ </div>
+
+ <div class="navigation">
+ <a href="index.html" class="nav-btn secondary">README</a>
+ <a href="files.html" class="nav-btn active">Files</a>
+ <a href="commits.html" class="nav-btn secondary">Commits</a>
+ </div>
+
+ <div class="file-list">
+ """
+ ]
+
+ if files:
+ for directory in sorted_dirs:
+ html_fragments.append(f'<div class="directory">📁 {html.escape(directory) if directory != "." else "Root Directory"}</div>\n')
+ for file_info in files_by_dir[directory]:
+ html_fragments.append(f"""
+ <div class="file-item">
+ <div class="file-info">
+ <div class="file-path">
+ <span class="file-icon">🗎</span>
+ {file_info['name']}
+ </div>
+ </div>
+ </div>
+ """)
+ else:
+ html_fragments.append("""
+ <div style="padding: 3rem; text-align: center; color: #718096;">
+ <p style="font-size: 1.2rem; margin-bottom: 1rem;">No files found in this repository.</p>
+ <p>This repository might be empty.</p>
+ </div>
+ """)
+
+ html_fragments.append(f"""
+ </div>
+
+ <div class="footer">
+ <p>Repository: {repo['name_escaped']} • Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
+ </div>
+ </body>
+ </html>""")
+
+ return "".join(html_fragments)
+
+ def generate_commits_page(self, repo: Dict, commits: List[Dict]) -> str:
+ """Generate commits page"""
+ html_fragments = [
+ f"""<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{repo['name_escaped']} - Commits</title>
+ <link rel="stylesheet" href="../main.css">
+ </head>
+ <body>
+ <div class="header">
+ <h1><a href="../index.html" title="gitgen"> <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 21 21" width="38" height="38" fill="none" stroke="#222222" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M4.13 1.101v12.56m11.972-6.28a2.993 3.14 0 1 0 0-6.28 2.993 3.14 0 0 0 0 6.28M4.13 19.94a2.993 3.14 0 1 0 0-6.28 2.993 3.14 0 0 0 0 6.28" style="stroke-width:2.04357"/><path d="M13.109 4.241a8.978 9.42 0 0 0-8.978 9.42" style="stroke-width:2.04357"/><path stroke="#0f0" d="M16.172 13.915v6m3-3h-6"/><path stroke="red" d="M19.135 11.43h-6"/></svg></a> Cybrkyd's git repositories</h1>
+ <p>{repo['name_escaped']} - Commit History</p>
+ </div>
+
+ <div class="breadcrumb">
+ <a href="../index.html">← All Repositories</a> •
+ <a href="index.html">← {repo['name_escaped']} Overview</a>
+ </div>
+
+ <div class="navigation">
+ <a href="index.html" class="nav-btn secondary">README</a>
+ <a href="files.html" class="nav-btn secondary">Files</a>
+ <a href="commits.html" class="nav-btn active">Commits</a>
+ </div>
+
+ <div class="commit-list">
+ """
+ ]
+
+ if commits:
+ for commit in commits:
+ commit_details = self.scanner.get_commit_details(repo['path'], commit['hash'])
+ formatted_message = commit_details.get('message_escaped', '') if commit_details else commit.get('message_escaped', '')
+ html_fragments.append(f"""
+ <div class="commit-item commit-meta">
+ <span class="label">commit:</span>
+ <span class="value"><a href="commits/{commit['short_hash']}.html">{commit['hash']}</a></span>
+ <span class="label">date:</span>
+ <span class="value">{commit['date']}</span>
+ <span class="label">author:</span>
+ <span class="value">{commit['author_escaped']} &lt;{commit['email_escaped']}&gt;</span>
+
+ <div class="commit-message-box">
+ <h3>Commit Message</h3>
+ <p>{formatted_message}</p>
+ </div>
+ </div>
+ """)
+ else:
+ html_fragments.append("""
+ <div style="padding: 3rem; text-align: center; color: #718096;">
+ <p style="font-size: 1.2rem; margin-bottom: 1rem;">No commits found in this repository.</p>
+ <p>This repository might be empty.</p>
+ </div>
+ """)
+
+ html_fragments.append(f"""
+ </div>
+
Diff truncated. 200 more lines not shown.