// Why: "which remote has this URL?" was answered with one `git remote get-url`
// subprocess per remote, awaited serially -- 58 spawns on a repo with 58 remotes,
// on every push-target resolution. `git remote -v` answers for every remote from
// one child.
//
// `remote -v` is the faithful one-command form, not `config --get-regexp '^remote\.'`:
// both `remote -v` and `remote get-url` print the URL *after* `url..insteadOf`
// expansion and pick the first of several `remote..url` values, while raw config
// reads return the unexpanded value and the last of the multiple values.
//
// `remote -v` also predates `remote get-url` (2.7), so this lowers rather than raises
// the Git floor and needs no capability gate.
import { iterateProcessOutputLines } from './process-output-field-scanner'
export type GitRemoteVerboseEntry = {
name: string
url: string
direction: 'fetch' | 'push'
}
// Greedy prefix so a URL containing spaces or parentheses keeps them.
const REMOTE_VERBOSE_URL_PATTERN = /^(.*) \((fetch|push)\)$/
/** Parse one `\t (fetch|push)` row. */
export function parseGitRemoteVerboseLine(line: string): GitRemoteVerboseEntry | null {
const tabIndex = line.indexOf('\t')
if (tabIndex === -1) {
return null
}
const name = line.slice(0, tabIndex)
const match = REMOTE_VERBOSE_URL_PATTERN.exec(line.slice(tabIndex + 1).trim())
return match ? { name, url: match[1], direction: match[2] as 'fetch' | 'push' } : null
}
/**
* Fetch URL per remote in `git remote` order -- the value `git remote get-url `
* prints. A remote configured with only a `pushurl` has no fetch row and is absent
* here; `get-url` echoed the remote's own name for it, which no caller can match.
*/
export function parseGitRemoteFetchUrls(stdout: string): Map {
const fetchUrls = new Map()
for (const line of iterateProcessOutputLines(stdout)) {
const parsed = parseGitRemoteVerboseLine(line)
// First wins: `get-url` without `--all` prints the first `remote..url`.
if (parsed?.direction === 'fetch' && !fetchUrls.has(parsed.name)) {
fetchUrls.set(parsed.name, parsed.url)
}
}
return fetchUrls
}
/** First remote whose fetch URL matches, in the order the per-remote scan visited them. */
export function findGitRemoteNameByFetchUrl(
stdout: string,
matchesUrl: (url: string) => boolean
): string | null {
for (const [name, url] of parseGitRemoteFetchUrls(stdout)) {
if (matchesUrl(url)) {
return name
}
}
return null
}