399 lines
17 KiB
HTML
399 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
{{template "head" .}}
|
|
<link rel="stylesheet" href="/static/lib/diff2html.min.css">
|
|
<script src="/static/lib/d3.min.js"></script>
|
|
<script src="/static/lib/diff2html.min.js"></script>
|
|
<script src="/static/lib/highlight.min.js"></script>
|
|
<script src="/static/js/sse.js"></script>
|
|
<script src="/static/js/graph.js"></script>
|
|
<script src="/static/js/diff-viewer.js"></script>
|
|
<style>
|
|
.graph-container {
|
|
background: #f8fafc;
|
|
border-radius: 8px;
|
|
min-height: 300px;
|
|
overflow: auto;
|
|
}
|
|
.graph-container svg {
|
|
display: block;
|
|
}
|
|
.selection-hint {
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
padding: 6px 12px;
|
|
background: #f3f4f6;
|
|
border-radius: 6px;
|
|
display: inline-block;
|
|
}
|
|
.selection-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-family: monospace;
|
|
}
|
|
.badge-base {
|
|
background: #d1fae5;
|
|
color: #065f46;
|
|
border: 1px solid #a7f3d0;
|
|
}
|
|
.badge-head {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
border: 1px solid #fde68a;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50 min-h-screen">
|
|
{{template "nav" .}}
|
|
|
|
<main class="max-w-7xl mx-auto px-4 py-8">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900">仓库详情</h1>
|
|
<p id="repo-url" class="text-gray-500 text-sm mt-1"></p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<a id="btn-generate" href="#" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
|
|
生成 PR 描述
|
|
</a>
|
|
<a id="btn-review" href="#" class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
|
|
AI 代码审查
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div id="loading" class="bg-white rounded-lg shadow-md p-8 text-center">
|
|
<div class="spinner inline-block"></div>
|
|
<p class="mt-2 text-gray-500">加载仓库数据...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="error" class="bg-red-50 border border-red-200 rounded-lg p-4 hidden">
|
|
<p class="text-red-600" id="error-message"></p>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div id="main-content" class="hidden">
|
|
<!-- Shared Selection Bar -->
|
|
<div class="bg-white rounded-lg shadow-md p-4 mb-4">
|
|
<div class="flex items-center gap-4 flex-wrap">
|
|
<div class="flex items-center gap-2">
|
|
<label class="text-sm font-medium text-gray-600">Base:</label>
|
|
<select id="select-base" class="border rounded px-3 py-1.5 text-sm min-w-[160px]">
|
|
<option value="">选择分支/标签</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<label class="text-sm font-medium text-gray-600">Head:</label>
|
|
<select id="select-head" class="border rounded px-3 py-1.5 text-sm min-w-[160px]">
|
|
<option value="">选择分支/标签</option>
|
|
</select>
|
|
</div>
|
|
<button onclick="loadDiff()" class="px-4 py-1.5 bg-blue-600 text-white rounded text-sm hover:bg-blue-700">
|
|
查看 Diff
|
|
</button>
|
|
<div class="flex gap-1 ml-auto">
|
|
<button onclick="setDiffView('side-by-side')" id="btn-side-by-side" class="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded font-medium">并排</button>
|
|
<button onclick="setDiffView('unified')" id="btn-unified" class="px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded hover:bg-gray-200">统一</button>
|
|
</div>
|
|
</div>
|
|
<div class="mt-2 flex items-center gap-3">
|
|
<span class="selection-hint" id="selection-hint">
|
|
💡 在 Git Graph 中点击节点选择 Base(绿)和 Head(橙)
|
|
</span>
|
|
<span id="selection-display" class="hidden">
|
|
<span id="sel-base-badge" class="selection-badge badge-base"></span>
|
|
<span class="text-gray-400 mx-1">→</span>
|
|
<span id="sel-head-badge" class="selection-badge badge-head"></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="bg-white rounded-lg shadow-md">
|
|
<div class="border-b border-gray-200">
|
|
<nav class="flex -mb-px">
|
|
<button onclick="showTab('graph')" id="tab-graph" class="px-6 py-3 text-sm font-medium border-b-2 border-blue-500 text-blue-600">
|
|
Git Graph
|
|
</button>
|
|
<button onclick="showTab('diff')" id="tab-diff" class="px-6 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
|
|
Diff 视图
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Graph Tab -->
|
|
<div id="panel-graph" class="p-4">
|
|
<div id="git-graph" class="graph-container p-4"></div>
|
|
</div>
|
|
|
|
<!-- Diff Tab -->
|
|
<div id="panel-diff" class="p-4 hidden">
|
|
<div id="diff-container"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
{{template "footer" .}}
|
|
|
|
<script>
|
|
const repoId = '{{.ID}}';
|
|
let currentRefs = [];
|
|
|
|
async function init() {
|
|
try {
|
|
// Load repo info
|
|
const reposResp = await fetch('/api/repos');
|
|
if (reposResp.ok) {
|
|
const repos = await reposResp.json();
|
|
const repo = repos.find(r => r.id == repoId || r.id === parseInt(repoId));
|
|
if (repo) {
|
|
document.getElementById('repo-url').textContent = repo.url;
|
|
document.getElementById('btn-generate').href = `/repo/${repoId}/generate`;
|
|
document.getElementById('btn-review').href = `/repo/${repoId}/review`;
|
|
} else {
|
|
showError('仓库未找到 (ID: ' + repoId + ')');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Load refs
|
|
const refsResp = await fetch(`/api/repos/${repoId}/refs`);
|
|
if (refsResp.ok) {
|
|
currentRefs = await refsResp.json();
|
|
populateRefSelects(currentRefs);
|
|
updateButtonLinks();
|
|
}
|
|
|
|
// Init graph with click-to-select
|
|
GitGraph.init('git-graph', repoId);
|
|
GitGraph.onSelectionChange = onGraphSelectionChange;
|
|
|
|
// Init diff viewer
|
|
DiffViewer.init('diff-container');
|
|
|
|
// Show main content
|
|
document.getElementById('loading').classList.add('hidden');
|
|
document.getElementById('main-content').classList.remove('hidden');
|
|
|
|
} catch (err) {
|
|
console.error('Init error:', err);
|
|
showError('加载失败: ' + err.message);
|
|
}
|
|
}
|
|
|
|
function showError(message) {
|
|
document.getElementById('loading').classList.add('hidden');
|
|
document.getElementById('error').classList.remove('hidden');
|
|
document.getElementById('error-message').textContent = message;
|
|
}
|
|
|
|
function populateRefSelects(refs) {
|
|
const selects = ['select-base', 'select-head'];
|
|
const branches = refs.filter(r => !r.is_tag);
|
|
const tags = refs.filter(r => r.is_tag);
|
|
|
|
selects.forEach(selectId => {
|
|
const select = document.getElementById(selectId);
|
|
select.innerHTML = '<option value="">选择分支/标签</option>';
|
|
|
|
if (branches.length > 0) {
|
|
const group = document.createElement('optgroup');
|
|
group.label = '分支';
|
|
branches.forEach(ref => {
|
|
const opt = document.createElement('option');
|
|
opt.value = ref.name;
|
|
opt.dataset.hash = ref.hash;
|
|
opt.textContent = ref.name + (ref.is_head ? ' (HEAD)' : '');
|
|
group.appendChild(opt);
|
|
});
|
|
select.appendChild(group);
|
|
}
|
|
|
|
if (tags.length > 0) {
|
|
const group = document.createElement('optgroup');
|
|
group.label = '标签';
|
|
tags.forEach(ref => {
|
|
const opt = document.createElement('option');
|
|
opt.value = ref.name;
|
|
opt.dataset.hash = ref.hash;
|
|
opt.textContent = ref.name;
|
|
group.appendChild(opt);
|
|
});
|
|
select.appendChild(group);
|
|
}
|
|
|
|
// Add "Recent Commits" group
|
|
if (GitGraph.commits && GitGraph.commits.length > 0) {
|
|
const group = document.createElement('optgroup');
|
|
group.label = '最近提交';
|
|
GitGraph.commits.slice(0, 20).forEach(commit => {
|
|
const opt = document.createElement('option');
|
|
opt.value = commit.hash;
|
|
opt.dataset.hash = commit.hash;
|
|
opt.textContent = `${commit.short_hash} ${commit.message.substring(0, 40)}`;
|
|
group.appendChild(opt);
|
|
});
|
|
select.appendChild(group);
|
|
}
|
|
});
|
|
|
|
// Auto-select main/master as base, HEAD as head
|
|
const mainBranch = branches.find(b => b.name === 'main' || b.name === 'master');
|
|
const headBranch = refs.find(r => r.is_head);
|
|
|
|
if (mainBranch) {
|
|
document.getElementById('select-base').value = mainBranch.name;
|
|
}
|
|
if (headBranch) {
|
|
document.getElementById('select-head').value = headBranch.name;
|
|
}
|
|
|
|
// Sync dropdowns to graph
|
|
syncDropdownsToGraph();
|
|
}
|
|
|
|
// Called when user clicks a commit node in the graph
|
|
function onGraphSelectionChange(baseHash, headHash) {
|
|
const baseSelect = document.getElementById('select-base');
|
|
const headSelect = document.getElementById('select-head');
|
|
const selectionDisplay = document.getElementById('selection-display');
|
|
const selectionHint = document.getElementById('selection-hint');
|
|
|
|
// Try to match hash to a ref name
|
|
const baseRef = currentRefs.find(r => r.hash === baseHash);
|
|
const headRef = currentRefs.find(r => r.hash === headHash);
|
|
|
|
if (baseHash) {
|
|
const val = baseRef ? baseRef.name : baseHash;
|
|
// Check if value exists as an option, add if not
|
|
ensureOption(baseSelect, val, baseRef ? baseRef.name : GitGraph.getRefName(baseHash));
|
|
baseSelect.value = val;
|
|
|
|
document.getElementById('sel-base-badge').textContent = baseRef ? baseRef.name : GitGraph.getRefName(baseHash);
|
|
} else {
|
|
baseSelect.value = '';
|
|
}
|
|
|
|
if (headHash) {
|
|
const val = headRef ? headRef.name : headHash;
|
|
ensureOption(headSelect, val, headRef ? headRef.name : GitGraph.getRefName(headHash));
|
|
headSelect.value = val;
|
|
|
|
document.getElementById('sel-head-badge').textContent = headRef ? headRef.name : GitGraph.getRefName(headHash);
|
|
} else {
|
|
headSelect.value = '';
|
|
}
|
|
|
|
// Update display
|
|
if (baseHash || headHash) {
|
|
selectionDisplay.classList.remove('hidden');
|
|
selectionHint.classList.add('hidden');
|
|
} else {
|
|
selectionDisplay.classList.add('hidden');
|
|
selectionHint.classList.remove('hidden');
|
|
}
|
|
|
|
// If both selected, auto-load diff
|
|
if (baseHash && headHash) {
|
|
showTab('diff');
|
|
loadDiff();
|
|
}
|
|
|
|
updateButtonLinks();
|
|
}
|
|
|
|
// Ensure a select has an option with given value
|
|
function ensureOption(select, value, label) {
|
|
for (const opt of select.options) {
|
|
if (opt.value === value) return;
|
|
}
|
|
const opt = document.createElement('option');
|
|
opt.value = value;
|
|
opt.textContent = label || value;
|
|
select.appendChild(opt);
|
|
}
|
|
|
|
// Sync dropdown selections to graph highlights
|
|
function syncDropdownsToGraph() {
|
|
const baseSelect = document.getElementById('select-base');
|
|
const headSelect = document.getElementById('select-head');
|
|
|
|
const sync = () => {
|
|
const baseOpt = baseSelect.options[baseSelect.selectedIndex];
|
|
const headOpt = headSelect.options[headSelect.selectedIndex];
|
|
const baseHash = baseOpt ? (baseOpt.dataset.hash || '') : '';
|
|
const headHash = headOpt ? (headOpt.dataset.hash || '') : '';
|
|
if (baseHash || headHash) {
|
|
GitGraph.setSelection(baseHash || null, headHash || null);
|
|
}
|
|
updateButtonLinks();
|
|
};
|
|
|
|
baseSelect.addEventListener('change', sync);
|
|
headSelect.addEventListener('change', sync);
|
|
}
|
|
|
|
function updateButtonLinks() {
|
|
const base = document.getElementById('select-base').value;
|
|
const head = document.getElementById('select-head').value;
|
|
const params = new URLSearchParams();
|
|
if (base) params.set('base', base);
|
|
if (head) params.set('head', head);
|
|
const qs = params.toString();
|
|
const suffix = qs ? '?' + qs : '';
|
|
document.getElementById('btn-generate').href = `/repo/${repoId}/generate${suffix}`;
|
|
document.getElementById('btn-review').href = `/repo/${repoId}/review${suffix}`;
|
|
}
|
|
|
|
function showTab(tab) {
|
|
document.getElementById('panel-graph').classList.toggle('hidden', tab !== 'graph');
|
|
document.getElementById('panel-diff').classList.toggle('hidden', tab !== 'diff');
|
|
document.getElementById('tab-graph').classList.toggle('border-blue-500', tab === 'graph');
|
|
document.getElementById('tab-graph').classList.toggle('text-blue-600', tab === 'graph');
|
|
document.getElementById('tab-graph').classList.toggle('border-transparent', tab !== 'graph');
|
|
document.getElementById('tab-graph').classList.toggle('text-gray-500', tab !== 'graph');
|
|
document.getElementById('tab-diff').classList.toggle('border-blue-500', tab === 'diff');
|
|
document.getElementById('tab-diff').classList.toggle('text-blue-600', tab === 'diff');
|
|
document.getElementById('tab-diff').classList.toggle('border-transparent', tab !== 'diff');
|
|
document.getElementById('tab-diff').classList.toggle('text-gray-500', tab !== 'diff');
|
|
}
|
|
|
|
function loadDiff() {
|
|
const base = document.getElementById('select-base').value;
|
|
const head = document.getElementById('select-head').value;
|
|
if (!base || !head) {
|
|
alert('请选择 Base 和 Head 分支');
|
|
return;
|
|
}
|
|
DiffViewer.loadDiff(repoId, base, head);
|
|
}
|
|
|
|
function setDiffView(mode) {
|
|
DiffViewer.setViewMode(mode);
|
|
|
|
// Update button styles
|
|
const btnSide = document.getElementById('btn-side-by-side');
|
|
const btnUnified = document.getElementById('btn-unified');
|
|
|
|
if (mode === 'side-by-side') {
|
|
btnSide.className = 'px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded font-medium';
|
|
btnUnified.className = 'px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded hover:bg-gray-200';
|
|
} else {
|
|
btnUnified.className = 'px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded font-medium';
|
|
btnSide.className = 'px-3 py-1 text-xs bg-gray-100 text-gray-600 rounded hover:bg-gray-200';
|
|
}
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|