diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 86ff30ec8f9..8a897fcd2d5 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -688,6 +688,61 @@ export interface Commit { refNames: string[]; } +interface GitConfigSection { + name: string; + subSectionName?: string; + properties: { [key: string]: string }; +} + +class GitConfigParser { + private static readonly _lineSeparator = /\r?\n/; + + private static readonly _commentRegex = /^\s*[#;].*/; + private static readonly _emptyLineRegex = /^\s*$/; + private static readonly _propertyRegex = /^\s*(\w+)\s*=\s*(.*)$/; + private static readonly _sectionRegex = /^\s*\[\s*([^\]]+?)\s*(\"[^"]+\")*\]\s*$/; + + static parse(raw: string, sectionName: string): GitConfigSection[] { + let section: GitConfigSection | undefined; + const config: { sections: GitConfigSection[] } = { sections: [] }; + + const addSection = (section?: GitConfigSection) => { + if (!section) { return; } + config.sections.push(section); + }; + + for (const configFileLine of raw.split(GitConfigParser._lineSeparator)) { + // Ignore empty lines and comments + if (GitConfigParser._emptyLineRegex.test(configFileLine) || + GitConfigParser._commentRegex.test(configFileLine)) { + continue; + } + + // Section + const sectionMatch = configFileLine.match(GitConfigParser._sectionRegex); + if (sectionMatch?.length === 3) { + addSection(section); + section = sectionMatch[1] === sectionName ? + { name: sectionMatch[1], subSectionName: sectionMatch[2]?.replaceAll('"', ''), properties: {} } : undefined; + + continue; + } + + // Properties + if (section) { + const propertyMatch = configFileLine.match(GitConfigParser._propertyRegex); + if (propertyMatch?.length === 3 && !Object.keys(section.properties).includes(propertyMatch[1])) { + section.properties[propertyMatch[1]] = propertyMatch[2]; + } + } + } + + addSection(section); + + return config.sections; + } +} + export class GitStatusParser { private lastRaw = ''; @@ -761,61 +816,41 @@ export interface Submodule { } export function parseGitmodules(raw: string): Submodule[] { - const regex = /\r?\n/g; - let position = 0; - let match: RegExpExecArray | null = null; - const result: Submodule[] = []; - let submodule: Partial = {}; - function parseLine(line: string): void { - const sectionMatch = /^\s*\[submodule "([^"]+)"\]\s*$/.exec(line); - - if (sectionMatch) { - if (submodule.name && submodule.path && submodule.url) { - result.push(submodule as Submodule); - } - - const name = sectionMatch[1]; - - if (name) { - submodule = { name }; - return; - } + for (const submoduleSection of GitConfigParser.parse(raw, 'submodule')) { + if (submoduleSection.subSectionName && submoduleSection.properties['path'] && submoduleSection.properties['url']) { + result.push({ + name: submoduleSection.subSectionName, + path: submoduleSection.properties['path'], + url: submoduleSection.properties['url'] + }); } - - if (!submodule) { - return; - } - - const propertyMatch = /^\s*(\w+)\s*=\s*(.*)$/.exec(line); - - if (!propertyMatch) { - return; - } - - const [, key, value] = propertyMatch; - - switch (key) { - case 'path': submodule.path = value; break; - case 'url': submodule.url = value; break; - } - } - - while (match = regex.exec(raw)) { - parseLine(raw.substring(position, match.index)); - position = match.index + match[0].length; - } - - parseLine(raw.substring(position)); - - if (submodule.name && submodule.path && submodule.url) { - result.push(submodule as Submodule); } return result; } +export function parseGitRemotes(raw: string): Remote[] { + const remotes: Remote[] = []; + + for (const remoteSection of GitConfigParser.parse(raw, 'remote')) { + if (!remoteSection.subSectionName) { + continue; + } + + remotes.push({ + name: remoteSection.subSectionName, + fetchUrl: remoteSection.properties['url'], + pushUrl: remoteSection.properties['pushurl'] ?? remoteSection.properties['url'], + // https://github.com/microsoft/vscode/issues/45271 + isReadOnly: remoteSection.properties['pushurl'] === 'no_push' + }); + } + + return remotes; +} + const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm; export function parseGitCommits(data: string): Commit[] { @@ -2148,6 +2183,20 @@ export class Repository { } async getRemotes(): Promise { + try { + // Attempt to parse the config file + const remotes = await this.getRemotesFS(); + if (remotes.length === 0) { + throw new Error('No remotes found in the git config file.'); + } + + return remotes; + } + catch (err) { + this.logger.warn(err.message); + } + + // Fallback to using git to determine remotes const result = await this.exec(['remote', '--verbose']); const lines = result.stdout.trim().split('\n').filter(l => !!l); const remotes: MutableRemote[] = []; @@ -2179,6 +2228,11 @@ export class Repository { return remotes; } + private async getRemotesFS(): Promise { + const raw = await fs.readFile(path.join(this.dotGit.commonPath ?? this.dotGit.path, 'config'), 'utf8'); + return parseGitRemotes(raw); + } + async getBranch(name: string): Promise { if (name === 'HEAD') { return this.getHEAD(); diff --git a/extensions/git/src/test/git.test.ts b/extensions/git/src/test/git.test.ts index 3b225157c3b..27ad7d93d5f 100644 --- a/extensions/git/src/test/git.test.ts +++ b/extensions/git/src/test/git.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'mocha'; -import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles } from '../git'; +import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles, parseGitRemotes } from '../git'; import * as assert from 'assert'; import { splitInChunks } from '../util'; @@ -197,6 +197,89 @@ suite('git', () => { }); }); + suite('parseGitRemotes', () => { + test('empty', () => { + assert.deepStrictEqual(parseGitRemotes(''), []); + }); + + test('single remote', () => { + const sample = `[remote "origin"] + url = https://github.com/microsoft/vscode.git + fetch = +refs/heads/*:refs/remotes/origin/* +`; + + assert.deepStrictEqual(parseGitRemotes(sample), [ + { name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode.git', isReadOnly: false } + ]); + }); + + test('single remote (read-only)', () => { + const sample = `[remote "origin"] + url = https://github.com/microsoft/vscode.git + fetch = +refs/heads/*:refs/remotes/origin/* + pushurl = no_push +`; + + assert.deepStrictEqual(parseGitRemotes(sample), [ + { name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'no_push', isReadOnly: true } + ]); + }); + + test('single remote (multiple urls)', () => { + const sample = `[remote "origin"] + url = https://github.com/microsoft/vscode.git + url = https://github.com/microsoft/vscode2.git + fetch = +refs/heads/*:refs/remotes/origin/* +`; + + assert.deepStrictEqual(parseGitRemotes(sample), [ + { name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode.git', isReadOnly: false } + ]); + }); + + test('multiple remotes', () => { + const sample = `[remote "origin"] + url = https://github.com/microsoft/vscode.git + pushurl = https://github.com/microsoft/vscode1.git + fetch = +refs/heads/*:refs/remotes/origin/* +[remote "remote2"] + url = https://github.com/microsoft/vscode2.git + fetch = +refs/heads/*:refs/remotes/origin/* +`; + + assert.deepStrictEqual(parseGitRemotes(sample), [ + { name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode1.git', isReadOnly: false }, + { name: 'remote2', fetchUrl: 'https://github.com/microsoft/vscode2.git', pushUrl: 'https://github.com/microsoft/vscode2.git', isReadOnly: false } + ]); + }); + + test('remotes (white space)', () => { + const sample = ` [remote "origin"] + url = https://github.com/microsoft/vscode.git + pushurl=https://github.com/microsoft/vscode1.git + fetch = +refs/heads/*:refs/remotes/origin/* +[ remote"remote2"] + url = https://github.com/microsoft/vscode2.git + fetch = +refs/heads/*:refs/remotes/origin/* +`; + + assert.deepStrictEqual(parseGitRemotes(sample), [ + { name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode1.git', isReadOnly: false }, + { name: 'remote2', fetchUrl: 'https://github.com/microsoft/vscode2.git', pushUrl: 'https://github.com/microsoft/vscode2.git', isReadOnly: false } + ]); + }); + + test('remotes (invalid section)', () => { + const sample = `[remote "origin" + url = https://github.com/microsoft/vscode.git + pushurl = https://github.com/microsoft/vscode1.git + fetch = +refs/heads/*:refs/remotes/origin/* +`; + + assert.deepStrictEqual(parseGitRemotes(sample), []); + }); + }); + suite('parseGitCommit', () => { test('single parent commit', function () { const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1