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 }; }