// // Repository discovery. In workspace mode the open folder's git remotes decide // which repositories appear; in allAccessible mode every repository the token // can reach is listed. // const config = require("../util/config.js"); const credentials = require("./credentials.js"); const git = require("../util/git.js"); const log = require("../util/log.js"); const remotes = require("./remotes.js"); /** Stable identity for a repository across refreshes. */ function keyFor(baseUrl, owner, name) { return `${baseUrl}#${owner}/${name}`; } function makeRepo({ baseUrl, owner, name, dir = null, remoteName = null, remoteUrl = null }) { return { key: keyFor(baseUrl, owner, name), baseUrl: baseUrl, owner: owner, name: name, fullName: `${owner}/${name}`, dir: dir, remoteName: remoteName, remoteUrl: remoteUrl, htmlUrl: `${baseUrl}/${owner}/${name}`, branch: null, headSha: null, }; } async function discoverFromWorkspace(router) { const root = nova.workspace.path; if (!root) return []; const dirs = await git.discoverRepositories(root); const found = new Map(); for (const dir of dirs) { const list = await git.remotes(dir); if (!list.length) continue; // Prefer origin, but consider every remote so forks still resolve. const ordered = list.slice().sort((a, b) => { if (a.name === b.name) return 0; if (a.name === "origin") return -1; if (b.name === "origin") return 1; return 0; }); for (const remote of ordered) { const parsed = remotes.parse(remote.url); if (!parsed) continue; const baseUrl = router.baseUrlForHost(parsed.host); if (!baseUrl) { log.debug(`no configured instance for host ${parsed.host} (${remote.url})`); continue; } const repo = makeRepo({ baseUrl: baseUrl, owner: parsed.owner, name: parsed.name, dir: dir, remoteName: remote.name, remoteUrl: remote.url, }); if (!found.has(repo.key)) { repo.branch = await git.currentBranch(dir); repo.headSha = await git.headSha(dir); found.set(repo.key, repo); } break; } } return [...found.values()]; } async function discoverFromApi(router) { const found = []; for (const baseUrl of router.baseUrls) { if (!credentials.hasToken(baseUrl)) continue; const api = router.apiFor(baseUrl); try { const list = await api.accessibleRepositories(100); for (const entry of list) { const owner = (entry.owner && entry.owner.login) || entry.full_name.split("/")[0]; const name = entry.name; found.push( Object.assign(makeRepo({ baseUrl: baseUrl, owner: owner, name: name }), { htmlUrl: entry.html_url || `${baseUrl}/${owner}/${name}`, branch: entry.default_branch || null, }), ); } } catch (error) { log.error(`repository discovery failed for ${baseUrl}:`, String(error)); } } return found; } /** * Discovers repositories for the configured mode. Workspace mode additionally * merges in local checkouts so allAccessible never loses the local directory * needed for checkout and log-saving. */ async function discover(router) { const mode = config.discoveryMode(); const workspaceRepos = await discoverFromWorkspace(router); if (mode !== "allAccessible") return workspaceRepos; const byKey = new Map(); for (const repo of await discoverFromApi(router)) byKey.set(repo.key, repo); for (const repo of workspaceRepos) { const existing = byKey.get(repo.key); if (existing) { existing.dir = repo.dir; existing.remoteName = repo.remoteName; existing.remoteUrl = repo.remoteUrl; existing.branch = repo.branch; existing.headSha = repo.headSha; } else { byKey.set(repo.key, repo); } } return [...byKey.values()].sort((a, b) => a.fullName.localeCompare(b.fullName)); } exports.discover = discover; exports.makeRepo = makeRepo; exports.keyFor = keyFor;