#!/usr/bin/env python3
"""
Git Hologram + CAPT Analysis — Visual Overhaul
Dark holographic theme with neural-git graph visualization.
"""
import subprocess, tempfile, shutil, os, hashlib, json, re
from datetime import datetime
from typing import List, Dict
import gradio as gr
from capt_backend import CAPTBackend, health_banner_html, BRAINS
backend = CAPTBackend()
CACHE_DIR = "/tmp/git-hologram-cache"
os.makedirs(CACHE_DIR, exist_ok=True)
# ── Helpers ────────────────────────────────────────────────────────────
def _repo_key(repo_url: str) -> str:
return hashlib.sha256(repo_url.encode()).hexdigest()[:16]
def _get_cached(repo_url: str):
key = _repo_key(repo_url)
meta_path = os.path.join(CACHE_DIR, f"{key}.json")
if os.path.exists(meta_path):
try:
with open(meta_path) as f:
meta = json.load(f)
repo_path = os.path.join(CACHE_DIR, key)
if os.path.isdir(repo_path):
return repo_path, meta
except Exception:
pass
return None, None
def _set_cached(repo_url: str, repo_path: str, meta: dict):
key = _repo_key(repo_url)
dest = os.path.join(CACHE_DIR, key)
if dest != repo_path and os.path.exists(repo_path):
if os.path.exists(dest):
shutil.rmtree(dest, ignore_errors=True)
shutil.move(repo_path, dest)
with open(os.path.join(CACHE_DIR, f"{key}.json"), "w") as f:
json.dump(meta, f)
return dest
def normalize_url(repo_url: str) -> str:
repo_url = repo_url.strip()
if not repo_url:
return ""
if repo_url.startswith("http"):
return repo_url.replace(".git", "") + ".git"
if repo_url.startswith("git@"):
return repo_url
if "/" not in repo_url:
repo_url = f"knowurknot/{repo_url}"
return f"https://github.com/{repo_url}.git"
def get_git_commits(repo_path: str, max_commits: int = 50) -> List[Dict]:
cmd = ["git", "-C", repo_path, "log", f"--max-count={max_commits}",
"--pretty=format:%H|%an|%ae|%ad|%s", "--date=iso"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode != 0:
return []
commits = []
for line in result.stdout.strip().split("\n"):
if "|" in line:
parts = line.split("|", 4)
if len(parts) == 5:
commits.append({"hash": parts[0][:8], "author": parts[1], "email": parts[2],
"date": parts[3], "message": parts[4]})
return commits
def analyze_repo(repo_path: str) -> Dict:
stats = {"files": 0, "languages": {}, "size_kb": 0}
try:
result = subprocess.run(["git", "-C", repo_path, "ls-files"], capture_output=True, text=True, timeout=10)
files = [f for f in result.stdout.strip().split("\n") if f]
stats["files"] = len(files)
for f in files[:500]:
ext = f.split(".")[-1].lower() if "." in f else "none"
if ext in ["py", "js", "ts", "tsx", "jsx", "rs", "go", "java", "cpp", "c", "h", "rb", "php", "swift", "kt"]:
stats["languages"][ext] = stats["languages"].get(ext, 0) + 1
result = subprocess.run(["du", "-sk", repo_path], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
stats["size_kb"] = int(result.stdout.split()[0])
except Exception:
pass
return stats
# ── Visual Renderers ───────────────────────────────────────────────────
LANGUAGE_COLORS = {
"py": "#3b82f6", "js": "#fbbf24", "ts": "#60a5fa", "tsx": "#38bdf8",
"jsx": "#fbbf24", "rs": "#f97316", "go": "#06b6d4", "java": "#ef4444",
"cpp": "#a855f7", "c": "#6366f1", "h": "#8b5cf6", "rb": "#ef4444",
"php": "#8b5cf6", "swift": "#f97316", "kt": "#a855f7",
}
def get_commit_color(msg: str) -> str:
m = msg.lower()
if any(w in m for w in ["feat", "feature", "add", "introduce", "implement"]):
return "#22d3ee" # cyan
if any(w in m for w in ["fix", "bugfix", "patch", "hotfix", "resolve"]):
return "#34d399" # emerald
if any(w in m for w in ["refactor", "clean", "restructure", "simplify"]):
return "#fbbf24" # amber
if any(w in m for w in ["test", "spec", "coverage"]):
return "#a78bfa" # violet
if any(w in m for w in ["doc", "readme", "comment", "guide"]):
return "#60a5fa" # blue
if any(w in m for w in ["chore", "ci", "build", "deps", "bump"]):
return "#94a3b8" # slate
if any(w in m for w in ["merge", "pull request", "pr "]):
return "#f472b6" # pink
return "#64748b" # gray
def render_stats(stats: Dict) -> str:
if not stats or not stats.get("languages"):
return "
No language data
"
langs = stats["languages"]
total = sum(langs.values())
bars = []
for lang, count in sorted(langs.items(), key=lambda x: -x[1]):
pct = count / total * 100
color = LANGUAGE_COLORS.get(lang, "#64748b")
bars.append(f'''
{lang}
{count} · {pct:.1f}%
''')
size_mb = stats.get("size_kb", 0) / 1024
size_str = f"{size_mb:.1f} MB" if size_mb > 1 else f"{stats.get('size_kb', 0)} KB"
return f'''
{stats.get('files', 0)}
Files
Languages
{''.join(bars)}
'''
def render_commits(commits: List[Dict]) -> str:
if not commits:
return "No commits found.
"
authors = {}
for c in commits:
authors[c["author"]] = authors.get(c["author"], 0) + 1
top_author = max(authors, key=authors.get) if authors else "Unknown"
# Build a visual git graph
rows = []
for i, c in enumerate(commits[:40]):
msg = c["message"][:60] + ("..." if len(c["message"]) > 60 else "")
date = c["date"][:10] if len(c["date"]) > 10 else c["date"]
color = get_commit_color(c["message"])
initials = ''.join(p[0].upper() for p in c["author"].split() if p)[:2]
# Alternate branch line offset for visual variety
offset = (i % 3) * 8
rows.append(f'''
{initials}
{msg}
{c["hash"]} · {date}
''')
more = f'+ {len(commits) - 40} more commits
' if len(commits) > 40 else ""
return f'''
{top_author.split()[0]}
Top
{''.join(rows)}
{more}
'''
def render_analysis(result: Dict, analysis_type: str) -> str:
if not result.get("success"):
err = result.get("error", "Unknown error")
return f''''''
resp = result.get("response", {})
if isinstance(resp, dict):
text = resp.get("response") or resp.get("choices", [{}])[0].get("message", {}).get("content", "")
else:
text = str(resp)
if not text or text.strip() == "":
text = "*The backend returned an empty response. Try again.*"
# Convert markdown to styled HTML
text = text.replace("\n\n", "").replace("\n", "
")
text = f"
{text}
"
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
text = re.sub(r'`(.+?)`', r'\1', text)
text = re.sub(r'#{3,6}\s*(.+?)(?=<|$)', r'\1
', text)
brain = result.get("brain", "capt").upper()
fallback = " · fallback" if result.get("fallback") else ""
icons = {"codebase": "🔍", "commits": "📊", "security": "🛡️"}
titles = {"codebase": "Codebase Analysis", "commits": "Commit Patterns", "security": "Security Audit"}
return f'''
{icons.get(analysis_type, "🧠")}
{titles.get(analysis_type, "Analysis")}
● {brain}{fallback}
{text}
'''
# ── Core Logic ─────────────────────────────────────────────────────────
def fetch_and_render(repo_url: str, max_commits: int):
repo_url = normalize_url(repo_url)
if not repo_url:
return "Please enter a repository URL.
", "", ""
cached_path, meta = _get_cached(repo_url)
if cached_path:
commits = get_git_commits(cached_path, max_commits)
if commits:
stats = analyze_repo(cached_path)
return render_commits(commits), repo_url, render_stats(stats)
tmpdir = tempfile.mkdtemp(prefix="hologram_")
try:
depth = max(max_commits + 10, 50)
result = subprocess.run(
["git", "clone", "--depth", str(depth), "--no-tags", repo_url, tmpdir],
capture_output=True, text=True, timeout=60,
)
if result.returncode != 0:
err = result.stderr[:200] if result.stderr else "Unknown clone error"
return f"❌ Clone failed: {err}
", repo_url, ""
commits = get_git_commits(tmpdir, max_commits)
if not commits:
return "No commits found.
", repo_url, ""
stats = analyze_repo(tmpdir)
_set_cached(repo_url, tmpdir, {"url": repo_url, "commits": len(commits), "cached_at": datetime.now().isoformat()})
return render_commits(commits), repo_url, render_stats(stats)
except subprocess.TimeoutExpired:
return "⏱️ Clone timed out — repo too large.
", repo_url, ""
except Exception as e:
return f"💥 Error: {str(e)[:200]}
", repo_url, ""
finally:
if os.path.exists(tmpdir) and tmpdir != cached_path:
shutil.rmtree(tmpdir, ignore_errors=True)
def run_analysis(repo_url: str, analysis_type: str):
if not repo_url:
return "Generate a hologram first.
"
prompts = {
"codebase": f"Analyze the codebase at {repo_url}. Provide: 1) Architecture overview, 2) Tech stack, 3) Code quality assessment, 4) Key files and their roles, 5) Suggestions for improvement. Be thorough but concise.",
"commits": f"Analyze the commit history patterns for {repo_url}. What do the commit messages reveal about development velocity, team structure, and code maturity?",
"security": f"Perform a security audit of {repo_url}. Check for: 1) Common vulnerabilities, 2) Dependency risks, 3) Secret leakage patterns, 4) Supply chain concerns. Be specific and actionable.",
}
fallback_prompts = {
"codebase": f"Brief codebase overview of {repo_url}",
"commits": f"Commit patterns in {repo_url}",
"security": f"Security notes for {repo_url}",
}
prompt = prompts.get(analysis_type, prompts["codebase"])
for attempt, p in enumerate([prompt, fallback_prompts.get(analysis_type, "")], 1):
if not p:
continue
try:
result = backend.cogitate(p)
if result and result.get("success"):
return render_analysis(result, analysis_type)
except Exception:
pass
return '''
⏳ The CAPT brains are warming up
The cognitive backend is experiencing high load or cold-start latency.
Please wait a moment and try again.
Short queries usually work immediately.
'''
# ── CSS ────────────────────────────────────────────────────────────────
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
.gradio-container {
max-width: 1200px !important;
font-family: 'Inter', system-ui, sans-serif !important;
}
/* Dark holographic background */
.gradio-container {
background: linear-gradient(135deg, #0b0f1a 0%, #0f172a 40%, #1a103c 100%) !important;
background-attachment: fixed !important;
}
/* Custom scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #475569; }
/* Primary button — cyan glow */
.gr-button-primary {
background: linear-gradient(135deg, #0891b2, #06b6d4) !important;
border: none !important;
border-radius: 10px !important;
box-shadow: 0 0 20px rgba(6,182,212,0.3), 0 4px 14px rgba(6,182,212,0.2) !important;
color: #fff !important;
font-weight: 600 !important;
letter-spacing: 0.3px !important;
transition: all 0.2s ease !important;
}
.gr-button-primary:hover {
box-shadow: 0 0 30px rgba(6,182,212,0.5), 0 6px 20px rgba(6,182,212,0.3) !important;
transform: translateY(-1px) !important;
}
/* Secondary elements */
.gr-box, .gr-form, .gr-panel {
background: rgba(15, 23, 42, 0.5) !important;
border: 1px solid rgba(51, 65, 85, 0.4) !important;
border-radius: 14px !important;
backdrop-filter: blur(12px) !important;
}
/* Inputs */
.gr-input, .gr-textbox textarea, .gr-textbox input {
background: rgba(15, 23, 42, 0.6) !important;
border: 1px solid rgba(51, 65, 85, 0.5) !important;
border-radius: 10px !important;
color: #e2e8f0 !important;
font-family: 'JetBrains Mono', monospace !important;
}
.gr-input:focus, .gr-textbox textarea:focus {
border-color: #22d3ee !important;
box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.1) !important;
}
/* Labels */
.gr-form .gr-box > label, .gr-input-label {
color: #94a3b8 !important;
font-size: 11px !important;
text-transform: uppercase !important;
letter-spacing: 1px !important;
font-weight: 600 !important;
}
/* Dropdown */
.gr-dropdown {
background: rgba(15, 23, 42, 0.6) !important;
border: 1px solid rgba(51, 65, 85, 0.5) !important;
border-radius: 10px !important;
color: #e2e8f0 !important;
}
/* Slider */
.gr-slider input[type="range"] {
accent-color: #22d3ee !important;
}
/* JSON panel */
.gr-json {
background: rgba(15, 23, 42, 0.4) !important;
border: 1px solid rgba(51, 65, 85, 0.3) !important;
border-radius: 12px !important;
color: #e2e8f0 !important;
font-family: 'JetBrains Mono', monospace !important;
font-size: 12px !important;
}
/* Commit row hover */
.commit-row:hover {
background: rgba(34, 211, 238, 0.05) !important;
}
"""
# ── Gradio UI ──────────────────────────────────────────────────────────
with gr.Blocks(
title="Git Hologram",
css=CUSTOM_CSS,
theme=gr.themes.Base(
primary_hue="cyan",
secondary_hue="violet",
neutral_hue="slate",
).set(
body_background_fill="transparent",
body_text_color="#e2e8f0",
block_background_fill="rgba(15,23,42,0.5)",
block_border_color="rgba(51,65,85,0.4)",
block_border_width="1px",
block_label_text_color="#94a3b8",
input_background_fill="rgba(15,23,42,0.6)",
input_border_color="rgba(51,65,85,0.5)",
input_placeholder_color="#475569",
button_primary_background_fill="linear-gradient(135deg, #0891b2, #06b6d4)",
button_primary_text_color="#ffffff",
)
) as demo:
# Hero header
gr.HTML('''
🔮
Git Hologram
Visualize repository history as a living neural graph.
Clone any repo and analyze it through the CAPT cognitive lens.
Git Clone
Commit Graph
CAPT Analysis
''')
# Brain status
gr.HTML(health_banner_html(backend))
# Input row
with gr.Row(equal_height=True):
with gr.Column(scale=4):
repo_input = gr.Textbox(
label="Repository",
placeholder="owner/repo or https://github.com/owner/repo",
lines=1,
)
with gr.Column(scale=1):
max_commits = gr.Slider(minimum=10, maximum=200, value=50, step=10, label="Max Commits")
with gr.Column(scale=1):
generate_btn = gr.Button("🔮 Generate Hologram", variant="primary", size="lg")
# Results
with gr.Row():
with gr.Column(scale=3):
hologram_output = gr.HTML(label="Commit Neural Graph")
with gr.Column(scale=2):
stats_output = gr.HTML(label="Repository Telemetry")
current_repo = gr.State("")
# Analysis section
with gr.Row():
with gr.Column(scale=3):
analysis_type = gr.Dropdown(
choices=[("🔍 Codebase Analysis", "codebase"), ("📊 Commit Patterns", "commits"), ("🛡️ Security Audit", "security")],
value="codebase",
label="Analysis Type",
)
with gr.Column(scale=1):
analyze_btn = gr.Button("🧠 Analyze with CAPT", variant="primary")
analysis_output = gr.HTML(label="CAPT Analysis")
# Footer
gr.HTML('''
Git Hologram
·
Powered by CAPT
·
5 Brain Instances
Real-time cognitive analysis through Cloudflare Workers
''')
# Event wiring
generate_btn.click(
fn=fetch_and_render,
inputs=[repo_input, max_commits],
outputs=[hologram_output, current_repo, stats_output]
)
repo_input.submit(
fn=fetch_and_render,
inputs=[repo_input, max_commits],
outputs=[hologram_output, current_repo, stats_output]
)
analyze_btn.click(
fn=run_analysis,
inputs=[current_repo, analysis_type],
outputs=analysis_output
)
if __name__ == "__main__":
demo.launch(show_error=True, server_name="0.0.0.0", server_port=7860)