fix: 改进 SSE 克隆处理,仓库页面添加调试信息

This commit is contained in:
2026-06-18 23:04:58 +08:00
parent 34ef868e10
commit 4f873cf2ce
3 changed files with 195 additions and 85 deletions
+1
View File
@@ -110,6 +110,7 @@ func (h *ReposHandler) CloneRepo(c *gin.Context) {
c.Header("Cache-Control", "no-cache") c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive") c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no") c.Header("X-Accel-Buffering", "no")
c.Status(http.StatusOK)
flusher, ok := c.Writer.(http.Flusher) flusher, ok := c.Writer.(http.Flusher)
if !ok { if !ok {
+45 -6
View File
@@ -78,41 +78,80 @@ document.getElementById('clone-form').addEventListener('submit', async function(
const bar = document.getElementById('clone-bar'); const bar = document.getElementById('clone-bar');
const errorEl = document.getElementById('clone-error'); const errorEl = document.getElementById('clone-error');
btn.disabled = true; btn.disabled = true;
btn.textContent = '克隆中...';
progress.classList.remove('hidden'); progress.classList.remove('hidden');
errorEl.classList.add('hidden'); errorEl.classList.add('hidden');
bar.style.width = '0%'; bar.style.width = '0%';
status.textContent = '正在连接...';
try { try {
const resp = await fetch('/api/repos', { const resp = await fetch('/api/repos', {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({url: url}) body: JSON.stringify({url: url})
}); });
if (!resp.ok) { const data = await resp.json(); throw new Error(data.error || 'clone failed'); }
if (!resp.ok) {
const data = await resp.json();
throw new Error(data.error || 'clone failed');
}
const reader = resp.body.getReader(); const reader = resp.body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = ''; let buffer = '';
let currentEvent = '';
while (true) { while (true) {
const {done, value} = await reader.read(); const {done, value} = await reader.read();
if (done) break; if (done) break;
buffer += decoder.decode(value, {stream: true}); buffer += decoder.decode(value, {stream: true});
const lines = buffer.split('\n'); const lines = buffer.split('\n');
buffer = lines.pop() || ''; buffer = lines.pop() || '';
for (const line of lines) { for (const line of lines) {
if (line.startsWith('data: ')) { if (line.startsWith('event: ')) {
currentEvent = line.slice(7).trim();
} else if (line.startsWith('data: ')) {
try { try {
const data = JSON.parse(line.slice(6)); const data = JSON.parse(line.slice(6));
if (data.progress !== undefined) bar.style.width = data.progress + '%';
if (data.message) status.textContent = data.message; if (currentEvent === 'start') {
if (data.done) { window.location.href = '/repo/' + data.repo_id; return; } status.textContent = '开始克隆...';
} catch {} bar.style.width = '10%';
} else if (currentEvent === 'progress') {
status.textContent = data.step || '处理中...';
if (data.current !== undefined && data.total > 0) {
bar.style.width = Math.round((data.current / data.total) * 80 + 10) + '%';
} else {
bar.style.width = '50%';
}
} else if (currentEvent === 'complete') {
bar.style.width = '100%';
status.textContent = '克隆完成!';
// Redirect to repo page
setTimeout(() => {
window.location.href = '/repo/' + data.repo_id;
}, 500);
return;
} else if (currentEvent === 'error') {
throw new Error(data.message || '克隆失败');
}
} catch (parseErr) {
if (parseErr.message && !parseErr.message.includes('JSON')) {
throw parseErr;
}
}
} }
} }
} }
} catch (err) { } catch (err) {
errorEl.textContent = err.message; errorEl.textContent = err.message;
errorEl.classList.remove('hidden'); errorEl.classList.remove('hidden');
progress.classList.add('hidden');
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.textContent = '克隆';
} }
}); });
</script> </script>
+149 -79
View File
@@ -18,12 +18,6 @@
.graph-container svg { .graph-container svg {
display: block; display: block;
} }
.ref-tag {
cursor: pointer;
}
.ref-tag:hover {
opacity: 0.8;
}
</style> </style>
</head> </head>
<body class="bg-gray-50 min-h-screen"> <body class="bg-gray-50 min-h-screen">
@@ -31,9 +25,10 @@
<main class="max-w-7xl mx-auto px-4 py-8"> <main class="max-w-7xl mx-auto px-4 py-8">
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-900"> <div>
<span id="repo-url" class="text-gray-500 text-lg font-normal"></span> <h1 class="text-2xl font-bold text-gray-900">仓库详情</h1>
</h1> <p id="repo-url" class="text-gray-500 text-sm mt-1"></p>
</div>
<div class="flex gap-2"> <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"> <a id="btn-generate" href="#" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
生成 PR 描述 生成 PR 描述
@@ -44,62 +39,84 @@
</div> </div>
</div> </div>
<!-- Tabs --> <!-- Debug Info -->
<div class="bg-white rounded-lg shadow-md"> <div id="debug-info" class="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4 text-sm hidden">
<div class="border-b border-gray-200"> <p><strong>调试信息:</strong></p>
<nav class="flex -mb-px"> <p>仓库 ID: <span id="debug-id"></span></p>
<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"> <p>Refs: <span id="debug-refs"></span></p>
Git Graph <p>Graph: <span id="debug-graph"></span></p>
</button> </div>
<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 --> <!-- Loading State -->
<div id="panel-graph" class="p-4"> <div id="loading" class="bg-white rounded-lg shadow-md p-8 text-center">
<div class="flex items-center gap-4 mb-4"> <div class="spinner inline-block"></div>
<div class="flex items-center gap-2"> <p class="mt-2 text-gray-500">加载仓库数据...</p>
<label class="text-sm text-gray-600">Base:</label> </div>
<select id="graph-base" class="border rounded px-2 py-1 text-sm">
<option value="">选择分支/标签</option>
</select>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600">Head:</label>
<select id="graph-head" class="border rounded px-2 py-1 text-sm">
<option value="">选择分支/标签</option>
</select>
</div>
</div>
<div id="git-graph" class="graph-container p-4"></div>
</div>
<!-- Diff Tab --> <!-- Error State -->
<div id="panel-diff" class="p-4 hidden"> <div id="error" class="bg-red-50 border border-red-200 rounded-lg p-4 hidden">
<div class="flex items-center gap-4 mb-4"> <p class="text-red-600" id="error-message"></p>
<div class="flex items-center gap-2"> </div>
<label class="text-sm text-gray-600">Base:</label>
<select id="diff-base" class="border rounded px-2 py-1 text-sm"> <!-- Main Content -->
<option value="">选择分支/标签</option> <div id="main-content" class="hidden">
</select> <!-- Tabs -->
</div> <div class="bg-white rounded-lg shadow-md">
<div class="flex items-center gap-2"> <div class="border-b border-gray-200">
<label class="text-sm text-gray-600">Head:</label> <nav class="flex -mb-px">
<select id="diff-head" class="border rounded px-2 py-1 text-sm"> <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">
<option value="">选择分支/标签</option> Git Graph
</select> </button>
</div> <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">
<button onclick="loadDiff()" class="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700"> Diff 视图
查看 Diff </button>
</button> </nav>
<div class="flex gap-1 ml-auto"> </div>
<button onclick="setDiffView('side-by-side')" class="px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200">并排</button>
<button onclick="setDiffView('unified')" class="px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200">统一</button> <!-- Graph Tab -->
</div> <div id="panel-graph" class="p-4">
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600">Base:</label>
<select id="graph-base" class="border rounded px-2 py-1 text-sm">
<option value="">选择分支/标签</option>
</select>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600">Head:</label>
<select id="graph-head" class="border rounded px-2 py-1 text-sm">
<option value="">选择分支/标签</option>
</select>
</div>
</div>
<div id="git-graph" class="graph-container p-4"></div>
</div>
<!-- Diff Tab -->
<div id="panel-diff" class="p-4 hidden">
<div class="flex items-center gap-4 mb-4">
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600">Base:</label>
<select id="diff-base" class="border rounded px-2 py-1 text-sm">
<option value="">选择分支/标签</option>
</select>
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600">Head:</label>
<select id="diff-head" class="border rounded px-2 py-1 text-sm">
<option value="">选择分支/标签</option>
</select>
</div>
<button onclick="loadDiff()" class="px-3 py-1 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')" class="px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200">并排</button>
<button onclick="setDiffView('unified')" class="px-2 py-1 text-xs bg-gray-100 rounded hover:bg-gray-200">统一</button>
</div>
</div>
<div id="diff-container"></div>
</div> </div>
<div id="diff-container"></div>
</div> </div>
</div> </div>
</main> </main>
@@ -107,50 +124,103 @@
{{template "footer" .}} {{template "footer" .}}
<script> <script>
const repoId = {{.ID}}; const repoId = '{{.ID}}';
let currentRefs = []; let currentRefs = [];
// Load repo info and refs // Show debug info in development
async function init() { const showDebug = true;
try {
// Load refs
const refsResp = await fetch(`/api/repos/${repoId}/refs`);
if (refsResp.ok) {
currentRefs = await refsResp.json();
populateRefSelects(currentRefs);
}
// Load repo info async function init() {
console.log('Initializing repo page, ID:', repoId);
if (showDebug) {
document.getElementById('debug-info').classList.remove('hidden');
document.getElementById('debug-id').textContent = repoId;
}
try {
// Load repo info first
const reposResp = await fetch('/api/repos'); const reposResp = await fetch('/api/repos');
if (reposResp.ok) { if (reposResp.ok) {
const repos = await reposResp.json(); const repos = await reposResp.json();
const repo = repos.find(r => r.id === repoId); console.log('Repos:', repos);
const repo = repos.find(r => r.id == repoId || r.id === parseInt(repoId));
if (repo) { if (repo) {
document.getElementById('repo-url').textContent = repo.url; document.getElementById('repo-url').textContent = repo.url;
document.getElementById('btn-generate').href = `/repo/${repoId}/generate`; document.getElementById('btn-generate').href = `/repo/${repoId}/generate`;
document.getElementById('btn-review').href = `/repo/${repoId}/review`; document.getElementById('btn-review').href = `/repo/${repoId}/review`;
} else {
showError('仓库未找到 (ID: ' + repoId + ')');
return;
}
}
// Load refs
console.log('Loading refs...');
const refsResp = await fetch(`/api/repos/${repoId}/refs`);
if (refsResp.ok) {
currentRefs = await refsResp.json();
console.log('Refs loaded:', currentRefs);
if (showDebug) {
document.getElementById('debug-refs').textContent = currentRefs.length + ' refs';
}
populateRefSelects(currentRefs);
} else {
console.error('Failed to load refs:', refsResp.status);
if (showDebug) {
document.getElementById('debug-refs').textContent = 'Error: ' + refsResp.status;
} }
} }
// Init graph // Init graph
GitGraph.init('git-graph', repoId); console.log('Loading graph...');
const graphResp = await fetch(`/api/repos/${repoId}/graph`);
if (graphResp.ok) {
const graphData = await graphResp.json();
console.log('Graph data:', graphData);
if (showDebug) {
document.getElementById('debug-graph').textContent =
(graphData.commits ? graphData.commits.length : 0) + ' commits, ' +
(graphData.refs ? graphData.refs.length : 0) + ' refs';
}
// Render graph
GitGraph.init('git-graph', repoId);
} else {
console.error('Failed to load graph:', graphResp.status);
if (showDebug) {
document.getElementById('debug-graph').textContent = 'Error: ' + graphResp.status;
}
}
// Init diff viewer
DiffViewer.init('diff-container'); DiffViewer.init('diff-container');
// Show main content
document.getElementById('loading').classList.add('hidden');
document.getElementById('main-content').classList.remove('hidden');
} catch (err) { } catch (err) {
console.error('Init error:', 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) { function populateRefSelects(refs) {
const selects = ['graph-base', 'graph-head', 'diff-base', 'diff-head']; const selects = ['graph-base', 'graph-head', 'diff-base', 'diff-head'];
const branches = refs.filter(r => !r.is_tag);
const tags = refs.filter(r => r.is_tag);
selects.forEach(selectId => { selects.forEach(selectId => {
const select = document.getElementById(selectId); const select = document.getElementById(selectId);
select.innerHTML = '<option value="">选择分支/标签</option>'; select.innerHTML = '<option value="">选择分支/标签</option>';
// Branches first
const branches = refs.filter(r => !r.is_tag);
const tags = refs.filter(r => r.is_tag);
if (branches.length > 0) { if (branches.length > 0) {
const group = document.createElement('optgroup'); const group = document.createElement('optgroup');
group.label = '分支'; group.label = '分支';