Git - get remotes from the config file (#165909)

This commit is contained in:
Ladislau Szomoru
2022-11-10 14:35:43 +01:00
committed by GitHub
parent 35bdaa8003
commit b4eaea6bc6
2 changed files with 185 additions and 48 deletions

View File

@@ -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<Submodule> = {};
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<Remote[]> {
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<Remote[]> {
const raw = await fs.readFile(path.join(this.dotGit.commonPath ?? this.dotGit.path, 'config'), 'utf8');
return parseGitRemotes(raw);
}
async getBranch(name: string): Promise<Branch> {
if (name === 'HEAD') {
return this.getHEAD();

View File

@@ -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