Skip to content

Commit b4eaea6

Browse files
authored
Git - get remotes from the config file (microsoft#165909)
1 parent 35bdaa8 commit b4eaea6

File tree

2 files changed

+181
-44
lines changed

2 files changed

+181
-44
lines changed

extensions/git/src/git.ts

Lines changed: 97 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,61 @@ export interface Commit {
688688
refNames: string[];
689689
}
690690

691+
interface GitConfigSection {
692+
name: string;
693+
subSectionName?: string;
694+
properties: { [key: string]: string };
695+
}
696+
697+
class GitConfigParser {
698+
private static readonly _lineSeparator = /\r?\n/;
699+
700+
private static readonly _commentRegex = /^\s*[#;].*/;
701+
private static readonly _emptyLineRegex = /^\s*$/;
702+
private static readonly _propertyRegex = /^\s*(\w+)\s*=\s*(.*)$/;
703+
private static readonly _sectionRegex = /^\s*\[\s*([^\]]+?)\s*(\"[^"]+\")*\]\s*$/;
704+
705+
static parse(raw: string, sectionName: string): GitConfigSection[] {
706+
let section: GitConfigSection | undefined;
707+
const config: { sections: GitConfigSection[] } = { sections: [] };
708+
709+
const addSection = (section?: GitConfigSection) => {
710+
if (!section) { return; }
711+
config.sections.push(section);
712+
};
713+
714+
for (const configFileLine of raw.split(GitConfigParser._lineSeparator)) {
715+
// Ignore empty lines and comments
716+
if (GitConfigParser._emptyLineRegex.test(configFileLine) ||
717+
GitConfigParser._commentRegex.test(configFileLine)) {
718+
continue;
719+
}
720+
721+
// Section
722+
const sectionMatch = configFileLine.match(GitConfigParser._sectionRegex);
723+
if (sectionMatch?.length === 3) {
724+
addSection(section);
725+
section = sectionMatch[1] === sectionName ?
726+
{ name: sectionMatch[1], subSectionName: sectionMatch[2]?.replaceAll('"', ''), properties: {} } : undefined;
727+
728+
continue;
729+
}
730+
731+
// Properties
732+
if (section) {
733+
const propertyMatch = configFileLine.match(GitConfigParser._propertyRegex);
734+
if (propertyMatch?.length === 3 && !Object.keys(section.properties).includes(propertyMatch[1])) {
735+
section.properties[propertyMatch[1]] = propertyMatch[2];
736+
}
737+
}
738+
}
739+
740+
addSection(section);
741+
742+
return config.sections;
743+
}
744+
}
745+
691746
export class GitStatusParser {
692747

693748
private lastRaw = '';
@@ -761,59 +816,39 @@ export interface Submodule {
761816
}
762817

763818
export function parseGitmodules(raw: string): Submodule[] {
764-
const regex = /\r?\n/g;
765-
let position = 0;
766-
let match: RegExpExecArray | null = null;
767-
768819
const result: Submodule[] = [];
769-
let submodule: Partial<Submodule> = {};
770-
771-
function parseLine(line: string): void {
772-
const sectionMatch = /^\s*\[submodule "([^"]+)"\]\s*$/.exec(line);
773-
774-
if (sectionMatch) {
775-
if (submodule.name && submodule.path && submodule.url) {
776-
result.push(submodule as Submodule);
777-
}
778-
779-
const name = sectionMatch[1];
780-
781-
if (name) {
782-
submodule = { name };
783-
return;
784-
}
785-
}
786820

787-
if (!submodule) {
788-
return;
821+
for (const submoduleSection of GitConfigParser.parse(raw, 'submodule')) {
822+
if (submoduleSection.subSectionName && submoduleSection.properties['path'] && submoduleSection.properties['url']) {
823+
result.push({
824+
name: submoduleSection.subSectionName,
825+
path: submoduleSection.properties['path'],
826+
url: submoduleSection.properties['url']
827+
});
789828
}
829+
}
790830

791-
const propertyMatch = /^\s*(\w+)\s*=\s*(.*)$/.exec(line);
792-
793-
if (!propertyMatch) {
794-
return;
795-
}
831+
return result;
832+
}
796833

797-
const [, key, value] = propertyMatch;
834+
export function parseGitRemotes(raw: string): Remote[] {
835+
const remotes: Remote[] = [];
798836

799-
switch (key) {
800-
case 'path': submodule.path = value; break;
801-
case 'url': submodule.url = value; break;
837+
for (const remoteSection of GitConfigParser.parse(raw, 'remote')) {
838+
if (!remoteSection.subSectionName) {
839+
continue;
802840
}
803-
}
804-
805-
while (match = regex.exec(raw)) {
806-
parseLine(raw.substring(position, match.index));
807-
position = match.index + match[0].length;
808-
}
809841

810-
parseLine(raw.substring(position));
811-
812-
if (submodule.name && submodule.path && submodule.url) {
813-
result.push(submodule as Submodule);
842+
remotes.push({
843+
name: remoteSection.subSectionName,
844+
fetchUrl: remoteSection.properties['url'],
845+
pushUrl: remoteSection.properties['pushurl'] ?? remoteSection.properties['url'],
846+
// https://github.com/microsoft/vscode/issues/45271
847+
isReadOnly: remoteSection.properties['pushurl'] === 'no_push'
848+
});
814849
}
815850

816-
return result;
851+
return remotes;
817852
}
818853

819854
const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm;
@@ -2148,6 +2183,20 @@ export class Repository {
21482183
}
21492184

21502185
async getRemotes(): Promise<Remote[]> {
2186+
try {
2187+
// Attempt to parse the config file
2188+
const remotes = await this.getRemotesFS();
2189+
if (remotes.length === 0) {
2190+
throw new Error('No remotes found in the git config file.');
2191+
}
2192+
2193+
return remotes;
2194+
}
2195+
catch (err) {
2196+
this.logger.warn(err.message);
2197+
}
2198+
2199+
// Fallback to using git to determine remotes
21512200
const result = await this.exec(['remote', '--verbose']);
21522201
const lines = result.stdout.trim().split('\n').filter(l => !!l);
21532202
const remotes: MutableRemote[] = [];
@@ -2179,6 +2228,11 @@ export class Repository {
21792228
return remotes;
21802229
}
21812230

2231+
private async getRemotesFS(): Promise<Remote[]> {
2232+
const raw = await fs.readFile(path.join(this.dotGit.commonPath ?? this.dotGit.path, 'config'), 'utf8');
2233+
return parseGitRemotes(raw);
2234+
}
2235+
21822236
async getBranch(name: string): Promise<Branch> {
21832237
if (name === 'HEAD') {
21842238
return this.getHEAD();

extensions/git/src/test/git.test.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import 'mocha';
7-
import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
7+
import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles, parseGitRemotes } from '../git';
88
import * as assert from 'assert';
99
import { splitInChunks } from '../util';
1010

@@ -197,6 +197,89 @@ suite('git', () => {
197197
});
198198
});
199199

200+
suite('parseGitRemotes', () => {
201+
test('empty', () => {
202+
assert.deepStrictEqual(parseGitRemotes(''), []);
203+
});
204+
205+
test('single remote', () => {
206+
const sample = `[remote "origin"]
207+
url = https://github.com/microsoft/vscode.git
208+
fetch = +refs/heads/*:refs/remotes/origin/*
209+
`;
210+
211+
assert.deepStrictEqual(parseGitRemotes(sample), [
212+
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode.git', isReadOnly: false }
213+
]);
214+
});
215+
216+
test('single remote (read-only)', () => {
217+
const sample = `[remote "origin"]
218+
url = https://github.com/microsoft/vscode.git
219+
fetch = +refs/heads/*:refs/remotes/origin/*
220+
pushurl = no_push
221+
`;
222+
223+
assert.deepStrictEqual(parseGitRemotes(sample), [
224+
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'no_push', isReadOnly: true }
225+
]);
226+
});
227+
228+
test('single remote (multiple urls)', () => {
229+
const sample = `[remote "origin"]
230+
url = https://github.com/microsoft/vscode.git
231+
url = https://github.com/microsoft/vscode2.git
232+
fetch = +refs/heads/*:refs/remotes/origin/*
233+
`;
234+
235+
assert.deepStrictEqual(parseGitRemotes(sample), [
236+
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode.git', isReadOnly: false }
237+
]);
238+
});
239+
240+
test('multiple remotes', () => {
241+
const sample = `[remote "origin"]
242+
url = https://github.com/microsoft/vscode.git
243+
pushurl = https://github.com/microsoft/vscode1.git
244+
fetch = +refs/heads/*:refs/remotes/origin/*
245+
[remote "remote2"]
246+
url = https://github.com/microsoft/vscode2.git
247+
fetch = +refs/heads/*:refs/remotes/origin/*
248+
`;
249+
250+
assert.deepStrictEqual(parseGitRemotes(sample), [
251+
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode1.git', isReadOnly: false },
252+
{ name: 'remote2', fetchUrl: 'https://github.com/microsoft/vscode2.git', pushUrl: 'https://github.com/microsoft/vscode2.git', isReadOnly: false }
253+
]);
254+
});
255+
256+
test('remotes (white space)', () => {
257+
const sample = ` [remote "origin"]
258+
url = https://github.com/microsoft/vscode.git
259+
pushurl=https://github.com/microsoft/vscode1.git
260+
fetch = +refs/heads/*:refs/remotes/origin/*
261+
[ remote"remote2"]
262+
url = https://github.com/microsoft/vscode2.git
263+
fetch = +refs/heads/*:refs/remotes/origin/*
264+
`;
265+
266+
assert.deepStrictEqual(parseGitRemotes(sample), [
267+
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode1.git', isReadOnly: false },
268+
{ name: 'remote2', fetchUrl: 'https://github.com/microsoft/vscode2.git', pushUrl: 'https://github.com/microsoft/vscode2.git', isReadOnly: false }
269+
]);
270+
});
271+
272+
test('remotes (invalid section)', () => {
273+
const sample = `[remote "origin"
274+
url = https://github.com/microsoft/vscode.git
275+
pushurl = https://github.com/microsoft/vscode1.git
276+
fetch = +refs/heads/*:refs/remotes/origin/*
277+
`;
278+
279+
assert.deepStrictEqual(parseGitRemotes(sample), []);
280+
});
281+
});
282+
200283
suite('parseGitCommit', () => {
201284
test('single parent commit', function () {
202285
const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1

0 commit comments

Comments
 (0)