// // 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 { repos: [], unmatched: [] }; const dirs = await git.discoverRepositories(root); const found = new Map(); const unmatched = 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; }); let matched = false; const skipped = []; for (const remote of ordered) { const parsed = remotes.parse(remote.url); if (!parsed) continue; // A configured or already-learned host resolves immediately; // otherwise ask the instances whether one of them serves it. const baseUrl = router.baseUrlForHost(parsed.host) || (await router.detectHostAlias(parsed.host, parsed.owner, parsed.name)); if (!baseUrl) { log.debug(`no configured instance for host ${parsed.host} (${remote.url})`); skipped.push({ host: parsed.host, remoteUrl: remote.url, dir: dir }); 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); } matched = true; break; } // Only report hosts for a checkout that resolved to nothing at all; a // repository with a matching origin plus an unrelated fork is fine. if (!matched) { for (const entry of skipped) { if (!unmatched.has(entry.host)) unmatched.set(entry.host, entry); } } } return { repos: [...found.values()], unmatched: [...unmatched.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, returning them alongside any * remote hosts that matched no configured instance. 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 { repos: workspaceRepos, unmatched } = await discoverFromWorkspace(router); if (mode !== "allAccessible") return { repos: workspaceRepos, unmatched: unmatched }; 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 { repos: [...byKey.values()].sort((a, b) => a.fullName.localeCompare(b.fullName)), unmatched: unmatched, }; } exports.discover = discover; exports.makeRepo = makeRepo; exports.keyFor = keyFor;