|  | import http.server
 | 
						
						
						
							|  | import socketserver
 | 
						
						
						
							|  | import json
 | 
						
						
						
							|  | import os
 | 
						
						
						
							|  | import subprocess
 | 
						
						
						
							|  | from urllib.parse import parse_qs, urlparse
 | 
						
						
						
							|  | import mimetypes
 | 
						
						
						
							|  | import html
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | # --- Theme selection (choose one: "light1", "light2", "dark1", "dark2") ---
 | 
						
						
						
							|  | THEME = "light1"  # Change this to "light2", "dark1", or "dark2" as desired
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | THEMES = {
 | 
						
						
						
							|  |     "light1": """
 | 
						
						
						
							|  |         body { font-family: Arial, sans-serif; background: #f6f8fa; margin: 0; padding: 0; color: #222; }
 | 
						
						
						
							|  |         .container { max-width: 960px; margin: auto; padding: 2em; }
 | 
						
						
						
							|  |         .date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 1px solid #ccc; }
 | 
						
						
						
							|  |         .commit { margin: 1em 0; padding: 1em; background: #fff; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.07); }
 | 
						
						
						
							|  |         .hash { color: #555; font-family: monospace; }
 | 
						
						
						
							|  |         .diff { white-space: pre-wrap; background: #f1f1f1; padding: 1em; border-radius: 4px; overflow-x: auto; }
 | 
						
						
						
							|  |         ul { list-style: none; padding-left: 0; }
 | 
						
						
						
							|  |         li { margin: 0.3em 0; }
 | 
						
						
						
							|  |         a { text-decoration: none; color: #0366d6; }
 | 
						
						
						
							|  |         a:hover { text-decoration: underline; }
 | 
						
						
						
							|  |     """,
 | 
						
						
						
							|  |     "light2": """
 | 
						
						
						
							|  |         body { font-family: 'Segoe UI', sans-serif; background: #fdf6e3; margin: 0; padding: 0; color: #333; }
 | 
						
						
						
							|  |         .container { max-width: 900px; margin: auto; padding: 2em; }
 | 
						
						
						
							|  |         .date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 2px solid #e1c699; color: #b58900; }
 | 
						
						
						
							|  |         .commit { margin: 1em 0; padding: 1em; background: #fffbe6; border-radius: 6px; box-shadow: 0 1px 3px rgba(200,180,100,0.08); }
 | 
						
						
						
							|  |         .hash { color: #b58900; font-family: monospace; }
 | 
						
						
						
							|  |         .diff { white-space: pre-wrap; background: #f5e9c9; padding: 1em; border-radius: 4px; overflow-x: auto; }
 | 
						
						
						
							|  |         ul { list-style: none; padding-left: 0; }
 | 
						
						
						
							|  |         li { margin: 0.3em 0; }
 | 
						
						
						
							|  |         a { text-decoration: none; color: #b58900; }
 | 
						
						
						
							|  |         a:hover { text-decoration: underline; color: #cb4b16; }
 | 
						
						
						
							|  |     """,
 | 
						
						
						
							|  |     "dark1": """
 | 
						
						
						
							|  |         body { font-family: Arial, sans-serif; background: #181a1b; margin: 0; padding: 0; color: #eaeaea; }
 | 
						
						
						
							|  |         .container { max-width: 960px; margin: auto; padding: 2em; }
 | 
						
						
						
							|  |         .date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 1px solid #333; color: #8ab4f8; }
 | 
						
						
						
							|  |         .commit { margin: 1em 0; padding: 1em; background: #23272b; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.18); }
 | 
						
						
						
							|  |         .hash { color: #8ab4f8; font-family: monospace; }
 | 
						
						
						
							|  |         .diff { white-space: pre-wrap; background: #23272b; padding: 1em; border-radius: 4px; overflow-x: auto; }
 | 
						
						
						
							|  |         ul { list-style: none; padding-left: 0; }
 | 
						
						
						
							|  |         li { margin: 0.3em 0; }
 | 
						
						
						
							|  |         a { text-decoration: none; color: #8ab4f8; }
 | 
						
						
						
							|  |         a:hover { text-decoration: underline; color: #bb86fc; }
 | 
						
						
						
							|  |     """,
 | 
						
						
						
							|  |     "dark2": """
 | 
						
						
						
							|  |         body { font-family: 'Fira Sans', sans-serif; background: #121212; margin: 0; padding: 0; color: #d0d0d0; }
 | 
						
						
						
							|  |         .container { max-width: 900px; margin: auto; padding: 2em; }
 | 
						
						
						
							|  |         .date-header { font-size: 1.2em; margin-top: 2em; border-bottom: 2px solid #444; color: #ffb86c; }
 | 
						
						
						
							|  |         .commit { margin: 1em 0; padding: 1em; background: #22223b; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.22); }
 | 
						
						
						
							|  |         .hash { color: #ffb86c; font-family: monospace; }
 | 
						
						
						
							|  |         .diff { white-space: pre-wrap; background: #282a36; padding: 1em; border-radius: 4px; overflow-x: auto; }
 | 
						
						
						
							|  |         ul { list-style: none; padding-left: 0; }
 | 
						
						
						
							|  |         li { margin: 0.3em 0; }
 | 
						
						
						
							|  |         a { text-decoration: none; color: #ffb86c; }
 | 
						
						
						
							|  |         a:hover { text-decoration: underline; color: #8be9fd; }
 | 
						
						
						
							|  |     """,
 | 
						
						
						
							|  | }
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | def HTML_TEMPLATE(content, theme=THEME):
 | 
						
						
						
							|  |     return f"""
 | 
						
						
						
							|  | <!DOCTYPE html>
 | 
						
						
						
							|  | <html lang=\"en\">
 | 
						
						
						
							|  | <head>
 | 
						
						
						
							|  |     <meta charset=\"UTF-8\">
 | 
						
						
						
							|  |     <title>Git Log Viewer</title>
 | 
						
						
						
							|  |     <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css\">
 | 
						
						
						
							|  |     <style>
 | 
						
						
						
							|  |         {THEMES[theme]}
 | 
						
						
						
							|  |     </style>
 | 
						
						
						
							|  |     <script src=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js\"></script>
 | 
						
						
						
							|  |     <script>hljs.highlightAll();</script>
 | 
						
						
						
							|  | </head>
 | 
						
						
						
							|  | <body>
 | 
						
						
						
							|  |     <div class=\"container\">
 | 
						
						
						
							|  |         {content}
 | 
						
						
						
							|  |     </div>
 | 
						
						
						
							|  | </body>
 | 
						
						
						
							|  | </html>
 | 
						
						
						
							|  | """
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | REPO_ROOT = os.path.abspath(".")
 | 
						
						
						
							|  | LOG_FILE = os.path.join(REPO_ROOT, "gitlog.jsonl")
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | PORT = 8481
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | def format_diff_to_html(diff_text: str) -> str:
 | 
						
						
						
							|  |     lines = diff_text.strip().splitlines()
 | 
						
						
						
							|  |     html_lines = ['<div style="font-family: monospace; white-space: pre;">']
 | 
						
						
						
							|  |     while not lines[3].startswith('diff'):
 | 
						
						
						
							|  |         lines.pop(3)
 | 
						
						
						
							|  |     lines.insert(3, "")
 | 
						
						
						
							|  |     for line in lines:
 | 
						
						
						
							|  |         escaped = html.escape(line)
 | 
						
						
						
							|  |         if "//" in line:
 | 
						
						
						
							|  |             continue 
 | 
						
						
						
							|  |         if "#" in line:
 | 
						
						
						
							|  |             continue
 | 
						
						
						
							|  |         if "/*" in line:
 | 
						
						
						
							|  |             continue
 | 
						
						
						
							|  |         if "*/" in line:
 | 
						
						
						
							|  |             continue
 | 
						
						
						
							|  |         if line.startswith('+++') or line.startswith('---'):
 | 
						
						
						
							|  |             html_lines.append(f'<div style="color: #0000aa;">{escaped}</div>')
 | 
						
						
						
							|  |         elif line.startswith('@@'):
 | 
						
						
						
							|  |             html_lines.append(f'<div style="color: #005cc5;">{escaped}</div>')
 | 
						
						
						
							|  |         elif line.startswith('+'):
 | 
						
						
						
							|  |             html_lines.append(f'<div style="color: #22863a;">{escaped}</div>')
 | 
						
						
						
							|  |         elif line.startswith('-'):
 | 
						
						
						
							|  |             html_lines.append(f'<div style="color: #b31d28;">{escaped}</div>')
 | 
						
						
						
							|  |         elif line.startswith('\\'):
 | 
						
						
						
							|  |             html_lines.append(f'<div style="color: #6a737d;">{escaped}</div>')
 | 
						
						
						
							|  |         else:
 | 
						
						
						
							|  |             html_lines.append(f'<div>{escaped}</div>')
 | 
						
						
						
							|  |     html_lines.append('</div>')
 | 
						
						
						
							|  |     return '\n'.join(html_lines)
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | def parse_logs():
 | 
						
						
						
							|  |     logs = []
 | 
						
						
						
							|  |     if not os.path.exists(LOG_FILE):
 | 
						
						
						
							|  |         return []
 | 
						
						
						
							|  |     lines = []
 | 
						
						
						
							|  |     with open(LOG_FILE, "r", encoding="utf-8") as f:
 | 
						
						
						
							|  |         for line in f:
 | 
						
						
						
							|  |             if line.strip():
 | 
						
						
						
							|  |                 if line.strip() not in lines:
 | 
						
						
						
							|  |                     lines.append(line.strip())
 | 
						
						
						
							|  |                     logs.append(json.loads(line.strip()))
 | 
						
						
						
							|  |     return logs
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | def group_by_date(logs):
 | 
						
						
						
							|  |     grouped = {}
 | 
						
						
						
							|  |     for entry in logs:
 | 
						
						
						
							|  |         date = entry["date"]
 | 
						
						
						
							|  |         grouped.setdefault(date, []).append(entry)
 | 
						
						
						
							|  |     return dict(sorted(grouped.items(), reverse=True))
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | def get_git_diff(commit_hash):
 | 
						
						
						
							|  |     try:
 | 
						
						
						
							|  |         result = subprocess.run(
 | 
						
						
						
							|  |             ["git", "-C", REPO_ROOT, "show", commit_hash, "--no-color"],
 | 
						
						
						
							|  |             stdout=subprocess.PIPE,
 | 
						
						
						
							|  |             stderr=subprocess.PIPE,
 | 
						
						
						
							|  |             text=True,
 | 
						
						
						
							|  |             check=True,
 | 
						
						
						
							|  |         )
 | 
						
						
						
							|  |         return result.stdout
 | 
						
						
						
							|  |     except subprocess.CalledProcessError as e:
 | 
						
						
						
							|  |         return f"Error retrieving diff: {e.stderr}"
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | def list_directory(path, base_url="/browse?path="):
 | 
						
						
						
							|  |     try:
 | 
						
						
						
							|  |         entries = os.listdir(path)
 | 
						
						
						
							|  |     except OSError:
 | 
						
						
						
							|  |         return "<div>Cannot access directory.</div>"
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  |     entries.sort()
 | 
						
						
						
							|  |     content = "<ul>"
 | 
						
						
						
							|  |     # Parent directory link
 | 
						
						
						
							|  |     parent = os.path.dirname(path)
 | 
						
						
						
							|  |     if os.path.abspath(path) != REPO_ROOT:
 | 
						
						
						
							|  |         parent_rel = os.path.relpath(parent, REPO_ROOT)
 | 
						
						
						
							|  |         content += f"<li><a href='{base_url}{html.escape(parent_rel)}'>.. (parent directory)</a></li>"
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  |     for entry in entries:
 | 
						
						
						
							|  |         full_path = os.path.join(path, entry)
 | 
						
						
						
							|  |         rel_path = os.path.relpath(full_path, REPO_ROOT)
 | 
						
						
						
							|  |         if os.path.isdir(full_path):
 | 
						
						
						
							|  |             content += f"<li>📁 <a href='{base_url}{html.escape(rel_path)}'>{html.escape(entry)}/</a></li>"
 | 
						
						
						
							|  |         else:
 | 
						
						
						
							|  |             content += f"<li>📄 <a href='{base_url}{html.escape(rel_path)}'>{html.escape(entry)}</a></li>"
 | 
						
						
						
							|  |     content += "</ul>"
 | 
						
						
						
							|  |     return content
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | def read_file_content(path):
 | 
						
						
						
							|  |     try:
 | 
						
						
						
							|  |         with open(path, "r", encoding="utf-8") as f:
 | 
						
						
						
							|  |             return f.read()
 | 
						
						
						
							|  |     except Exception as e:
 | 
						
						
						
							|  |         return f"Error reading file: {e}"
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | def get_language_class(filename):
 | 
						
						
						
							|  |     ext = os.path.splitext(filename)[1].lower()
 | 
						
						
						
							|  |     return {
 | 
						
						
						
							|  |         '.py': 'python',
 | 
						
						
						
							|  |         '.js': 'javascript',
 | 
						
						
						
							|  |         '.html': 'html',
 | 
						
						
						
							|  |         '.css': 'css',
 | 
						
						
						
							|  |         '.json': 'json',
 | 
						
						
						
							|  |         '.sh': 'bash',
 | 
						
						
						
							|  |         '.md': 'markdown',
 | 
						
						
						
							|  |         '.c': 'c',
 | 
						
						
						
							|  |         '.cpp': 'cpp',
 | 
						
						
						
							|  |         '.h': 'cpp',
 | 
						
						
						
							|  |         '.java': 'java',
 | 
						
						
						
							|  |         '.rb': 'ruby',
 | 
						
						
						
							|  |         '.go': 'go',
 | 
						
						
						
							|  |         '.php': 'php',
 | 
						
						
						
							|  |         '.rs': 'rust',
 | 
						
						
						
							|  |         '.ts': 'typescript',
 | 
						
						
						
							|  |         '.xml': 'xml',
 | 
						
						
						
							|  |         '.yml': 'yaml',
 | 
						
						
						
							|  |         '.yaml': 'yaml',
 | 
						
						
						
							|  |     }.get(ext, '')
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | class GitLogHandler(http.server.SimpleHTTPRequestHandler):
 | 
						
						
						
							|  |     def do_GET(self):
 | 
						
						
						
							|  |         parsed = urlparse(self.path)
 | 
						
						
						
							|  |         if parsed.path == "/":
 | 
						
						
						
							|  |             self.send_response(200)
 | 
						
						
						
							|  |             self.send_header("Content-type", "text/html")
 | 
						
						
						
							|  |             self.end_headers()
 | 
						
						
						
							|  |             logs = parse_logs()
 | 
						
						
						
							|  |             grouped = group_by_date(logs)
 | 
						
						
						
							|  |             content = "<p><a href='/browse'>Browse Files</a></p>"
 | 
						
						
						
							|  |             for date, commits in grouped.items():
 | 
						
						
						
							|  |                 content += f"<div class='date-header'>{date}</div>"
 | 
						
						
						
							|  |                 for c in commits:
 | 
						
						
						
							|  |                     commit_link = f"/diff?hash={c['commit']}"
 | 
						
						
						
							|  |                     content += f"""
 | 
						
						
						
							|  |                     <div class='commit'>
 | 
						
						
						
							|  |                         <div><strong>{c['line'].splitlines()[0]}</strong></div>
 | 
						
						
						
							|  |                         <div class='hash'><a href='{commit_link}'>{c['commit']}</a></div>
 | 
						
						
						
							|  |                     </div>
 | 
						
						
						
							|  |                     """
 | 
						
						
						
							|  |             self.wfile.write(HTML_TEMPLATE(content).encode("utf-8"))
 | 
						
						
						
							|  |         elif parsed.path == "/diff":
 | 
						
						
						
							|  |             qs = parse_qs(parsed.query)
 | 
						
						
						
							|  |             commit = qs.get("hash", [""])[0]
 | 
						
						
						
							|  |             diff = format_diff_to_html(get_git_diff(html.escape(commit)))
 | 
						
						
						
							|  |             diff_html = f"<h2>Commit: {commit}</h2><div class='diff'>{diff}</div><p><a href='/'>← Back to commits</a></p>"
 | 
						
						
						
							|  |             self.send_response(200)
 | 
						
						
						
							|  |             self.send_header("Content-type", "text/html")
 | 
						
						
						
							|  |             self.end_headers()
 | 
						
						
						
							|  |             self.wfile.write(HTML_TEMPLATE(diff_html).encode("utf-8"))
 | 
						
						
						
							|  |         elif parsed.path == "/browse":
 | 
						
						
						
							|  |             qs = parse_qs(parsed.query)
 | 
						
						
						
							|  |             rel_path = qs.get("path", [""])[0]
 | 
						
						
						
							|  |             abs_path = os.path.abspath(os.path.join(REPO_ROOT, rel_path))
 | 
						
						
						
							|  |             # Security: prevent escaping the repo root
 | 
						
						
						
							|  |             if not abs_path.startswith(REPO_ROOT):
 | 
						
						
						
							|  |                 self.send_error(403, "Forbidden")
 | 
						
						
						
							|  |                 return
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  |             if os.path.isdir(abs_path):
 | 
						
						
						
							|  |                 content = f"<h2>Browsing: /{html.escape(rel_path)}</h2>"
 | 
						
						
						
							|  |                 content += list_directory(abs_path)
 | 
						
						
						
							|  |                 content += "<p><a href='/'>← Back to commits</a></p>"
 | 
						
						
						
							|  |                 self.send_response(200)
 | 
						
						
						
							|  |                 self.send_header("Content-type", "text/html")
 | 
						
						
						
							|  |                 self.end_headers()
 | 
						
						
						
							|  |                 self.wfile.write(HTML_TEMPLATE(content).encode("utf-8"))
 | 
						
						
						
							|  |             elif os.path.isfile(abs_path):
 | 
						
						
						
							|  |                 file_content = read_file_content(abs_path)
 | 
						
						
						
							|  |                 lang_class = get_language_class(abs_path)
 | 
						
						
						
							|  |                 content = f"<h2>File: /{html.escape(rel_path)}</h2>"
 | 
						
						
						
							|  |                 content += (
 | 
						
						
						
							|  |                     f"<pre style='background:#f1f1f1; padding:1em; border-radius:4px; overflow-x:auto;'>"
 | 
						
						
						
							|  |                     f"<code class='{lang_class}'>{html.escape(file_content)}</code></pre>"
 | 
						
						
						
							|  |                 )
 | 
						
						
						
							|  |                 content += "<p><a href='{}'>← Back to directory</a></p>".format(
 | 
						
						
						
							|  |                     f"/browse?path={html.escape(os.path.dirname(rel_path))}"
 | 
						
						
						
							|  |                 )
 | 
						
						
						
							|  |                 self.send_response(200)
 | 
						
						
						
							|  |                 self.send_header("Content-type", "text/html")
 | 
						
						
						
							|  |                 self.end_headers()
 | 
						
						
						
							|  |                 self.wfile.write(HTML_TEMPLATE(content).encode("utf-8"))
 | 
						
						
						
							|  |             else:
 | 
						
						
						
							|  |                 self.send_error(404, "Not found")
 | 
						
						
						
							|  |         else:
 | 
						
						
						
							|  |             self.send_error(404)
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | if __name__ == "__main__":
 | 
						
						
						
							|  |     
 | 
						
						
						
							|  |     while True:
 | 
						
						
						
							|  |         try:
 | 
						
						
						
							|  |             with socketserver.TCPServer(("", PORT), GitLogHandler) as httpd:
 | 
						
						
						
							|  |                 print(f"Serving at http://localhost:{PORT}")
 | 
						
						
						
							|  |                 httpd.serve_forever()
 | 
						
						
						
							|  |                 break
 | 
						
						
						
							|  |         except Exception as ex:
 | 
						
						
						
							|  |             print(ex)
 | 
						
						
						
							|  |             PORT += 1
 | 
						
						
						
							|  | 
 | 
						
						
						
							|  | 
 |