|
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
|
|
|
|
|