Show SVN repository auth hints

This commit is contained in:
lwt
2026-06-18 14:41:15 +08:00
parent 4ab3332f34
commit 11b2504ce5
2 changed files with 68 additions and 5 deletions
+10 -5
View File
@@ -152,7 +152,7 @@ function renderRepos() {
els.repoList.innerHTML = visibleRepos.map((repo) => `<button class="svn-repo-item${repo.id === selectedRepoId ? " is-active" : ""}" type="button" data-id="${escapeHtml(repo.id)}"> els.repoList.innerHTML = visibleRepos.map((repo) => `<button class="svn-repo-item${repo.id === selectedRepoId ? " is-active" : ""}" type="button" data-id="${escapeHtml(repo.id)}">
<span class="repo-name">${escapeHtml(repo.name)}</span> <span class="repo-name">${escapeHtml(repo.name)}</span>
<span class="repo-url">${escapeHtml(repo.url)}</span> <span class="repo-url">${escapeHtml(repo.url)}</span>
<span class="repo-meta">${escapeHtml(repo.source === "server" ? "服务器仓库" : "手动配置")} · ${escapeHtml(repo.localPath || repo.checkoutPath || repo.id)} · ${repo.hasPassword ? "已配置凭据" : "无密码"}</span> <span class="repo-meta">${escapeHtml(repo.source === "server" ? "服务器仓库" : "手动配置")} · ${escapeHtml(repo.localPath || repo.checkoutPath || repo.id)} · ${repo.hasPassword ? "已配置凭据" : (repo.serverAuthRequired ? "需要凭据" : "无密码")}</span>
</button>`).join("") || `<p class="empty-state">${repositories.length ? "没有匹配的仓库。" : "暂无 SVN 仓库,可以先点击“接入旧仓库”。"}</p>`; </button>`).join("") || `<p class="empty-state">${repositories.length ? "没有匹配的仓库。" : "暂无 SVN 仓库,可以先点击“接入旧仓库”。"}</p>`;
const repo = selectedRepo(); const repo = selectedRepo();
@@ -163,7 +163,7 @@ function renderRepos() {
els.selectedRepoUrl.textContent = compactPath(repo?.url); els.selectedRepoUrl.textContent = compactPath(repo?.url);
els.selectedRepoPath.textContent = compactPath(repo?.localPath || repo?.checkoutPath); els.selectedRepoPath.textContent = compactPath(repo?.localPath || repo?.checkoutPath);
els.selectedRepoRevision.textContent = repo?.serverRevision ? `r${repo.serverRevision}` : (repo?.lastAction?.at ? formatDate(repo.lastAction.at) : "--"); els.selectedRepoRevision.textContent = repo?.serverRevision ? `r${repo.serverRevision}` : (repo?.lastAction?.at ? formatDate(repo.lastAction.at) : "--");
els.selectedRepoSource.textContent = repo ? (repo.source === "server" ? "服务器旧仓库" : "手动配置") : "--"; els.selectedRepoSource.textContent = repo ? (repo.source === "server" ? `服务器旧仓库${repo.serverAuthRequired ? " · 需要 SVN 凭据" : ""}` : "手动配置") : "--";
els.deleteRepo.disabled = !repo; els.deleteRepo.disabled = !repo;
} }
@@ -172,13 +172,18 @@ function renderServerRepos() {
els.serverSummary.textContent = serverRepositories.length els.serverSummary.textContent = serverRepositories.length
? `发现 ${serverRepositories.length} 个,已接入 ${configuredCount}` ? `发现 ${serverRepositories.length} 个,已接入 ${configuredCount}`
: "未发现旧仓库"; : "未发现旧仓库";
els.serverRepoList.innerHTML = serverRepositories.map((repo) => `<button class="svn-server-repo${repo.configured ? " is-linked" : ""}" type="button" data-id="${escapeHtml(repo.id)}" data-configured-id="${escapeHtml(repo.configuredId || "")}"> els.serverRepoList.innerHTML = serverRepositories.map((repo) => {
const authText = repo.authRequired
? `需要凭据${repo.authUsers?.length ? `${repo.authUsers.join(", ")}` : ""}`
: "允许匿名";
return `<button class="svn-server-repo${repo.configured ? " is-linked" : ""}" type="button" data-id="${escapeHtml(repo.id)}" data-configured-id="${escapeHtml(repo.configuredId || "")}">
<span> <span>
<strong>${escapeHtml(repo.name)}</strong> <strong>${escapeHtml(repo.name)}</strong>
<small>${escapeHtml(repo.url)}</small> <small>${escapeHtml(repo.url)}</small>
</span> </span>
<em>${repo.configured ? "已接入" : "待接入"}${repo.revision ? ` · r${escapeHtml(repo.revision)}` : ""}</em> <em>${repo.configured ? "已接入" : "待接入"}${repo.revision ? ` · r${escapeHtml(repo.revision)}` : ""}<br>${escapeHtml(authText)}</em>
</button>`).join("") || `<p class="empty-state">没有扫描到服务器 SVN 仓库。</p>`; </button>`;
}).join("") || `<p class="empty-state">没有扫描到服务器 SVN 仓库。</p>`;
} }
async function loadRepos() { async function loadRepos() {
+58
View File
@@ -54,6 +54,11 @@ function limitText(value, maxLength = 4000) {
return String(value || "").trim().slice(0, maxLength); return String(value || "").trim().slice(0, maxLength);
} }
function limitStringArray(value, maxItems = 30, maxLength = 120) {
if (!Array.isArray(value)) return [];
return value.map((item) => limitText(item, maxLength)).filter(Boolean).slice(0, maxItems);
}
function isRemoteTarget(value) { function isRemoteTarget(value) {
return REMOTE_TARGET_PATTERN.test(String(value || "").trim()); return REMOTE_TARGET_PATTERN.test(String(value || "").trim());
} }
@@ -138,6 +143,41 @@ async function readTextIfExists(filePath) {
} }
} }
function parseSimpleConfig(text) {
const result = {};
for (const line of String(text || "").split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";") || trimmed.startsWith("[")) continue;
const index = trimmed.indexOf("=");
if (index === -1) continue;
result[trimmed.slice(0, index).trim()] = trimmed.slice(index + 1).trim();
}
return result;
}
function parsePasswdUsers(text) {
const users = [];
let inUsers = false;
for (const line of String(text || "").split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) continue;
if (/^\[users\]$/i.test(trimmed)) {
inUsers = true;
continue;
}
if (/^\[.+\]$/.test(trimmed)) {
inUsers = false;
continue;
}
if (!inUsers) continue;
const index = trimmed.indexOf("=");
if (index === -1) continue;
const username = trimmed.slice(0, index).trim();
if (username) users.push(username);
}
return users;
}
function addAuthArgs(args, repo) { function addAuthArgs(args, repo) {
const next = [...args, "--non-interactive"]; const next = [...args, "--non-interactive"];
if (repo.trustServerCert !== false) { if (repo.trustServerCert !== false) {
@@ -250,6 +290,9 @@ async function discoveredRepoInfo(options, req, rootPath, repoPath) {
const name = relativePath || path.basename(repoPath); const name = relativePath || path.basename(repoPath);
const current = await readTextIfExists(path.join(repoPath, "db", "current")); const current = await readTextIfExists(path.join(repoPath, "db", "current"));
const revision = current.split(/\s+/)[0] || ""; const revision = current.split(/\s+/)[0] || "";
const svnserveConfig = parseSimpleConfig(await readTextIfExists(path.join(repoPath, "conf", "svnserve.conf")));
const authUsers = parsePasswdUsers(await readTextIfExists(path.join(repoPath, "conf", svnserveConfig["password-db"] || "passwd")));
const anonAccess = svnserveConfig["anon-access"] || "";
return { return {
id: slugify(name), id: slugify(name),
name, name,
@@ -261,6 +304,11 @@ async function discoveredRepoInfo(options, req, rootPath, repoPath) {
uuid: await readTextIfExists(path.join(repoPath, "db", "uuid")), uuid: await readTextIfExists(path.join(repoPath, "db", "uuid")),
fsType: await readTextIfExists(path.join(repoPath, "db", "fs-type")), fsType: await readTextIfExists(path.join(repoPath, "db", "fs-type")),
format: await readTextIfExists(path.join(repoPath, "format")), format: await readTextIfExists(path.join(repoPath, "format")),
anonAccess,
authAccess: svnserveConfig["auth-access"] || "",
realm: svnserveConfig.realm || "",
authRequired: anonAccess.toLowerCase() === "none",
authUsers: authUsers.slice(0, 30),
}; };
} }
@@ -323,6 +371,11 @@ function repoFromDiscovered(discovered, existing = {}) {
serverRelativePath: discovered.relativePath, serverRelativePath: discovered.relativePath,
serverRevision: discovered.revision, serverRevision: discovered.revision,
serverUuid: discovered.uuid, serverUuid: discovered.uuid,
serverAnonAccess: discovered.anonAccess,
serverAuthAccess: discovered.authAccess,
serverRealm: discovered.realm,
serverAuthRequired: discovered.authRequired,
serverAuthUsers: discovered.authUsers,
description: existing.description || `从服务器 SVN 服务接入:${discovered.repositoryPath}`, description: existing.description || `从服务器 SVN 服务接入:${discovered.repositoryPath}`,
}, existing); }, existing);
} }
@@ -398,6 +451,11 @@ function normalizeRepoPatch(body, existing = {}) {
serverRelativePath: limitText(body.serverRelativePath || existing.serverRelativePath, 1000), serverRelativePath: limitText(body.serverRelativePath || existing.serverRelativePath, 1000),
serverRevision: limitText(body.serverRevision || existing.serverRevision, 80), serverRevision: limitText(body.serverRevision || existing.serverRevision, 80),
serverUuid: limitText(body.serverUuid || existing.serverUuid, 120), serverUuid: limitText(body.serverUuid || existing.serverUuid, 120),
serverAnonAccess: limitText(body.serverAnonAccess || existing.serverAnonAccess, 80),
serverAuthAccess: limitText(body.serverAuthAccess || existing.serverAuthAccess, 80),
serverRealm: limitText(body.serverRealm || existing.serverRealm, 200),
serverAuthRequired: parseBoolean(body.serverAuthRequired, existing.serverAuthRequired === true),
serverAuthUsers: limitStringArray(body.serverAuthUsers || existing.serverAuthUsers),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
createdAt: existing.createdAt || new Date().toISOString(), createdAt: existing.createdAt || new Date().toISOString(),
}; };