Every route handler was written as one dense semicolon-chained line (server.mjs's main handler was over 2000 characters wide) — real width, not a VSCode wrapping artifact. Ran Prettier to break statement structure into properly indented multi-line code; left template literal string contents untouched since Prettier doesn't reformat inside those, so the large HTML-generating return strings are still single long lines by nature, just no longer compounded by unformatted surrounding JS. Verified: tests pass, node --check passes, and the app renders identically before/after (screenshotted the diff). No devDependency added — npx prettier@3.9.5 pinned in the new scripts keeps the zero-runtime-dependency pledge intact; formatting is a dev- time concern only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
72 lines
2.6 KiB
JavaScript
72 lines
2.6 KiB
JavaScript
const FIX_WORDS =
|
|
/\b(fix(?:e[ds])?|bug|broken|race|crash|regression|rollback|restore[ds]?|duplicate[ds]?|auth|fail(?:ed|ure|ing)?)\b/i;
|
|
const validRepo = (value) => /^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i.test(value);
|
|
export async function scanGiteaCandidates({
|
|
baseUrl,
|
|
repositories,
|
|
token = '',
|
|
limit = 100,
|
|
fetchImpl = fetch,
|
|
}) {
|
|
const candidates = [],
|
|
errors = [],
|
|
headers = { accept: 'application/json' };
|
|
if (token) headers.authorization = `token ${token}`;
|
|
for (const repo of repositories) {
|
|
if (!validRepo(repo)) {
|
|
errors.push({ repo, error: 'invalid owner/repository identifier' });
|
|
continue;
|
|
}
|
|
const [owner, name] = repo.split('/');
|
|
let seen = 0;
|
|
try {
|
|
for (let page = 1; seen < limit; page++) {
|
|
const count = Math.min(50, limit - seen),
|
|
url = `${baseUrl.replace(/\/$/, '')}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/commits?limit=${count}&page=${page}`;
|
|
const response = await fetchImpl(url, { headers, signal: AbortSignal.timeout(5000) });
|
|
if (!response.ok) throw new Error(`Gitea returned ${response.status}`);
|
|
const commits = await response.json();
|
|
if (!Array.isArray(commits)) throw new Error('unexpected Gitea response');
|
|
for (const commit of commits) {
|
|
seen++;
|
|
const subject = String(commit.commit?.message || '')
|
|
.split('\n')[0]
|
|
.trim(),
|
|
match = subject.match(FIX_WORDS);
|
|
if (!match) continue;
|
|
let paths = '';
|
|
try {
|
|
const detail = await fetchImpl(
|
|
`${baseUrl.replace(/\/$/, '')}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/commits/${encodeURIComponent(commit.sha)}`,
|
|
{ headers, signal: AbortSignal.timeout(5000) },
|
|
);
|
|
if (detail.ok) {
|
|
const data = await detail.json();
|
|
paths = (data.files || [])
|
|
.map((file) => file.filename)
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
}
|
|
} catch {}
|
|
candidates.push({
|
|
repo,
|
|
sha: commit.sha,
|
|
committedAt:
|
|
commit.commit?.committer?.date ||
|
|
commit.commit?.author?.date ||
|
|
commit.created ||
|
|
new Date().toISOString(),
|
|
subject,
|
|
paths,
|
|
reason: `Gitea subject keyword: ${match[0].toLowerCase()}`,
|
|
});
|
|
}
|
|
if (commits.length < count) break;
|
|
}
|
|
} catch (error) {
|
|
errors.push({ repo, error: error.message });
|
|
}
|
|
}
|
|
return { candidates, errors };
|
|
}
|