1/*---------------------------------------------------------------------------------------------
2 *  Copyright (c) Microsoft Corporation. All rights reserved.
3 *  Licensed under the MIT License. See License.txt in the project root for license information.
4 *--------------------------------------------------------------------------------------------*/
5
6import * as fs from 'fs';
7import * as path from 'path';
8import * as os from 'os';
9import * as cp from 'child_process';
10import * as which from 'which';
11import { EventEmitter } from 'events';
12import iconv = require('iconv-lite');
13import * as filetype from 'file-type';
14import { assign, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util';
15import { CancellationToken } from 'vscode';
16import { URI } from 'vscode-uri';
17import { detectEncoding } from './encoding';
18import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git';
19
20// https://github.com/microsoft/vscode/issues/65693
21const MAX_CLI_LENGTH = 30000;
22
23const readfile = denodeify<string, string | null, string>(fs.readFile);
24
25export interface IGit {
26	path: string;
27	version: string;
28}
29
30export interface IFileStatus {
31	x: string;
32	y: string;
33	path: string;
34	rename?: string;
35}
36
37export interface Stash {
38	index: number;
39	description: string;
40}
41
42interface MutableRemote extends Remote {
43	fetchUrl?: string;
44	pushUrl?: string;
45	isReadOnly: boolean;
46}
47
48function parseVersion(raw: string): string {
49	return raw.replace(/^git version /, '');
50}
51
52function findSpecificGit(path: string, onLookup: (path: string) => void): Promise<IGit> {
53	return new Promise<IGit>((c, e) => {
54		onLookup(path);
55
56		const buffers: Buffer[] = [];
57		const child = cp.spawn(path, ['--version']);
58		child.stdout.on('data', (b: Buffer) => buffers.push(b));
59		child.on('error', cpErrorHandler(e));
60		child.on('exit', code => code ? e(new Error('Not found')) : c({ path, version: parseVersion(Buffer.concat(buffers).toString('utf8').trim()) }));
61	});
62}
63
64function findGitDarwin(onLookup: (path: string) => void): Promise<IGit> {
65	return new Promise<IGit>((c, e) => {
66		cp.exec('which git', (err, gitPathBuffer) => {
67			if (err) {
68				return e('git not found');
69			}
70
71			const path = gitPathBuffer.toString().replace(/^\s+|\s+$/g, '');
72
73			function getVersion(path: string) {
74				onLookup(path);
75
76				// make sure git executes
77				cp.exec('git --version', (err, stdout) => {
78
79					if (err) {
80						return e('git not found');
81					}
82
83					return c({ path, version: parseVersion(stdout.trim()) });
84				});
85			}
86
87			if (path !== '/usr/bin/git') {
88				return getVersion(path);
89			}
90
91			// must check if XCode is installed
92			cp.exec('xcode-select -p', (err: any) => {
93				if (err && err.code === 2) {
94					// git is not installed, and launching /usr/bin/git
95					// will prompt the user to install it
96
97					return e('git not found');
98				}
99
100				getVersion(path);
101			});
102		});
103	});
104}
105
106function findSystemGitWin32(base: string, onLookup: (path: string) => void): Promise<IGit> {
107	if (!base) {
108		return Promise.reject<IGit>('Not found');
109	}
110
111	return findSpecificGit(path.join(base, 'Git', 'cmd', 'git.exe'), onLookup);
112}
113
114function findGitWin32InPath(onLookup: (path: string) => void): Promise<IGit> {
115	const whichPromise = new Promise<string>((c, e) => which('git.exe', (err, path) => err ? e(err) : c(path)));
116	return whichPromise.then(path => findSpecificGit(path, onLookup));
117}
118
119function findGitWin32(onLookup: (path: string) => void): Promise<IGit> {
120	return findSystemGitWin32(process.env['ProgramW6432'] as string, onLookup)
121		.then(undefined, () => findSystemGitWin32(process.env['ProgramFiles(x86)'] as string, onLookup))
122		.then(undefined, () => findSystemGitWin32(process.env['ProgramFiles'] as string, onLookup))
123		.then(undefined, () => findSystemGitWin32(path.join(process.env['LocalAppData'] as string, 'Programs'), onLookup))
124		.then(undefined, () => findGitWin32InPath(onLookup));
125}
126
127export function findGit(hint: string | undefined, onLookup: (path: string) => void): Promise<IGit> {
128	const first = hint ? findSpecificGit(hint, onLookup) : Promise.reject<IGit>(null);
129
130	return first
131		.then(undefined, () => {
132			switch (process.platform) {
133				case 'darwin': return findGitDarwin(onLookup);
134				case 'win32': return findGitWin32(onLookup);
135				default: return findSpecificGit('git', onLookup);
136			}
137		})
138		.then(null, () => Promise.reject(new Error('Git installation not found.')));
139}
140
141export interface IExecutionResult<T extends string | Buffer> {
142	exitCode: number;
143	stdout: T;
144	stderr: string;
145}
146
147function cpErrorHandler(cb: (reason?: any) => void): (reason?: any) => void {
148	return err => {
149		if (/ENOENT/.test(err.message)) {
150			err = new GitError({
151				error: err,
152				message: 'Failed to execute git (ENOENT)',
153				gitErrorCode: GitErrorCodes.NotAGitRepository
154			});
155		}
156
157		cb(err);
158	};
159}
160
161export interface SpawnOptions extends cp.SpawnOptions {
162	input?: string;
163	encoding?: string;
164	log?: boolean;
165	cancellationToken?: CancellationToken;
166}
167
168async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToken): Promise<IExecutionResult<Buffer>> {
169	if (!child.stdout || !child.stderr) {
170		throw new GitError({ message: 'Failed to get stdout or stderr from git process.' });
171	}
172
173	if (cancellationToken && cancellationToken.isCancellationRequested) {
174		throw new GitError({ message: 'Cancelled' });
175	}
176
177	const disposables: IDisposable[] = [];
178
179	const once = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void) => {
180		ee.once(name, fn);
181		disposables.push(toDisposable(() => ee.removeListener(name, fn)));
182	};
183
184	const on = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void) => {
185		ee.on(name, fn);
186		disposables.push(toDisposable(() => ee.removeListener(name, fn)));
187	};
188
189	let result = Promise.all<any>([
190		new Promise<number>((c, e) => {
191			once(child, 'error', cpErrorHandler(e));
192			once(child, 'exit', c);
193		}),
194		new Promise<Buffer>(c => {
195			const buffers: Buffer[] = [];
196			on(child.stdout, 'data', (b: Buffer) => buffers.push(b));
197			once(child.stdout, 'close', () => c(Buffer.concat(buffers)));
198		}),
199		new Promise<string>(c => {
200			const buffers: Buffer[] = [];
201			on(child.stderr, 'data', (b: Buffer) => buffers.push(b));
202			once(child.stderr, 'close', () => c(Buffer.concat(buffers).toString('utf8')));
203		})
204	]) as Promise<[number, Buffer, string]>;
205
206	if (cancellationToken) {
207		const cancellationPromise = new Promise<[number, Buffer, string]>((_, e) => {
208			onceEvent(cancellationToken.onCancellationRequested)(() => {
209				try {
210					child.kill();
211				} catch (err) {
212					// noop
213				}
214
215				e(new GitError({ message: 'Cancelled' }));
216			});
217		});
218
219		result = Promise.race([result, cancellationPromise]);
220	}
221
222	try {
223		const [exitCode, stdout, stderr] = await result;
224		return { exitCode, stdout, stderr };
225	} finally {
226		dispose(disposables);
227	}
228}
229
230export interface IGitErrorData {
231	error?: Error;
232	message?: string;
233	stdout?: string;
234	stderr?: string;
235	exitCode?: number;
236	gitErrorCode?: string;
237	gitCommand?: string;
238}
239
240export class GitError {
241
242	error?: Error;
243	message: string;
244	stdout?: string;
245	stderr?: string;
246	exitCode?: number;
247	gitErrorCode?: string;
248	gitCommand?: string;
249
250	constructor(data: IGitErrorData) {
251		if (data.error) {
252			this.error = data.error;
253			this.message = data.error.message;
254		} else {
255			this.error = undefined;
256			this.message = '';
257		}
258
259		this.message = this.message || data.message || 'Git error';
260		this.stdout = data.stdout;
261		this.stderr = data.stderr;
262		this.exitCode = data.exitCode;
263		this.gitErrorCode = data.gitErrorCode;
264		this.gitCommand = data.gitCommand;
265	}
266
267	toString(): string {
268		let result = this.message + ' ' + JSON.stringify({
269			exitCode: this.exitCode,
270			gitErrorCode: this.gitErrorCode,
271			gitCommand: this.gitCommand,
272			stdout: this.stdout,
273			stderr: this.stderr
274		}, null, 2);
275
276		if (this.error) {
277			result += (<any>this.error).stack;
278		}
279
280		return result;
281	}
282}
283
284export interface IGitOptions {
285	gitPath: string;
286	version: string;
287	env?: any;
288}
289
290function getGitErrorCode(stderr: string): string | undefined {
291	if (/Another git process seems to be running in this repository|If no other git process is currently running/.test(stderr)) {
292		return GitErrorCodes.RepositoryIsLocked;
293	} else if (/Authentication failed/.test(stderr)) {
294		return GitErrorCodes.AuthenticationFailed;
295	} else if (/Not a git repository/i.test(stderr)) {
296		return GitErrorCodes.NotAGitRepository;
297	} else if (/bad config file/.test(stderr)) {
298		return GitErrorCodes.BadConfigFile;
299	} else if (/cannot make pipe for command substitution|cannot create standard input pipe/.test(stderr)) {
300		return GitErrorCodes.CantCreatePipe;
301	} else if (/Repository not found/.test(stderr)) {
302		return GitErrorCodes.RepositoryNotFound;
303	} else if (/unable to access/.test(stderr)) {
304		return GitErrorCodes.CantAccessRemote;
305	} else if (/branch '.+' is not fully merged/.test(stderr)) {
306		return GitErrorCodes.BranchNotFullyMerged;
307	} else if (/Couldn\'t find remote ref/.test(stderr)) {
308		return GitErrorCodes.NoRemoteReference;
309	} else if (/A branch named '.+' already exists/.test(stderr)) {
310		return GitErrorCodes.BranchAlreadyExists;
311	} else if (/'.+' is not a valid branch name/.test(stderr)) {
312		return GitErrorCodes.InvalidBranchName;
313	} else if (/Please,? commit your changes or stash them/.test(stderr)) {
314		return GitErrorCodes.DirtyWorkTree;
315	}
316
317	return undefined;
318}
319
320const COMMIT_FORMAT = '%H\n%ae\n%P\n%B';
321
322export class Git {
323
324	readonly path: string;
325	private env: any;
326
327	private _onOutput = new EventEmitter();
328	get onOutput(): EventEmitter { return this._onOutput; }
329
330	constructor(options: IGitOptions) {
331		this.path = options.gitPath;
332		this.env = options.env || {};
333	}
334
335	open(repository: string, dotGit: string): Repository {
336		return new Repository(this, repository, dotGit);
337	}
338
339	async init(repository: string): Promise<void> {
340		await this.exec(repository, ['init']);
341		return;
342	}
343
344	async clone(url: string, parentPath: string, cancellationToken?: CancellationToken): Promise<string> {
345		let baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository';
346		let folderName = baseFolderName;
347		let folderPath = path.join(parentPath, folderName);
348		let count = 1;
349
350		while (count < 20 && await new Promise(c => fs.exists(folderPath, c))) {
351			folderName = `${baseFolderName}-${count++}`;
352			folderPath = path.join(parentPath, folderName);
353		}
354
355		await mkdirp(parentPath);
356
357		try {
358			await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath], { cancellationToken });
359		} catch (err) {
360			if (err.stderr) {
361				err.stderr = err.stderr.replace(/^Cloning.+$/m, '').trim();
362				err.stderr = err.stderr.replace(/^ERROR:\s+/, '').trim();
363			}
364
365			throw err;
366		}
367
368		return folderPath;
369	}
370
371	async getRepositoryRoot(repositoryPath: string): Promise<string> {
372		const result = await this.exec(repositoryPath, ['rev-parse', '--show-toplevel']);
373		return path.normalize(result.stdout.trim());
374	}
375
376	async getRepositoryDotGit(repositoryPath: string): Promise<string> {
377		const result = await this.exec(repositoryPath, ['rev-parse', '--git-dir']);
378		let dotGitPath = result.stdout.trim();
379
380		if (!path.isAbsolute(dotGitPath)) {
381			dotGitPath = path.join(repositoryPath, dotGitPath);
382		}
383
384		return path.normalize(dotGitPath);
385	}
386
387	async exec(cwd: string, args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
388		options = assign({ cwd }, options || {});
389		return await this._exec(args, options);
390	}
391
392	async exec2(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
393		return await this._exec(args, options);
394	}
395
396	stream(cwd: string, args: string[], options: SpawnOptions = {}): cp.ChildProcess {
397		options = assign({ cwd }, options || {});
398		return this.spawn(args, options);
399	}
400
401	private async _exec(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
402		const child = this.spawn(args, options);
403
404		if (options.input) {
405			child.stdin.end(options.input, 'utf8');
406		}
407
408		const bufferResult = await exec(child, options.cancellationToken);
409
410		if (options.log !== false && bufferResult.stderr.length > 0) {
411			this.log(`${bufferResult.stderr}\n`);
412		}
413
414		let encoding = options.encoding || 'utf8';
415		encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
416
417		const result: IExecutionResult<string> = {
418			exitCode: bufferResult.exitCode,
419			stdout: iconv.decode(bufferResult.stdout, encoding),
420			stderr: bufferResult.stderr
421		};
422
423		if (bufferResult.exitCode) {
424			return Promise.reject<IExecutionResult<string>>(new GitError({
425				message: 'Failed to execute git',
426				stdout: result.stdout,
427				stderr: result.stderr,
428				exitCode: result.exitCode,
429				gitErrorCode: getGitErrorCode(result.stderr),
430				gitCommand: args[0]
431			}));
432		}
433
434		return result;
435	}
436
437	spawn(args: string[], options: SpawnOptions = {}): cp.ChildProcess {
438		if (!this.path) {
439			throw new Error('git could not be found in the system.');
440		}
441
442		if (!options) {
443			options = {};
444		}
445
446		if (!options.stdio && !options.input) {
447			options.stdio = ['ignore', null, null]; // Unless provided, ignore stdin and leave default streams for stdout and stderr
448		}
449
450		options.env = assign({}, process.env, this.env, options.env || {}, {
451			VSCODE_GIT_COMMAND: args[0],
452			LC_ALL: 'en_US.UTF-8',
453			LANG: 'en_US.UTF-8'
454		});
455
456		if (options.log !== false) {
457			this.log(`> git ${args.join(' ')}\n`);
458		}
459
460		return cp.spawn(this.path, args, options);
461	}
462
463	private log(output: string): void {
464		this._onOutput.emit('log', output);
465	}
466}
467
468export interface Commit {
469	hash: string;
470	message: string;
471	parents: string[];
472	authorEmail?: string | undefined;
473}
474
475export class GitStatusParser {
476
477	private lastRaw = '';
478	private result: IFileStatus[] = [];
479
480	get status(): IFileStatus[] {
481		return this.result;
482	}
483
484	update(raw: string): void {
485		let i = 0;
486		let nextI: number | undefined;
487
488		raw = this.lastRaw + raw;
489
490		while ((nextI = this.parseEntry(raw, i)) !== undefined) {
491			i = nextI;
492		}
493
494		this.lastRaw = raw.substr(i);
495	}
496
497	private parseEntry(raw: string, i: number): number | undefined {
498		if (i + 4 >= raw.length) {
499			return;
500		}
501
502		let lastIndex: number;
503		const entry: IFileStatus = {
504			x: raw.charAt(i++),
505			y: raw.charAt(i++),
506			rename: undefined,
507			path: ''
508		};
509
510		// space
511		i++;
512
513		if (entry.x === 'R' || entry.x === 'C') {
514			lastIndex = raw.indexOf('\0', i);
515
516			if (lastIndex === -1) {
517				return;
518			}
519
520			entry.rename = raw.substring(i, lastIndex);
521			i = lastIndex + 1;
522		}
523
524		lastIndex = raw.indexOf('\0', i);
525
526		if (lastIndex === -1) {
527			return;
528		}
529
530		entry.path = raw.substring(i, lastIndex);
531
532		// If path ends with slash, it must be a nested git repo
533		if (entry.path[entry.path.length - 1] !== '/') {
534			this.result.push(entry);
535		}
536
537		return lastIndex + 1;
538	}
539}
540
541export interface Submodule {
542	name: string;
543	path: string;
544	url: string;
545}
546
547export function parseGitmodules(raw: string): Submodule[] {
548	const regex = /\r?\n/g;
549	let position = 0;
550	let match: RegExpExecArray | null = null;
551
552	const result: Submodule[] = [];
553	let submodule: Partial<Submodule> = {};
554
555	function parseLine(line: string): void {
556		const sectionMatch = /^\s*\[submodule "([^"]+)"\]\s*$/.exec(line);
557
558		if (sectionMatch) {
559			if (submodule.name && submodule.path && submodule.url) {
560				result.push(submodule as Submodule);
561			}
562
563			const name = sectionMatch[1];
564
565			if (name) {
566				submodule = { name };
567				return;
568			}
569		}
570
571		if (!submodule) {
572			return;
573		}
574
575		const propertyMatch = /^\s*(\w+)\s+=\s+(.*)$/.exec(line);
576
577		if (!propertyMatch) {
578			return;
579		}
580
581		const [, key, value] = propertyMatch;
582
583		switch (key) {
584			case 'path': submodule.path = value; break;
585			case 'url': submodule.url = value; break;
586		}
587	}
588
589	while (match = regex.exec(raw)) {
590		parseLine(raw.substring(position, match.index));
591		position = match.index + match[0].length;
592	}
593
594	parseLine(raw.substring(position));
595
596	if (submodule.name && submodule.path && submodule.url) {
597		result.push(submodule as Submodule);
598	}
599
600	return result;
601}
602
603export function parseGitCommit(raw: string): Commit | null {
604	const match = /^([0-9a-f]{40})\n(.*)\n(.*)(\n([^]*))?$/m.exec(raw.trim());
605	if (!match) {
606		return null;
607	}
608
609	const parents = match[3] ? match[3].split(' ') : [];
610	return { hash: match[1], message: match[5], parents, authorEmail: match[2] };
611}
612
613interface LsTreeElement {
614	mode: string;
615	type: string;
616	object: string;
617	size: string;
618	file: string;
619}
620
621export function parseLsTree(raw: string): LsTreeElement[] {
622	return raw.split('\n')
623		.filter(l => !!l)
624		.map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!)
625		.filter(m => !!m)
626		.map(([, mode, type, object, size, file]) => ({ mode, type, object, size, file }));
627}
628
629interface LsFilesElement {
630	mode: string;
631	object: string;
632	stage: string;
633	file: string;
634}
635
636export function parseLsFiles(raw: string): LsFilesElement[] {
637	return raw.split('\n')
638		.filter(l => !!l)
639		.map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!)
640		.filter(m => !!m)
641		.map(([, mode, object, stage, file]) => ({ mode, object, stage, file }));
642}
643
644export interface CommitOptions {
645	all?: boolean | 'tracked';
646	amend?: boolean;
647	signoff?: boolean;
648	signCommit?: boolean;
649	empty?: boolean;
650}
651
652export interface PullOptions {
653	unshallow?: boolean;
654	tags?: boolean;
655	readonly cancellationToken?: CancellationToken;
656}
657
658export enum ForcePushMode {
659	Force,
660	ForceWithLease
661}
662
663export class Repository {
664
665	constructor(
666		private _git: Git,
667		private repositoryRoot: string,
668		readonly dotGit: string
669	) { }
670
671	get git(): Git {
672		return this._git;
673	}
674
675	get root(): string {
676		return this.repositoryRoot;
677	}
678
679	// TODO@Joao: rename to exec
680	async run(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
681		return await this.git.exec(this.repositoryRoot, args, options);
682	}
683
684	stream(args: string[], options: SpawnOptions = {}): cp.ChildProcess {
685		return this.git.stream(this.repositoryRoot, args, options);
686	}
687
688	spawn(args: string[], options: SpawnOptions = {}): cp.ChildProcess {
689		return this.git.spawn(args, options);
690	}
691
692	async config(scope: string, key: string, value: any = null, options: SpawnOptions = {}): Promise<string> {
693		const args = ['config'];
694
695		if (scope) {
696			args.push('--' + scope);
697		}
698
699		args.push(key);
700
701		if (value) {
702			args.push(value);
703		}
704
705		const result = await this.run(args, options);
706		return result.stdout.trim();
707	}
708
709	async getConfigs(scope: string): Promise<{ key: string; value: string; }[]> {
710		const args = ['config'];
711
712		if (scope) {
713			args.push('--' + scope);
714		}
715
716		args.push('-l');
717
718		const result = await this.run(args);
719		const lines = result.stdout.trim().split(/\r|\r\n|\n/);
720
721		return lines.map(entry => {
722			const equalsIndex = entry.indexOf('=');
723			return { key: entry.substr(0, equalsIndex), value: entry.substr(equalsIndex + 1) };
724		});
725	}
726
727	async log(options?: LogOptions): Promise<Commit[]> {
728		const maxEntries = options && typeof options.maxEntries === 'number' && options.maxEntries > 0 ? options.maxEntries : 32;
729		const args = ['log', '-' + maxEntries, `--pretty=format:${COMMIT_FORMAT}%x00%x00`];
730		const gitResult = await this.run(args);
731		if (gitResult.exitCode) {
732			// An empty repo.
733			return [];
734		}
735
736		const s = gitResult.stdout;
737		const result: Commit[] = [];
738		let index = 0;
739		while (index < s.length) {
740			let nextIndex = s.indexOf('\x00\x00', index);
741			if (nextIndex === -1) {
742				nextIndex = s.length;
743			}
744
745			let entry = s.substr(index, nextIndex - index);
746			if (entry.startsWith('\n')) {
747				entry = entry.substring(1);
748			}
749
750			const commit = parseGitCommit(entry);
751			if (!commit) {
752				break;
753			}
754
755			result.push(commit);
756			index = nextIndex + 2;
757		}
758
759		return result;
760	}
761
762	async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise<string> {
763		const stdout = await this.buffer(object);
764
765		if (autoGuessEncoding) {
766			encoding = detectEncoding(stdout) || encoding;
767		}
768
769		encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
770
771		return iconv.decode(stdout, encoding);
772	}
773
774	async buffer(object: string): Promise<Buffer> {
775		const child = this.stream(['show', object]);
776
777		if (!child.stdout) {
778			return Promise.reject<Buffer>('Can\'t open file from git');
779		}
780
781		const { exitCode, stdout, stderr } = await exec(child);
782
783		if (exitCode) {
784			const err = new GitError({
785				message: 'Could not show object.',
786				exitCode
787			});
788
789			if (/exists on disk, but not in/.test(stderr)) {
790				err.gitErrorCode = GitErrorCodes.WrongCase;
791			}
792
793			return Promise.reject<Buffer>(err);
794		}
795
796		return stdout;
797	}
798
799	async getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }> {
800		if (!treeish) { // index
801			const elements = await this.lsfiles(path);
802
803			if (elements.length === 0) {
804				throw new GitError({ message: 'Path not known by git', gitErrorCode: GitErrorCodes.UnknownPath });
805			}
806
807			const { mode, object } = elements[0];
808			const catFile = await this.run(['cat-file', '-s', object]);
809			const size = parseInt(catFile.stdout);
810
811			return { mode, object, size };
812		}
813
814		const elements = await this.lstree(treeish, path);
815
816		if (elements.length === 0) {
817			throw new GitError({ message: 'Path not known by git', gitErrorCode: GitErrorCodes.UnknownPath });
818		}
819
820		const { mode, object, size } = elements[0];
821		return { mode, object, size: parseInt(size) };
822	}
823
824	async lstree(treeish: string, path: string): Promise<LsTreeElement[]> {
825		const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', path]);
826		return parseLsTree(stdout);
827	}
828
829	async lsfiles(path: string): Promise<LsFilesElement[]> {
830		const { stdout } = await this.run(['ls-files', '--stage', '--', path]);
831		return parseLsFiles(stdout);
832	}
833
834	async getGitRelativePath(ref: string, relativePath: string): Promise<string> {
835		const relativePathLowercase = relativePath.toLowerCase();
836		const dirname = path.posix.dirname(relativePath) + '/';
837		const elements: { file: string; }[] = ref ? await this.lstree(ref, dirname) : await this.lsfiles(dirname);
838		const element = elements.filter(file => file.file.toLowerCase() === relativePathLowercase)[0];
839
840		if (!element) {
841			throw new GitError({ message: 'Git relative path not found.' });
842		}
843
844		return element.file;
845	}
846
847	async detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> {
848		const child = await this.stream(['show', object]);
849		const buffer = await readBytes(child.stdout, 4100);
850
851		try {
852			child.kill();
853		} catch (err) {
854			// noop
855		}
856
857		const encoding = detectUnicodeEncoding(buffer);
858		let isText = true;
859
860		if (encoding !== Encoding.UTF16be && encoding !== Encoding.UTF16le) {
861			for (let i = 0; i < buffer.length; i++) {
862				if (buffer.readInt8(i) === 0) {
863					isText = false;
864					break;
865				}
866			}
867		}
868
869		if (!isText) {
870			const result = filetype(buffer);
871
872			if (!result) {
873				return { mimetype: 'application/octet-stream' };
874			} else {
875				return { mimetype: result.mime };
876			}
877		}
878
879		if (encoding) {
880			return { mimetype: 'text/plain', encoding };
881		} else {
882			// TODO@JOAO: read the setting OUTSIDE!
883			return { mimetype: 'text/plain' };
884		}
885	}
886
887	async apply(patch: string, reverse?: boolean): Promise<void> {
888		const args = ['apply', patch];
889
890		if (reverse) {
891			args.push('-R');
892		}
893
894		try {
895			await this.run(args);
896		} catch (err) {
897			if (/patch does not apply/.test(err.stderr)) {
898				err.gitErrorCode = GitErrorCodes.PatchDoesNotApply;
899			}
900
901			throw err;
902		}
903	}
904
905	async diff(cached = false): Promise<string> {
906		const args = ['diff'];
907
908		if (cached) {
909			args.push('--cached');
910		}
911
912		const result = await this.run(args);
913		return result.stdout;
914	}
915
916	diffWithHEAD(): Promise<Change[]>;
917	diffWithHEAD(path: string): Promise<string>;
918	diffWithHEAD(path?: string | undefined): Promise<string | Change[]>;
919	async diffWithHEAD(path?: string | undefined): Promise<string | Change[]> {
920		if (!path) {
921			return await this.diffFiles(false);
922		}
923
924		const args = ['diff', '--', path];
925		const result = await this.run(args);
926		return result.stdout;
927	}
928
929	diffWith(ref: string): Promise<Change[]>;
930	diffWith(ref: string, path: string): Promise<string>;
931	diffWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
932	async diffWith(ref: string, path?: string): Promise<string | Change[]> {
933		if (!path) {
934			return await this.diffFiles(false, ref);
935		}
936
937		const args = ['diff', ref, '--', path];
938		const result = await this.run(args);
939		return result.stdout;
940	}
941
942	diffIndexWithHEAD(): Promise<Change[]>;
943	diffIndexWithHEAD(path: string): Promise<string>;
944	diffIndexWithHEAD(path?: string | undefined): Promise<string | Change[]>;
945	async diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
946		if (!path) {
947			return await this.diffFiles(true);
948		}
949
950		const args = ['diff', '--cached', '--', path];
951		const result = await this.run(args);
952		return result.stdout;
953	}
954
955	diffIndexWith(ref: string): Promise<Change[]>;
956	diffIndexWith(ref: string, path: string): Promise<string>;
957	diffIndexWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
958	async diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
959		if (!path) {
960			return await this.diffFiles(true, ref);
961		}
962
963		const args = ['diff', '--cached', ref, '--', path];
964		const result = await this.run(args);
965		return result.stdout;
966	}
967
968	async diffBlobs(object1: string, object2: string): Promise<string> {
969		const args = ['diff', object1, object2];
970		const result = await this.run(args);
971		return result.stdout;
972	}
973
974	diffBetween(ref1: string, ref2: string): Promise<Change[]>;
975	diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
976	diffBetween(ref1: string, ref2: string, path?: string | undefined): Promise<string | Change[]>;
977	async diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
978		const range = `${ref1}...${ref2}`;
979		if (!path) {
980			return await this.diffFiles(false, range);
981		}
982
983		const args = ['diff', range, '--', path];
984		const result = await this.run(args);
985
986		return result.stdout.trim();
987	}
988
989	private async diffFiles(cached: boolean, ref?: string): Promise<Change[]> {
990		const args = ['diff', '--name-status', '-z', '--diff-filter=ADMR'];
991		if (cached) {
992			args.push('--cached');
993		}
994
995		if (ref) {
996			args.push(ref);
997		}
998
999		const gitResult = await this.run(args);
1000		if (gitResult.exitCode) {
1001			return [];
1002		}
1003
1004		const entries = gitResult.stdout.split('\x00');
1005		let index = 0;
1006		const result: Change[] = [];
1007
1008		entriesLoop:
1009		while (index < entries.length - 1) {
1010			const change = entries[index++];
1011			const resourcePath = entries[index++];
1012			if (!change || !resourcePath) {
1013				break;
1014			}
1015
1016			const originalUri = URI.file(path.isAbsolute(resourcePath) ? resourcePath : path.join(this.repositoryRoot, resourcePath));
1017			let status: Status = Status.UNTRACKED;
1018
1019			// Copy or Rename status comes with a number, e.g. 'R100'. We don't need the number, so we use only first character of the status.
1020			switch (change[0]) {
1021				case 'M':
1022					status = Status.MODIFIED;
1023					break;
1024
1025				case 'A':
1026					status = Status.INDEX_ADDED;
1027					break;
1028
1029				case 'D':
1030					status = Status.DELETED;
1031					break;
1032
1033				// Rename contains two paths, the second one is what the file is renamed/copied to.
1034				case 'R':
1035					if (index >= entries.length) {
1036						break;
1037					}
1038
1039					const newPath = entries[index++];
1040					if (!newPath) {
1041						break;
1042					}
1043
1044					const uri = URI.file(path.isAbsolute(newPath) ? newPath : path.join(this.repositoryRoot, newPath));
1045					result.push({
1046						uri,
1047						renameUri: uri,
1048						originalUri,
1049						status: Status.INDEX_RENAMED
1050					});
1051
1052					continue;
1053
1054				default:
1055					// Unknown status
1056					break entriesLoop;
1057			}
1058
1059			result.push({
1060				status,
1061				originalUri,
1062				uri: originalUri,
1063				renameUri: originalUri,
1064			});
1065		}
1066
1067		return result;
1068	}
1069
1070	async getMergeBase(ref1: string, ref2: string): Promise<string> {
1071		const args = ['merge-base', ref1, ref2];
1072		const result = await this.run(args);
1073
1074		return result.stdout.trim();
1075	}
1076
1077	async hashObject(data: string): Promise<string> {
1078		const args = ['hash-object', '-w', '--stdin'];
1079		const result = await this.run(args, { input: data });
1080
1081		return result.stdout.trim();
1082	}
1083
1084	async add(paths: string[], opts?: { update?: boolean }): Promise<void> {
1085		const args = ['add'];
1086
1087		if (opts && opts.update) {
1088			args.push('-u');
1089		} else {
1090			args.push('-A');
1091		}
1092
1093		args.push('--');
1094
1095		if (paths && paths.length) {
1096			args.push.apply(args, paths);
1097		} else {
1098			args.push('.');
1099		}
1100
1101		await this.run(args);
1102	}
1103
1104	async rm(paths: string[]): Promise<void> {
1105		const args = ['rm', '--'];
1106
1107		if (!paths || !paths.length) {
1108			return;
1109		}
1110
1111		args.push(...paths);
1112
1113		await this.run(args);
1114	}
1115
1116	async stage(path: string, data: string): Promise<void> {
1117		const child = this.stream(['hash-object', '--stdin', '-w', '--path', path], { stdio: [null, null, null] });
1118		child.stdin.end(data, 'utf8');
1119
1120		const { exitCode, stdout } = await exec(child);
1121		const hash = stdout.toString('utf8');
1122
1123		if (exitCode) {
1124			throw new GitError({
1125				message: 'Could not hash object.',
1126				exitCode: exitCode
1127			});
1128		}
1129
1130		let mode: string;
1131		let add: string = '';
1132
1133		try {
1134			const details = await this.getObjectDetails('HEAD', path);
1135			mode = details.mode;
1136		} catch (err) {
1137			if (err.gitErrorCode !== GitErrorCodes.UnknownPath) {
1138				throw err;
1139			}
1140
1141			mode = '100644';
1142			add = '--add';
1143		}
1144
1145		await this.run(['update-index', add, '--cacheinfo', mode, hash, path]);
1146	}
1147
1148	async checkout(treeish: string, paths: string[], opts: { track?: boolean } = Object.create(null)): Promise<void> {
1149		const args = ['checkout', '-q'];
1150
1151		if (opts.track) {
1152			args.push('--track');
1153		}
1154
1155		if (treeish) {
1156			args.push(treeish);
1157		}
1158
1159		try {
1160			if (paths && paths.length > 0) {
1161				for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
1162					await this.run([...args, '--', ...chunk]);
1163				}
1164			} else {
1165				await this.run(args);
1166			}
1167		} catch (err) {
1168			if (/Please,? commit your changes or stash them/.test(err.stderr || '')) {
1169				err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
1170			}
1171
1172			throw err;
1173		}
1174	}
1175
1176	async commit(message: string, opts: CommitOptions = Object.create(null)): Promise<void> {
1177		const args = ['commit', '--quiet', '--allow-empty-message', '--file', '-'];
1178
1179		if (opts.all) {
1180			args.push('--all');
1181		}
1182
1183		if (opts.amend) {
1184			args.push('--amend');
1185		}
1186
1187		if (opts.signoff) {
1188			args.push('--signoff');
1189		}
1190
1191		if (opts.signCommit) {
1192			args.push('-S');
1193		}
1194		if (opts.empty) {
1195			args.push('--allow-empty');
1196		}
1197
1198		try {
1199			await this.run(args, { input: message || '' });
1200		} catch (commitErr) {
1201			await this.handleCommitError(commitErr);
1202		}
1203	}
1204
1205	async rebaseContinue(): Promise<void> {
1206		const args = ['rebase', '--continue'];
1207
1208		try {
1209			await this.run(args);
1210		} catch (commitErr) {
1211			await this.handleCommitError(commitErr);
1212		}
1213	}
1214
1215	private async handleCommitError(commitErr: any): Promise<void> {
1216		if (/not possible because you have unmerged files/.test(commitErr.stderr || '')) {
1217			commitErr.gitErrorCode = GitErrorCodes.UnmergedChanges;
1218			throw commitErr;
1219		}
1220
1221		try {
1222			await this.run(['config', '--get-all', 'user.name']);
1223		} catch (err) {
1224			err.gitErrorCode = GitErrorCodes.NoUserNameConfigured;
1225			throw err;
1226		}
1227
1228		try {
1229			await this.run(['config', '--get-all', 'user.email']);
1230		} catch (err) {
1231			err.gitErrorCode = GitErrorCodes.NoUserEmailConfigured;
1232			throw err;
1233		}
1234
1235		throw commitErr;
1236	}
1237
1238	async branch(name: string, checkout: boolean, ref?: string): Promise<void> {
1239		const args = checkout ? ['checkout', '-q', '-b', name, '--no-track'] : ['branch', '-q', name];
1240
1241		if (ref) {
1242			args.push(ref);
1243		}
1244
1245		await this.run(args);
1246	}
1247
1248	async deleteBranch(name: string, force?: boolean): Promise<void> {
1249		const args = ['branch', force ? '-D' : '-d', name];
1250		await this.run(args);
1251	}
1252
1253	async renameBranch(name: string): Promise<void> {
1254		const args = ['branch', '-m', name];
1255		await this.run(args);
1256	}
1257
1258	async setBranchUpstream(name: string, upstream: string): Promise<void> {
1259		const args = ['branch', '--set-upstream-to', upstream, name];
1260		await this.run(args);
1261	}
1262
1263	async deleteRef(ref: string): Promise<void> {
1264		const args = ['update-ref', '-d', ref];
1265		await this.run(args);
1266	}
1267
1268	async merge(ref: string): Promise<void> {
1269		const args = ['merge', ref];
1270
1271		try {
1272			await this.run(args);
1273		} catch (err) {
1274			if (/^CONFLICT /m.test(err.stdout || '')) {
1275				err.gitErrorCode = GitErrorCodes.Conflict;
1276			}
1277
1278			throw err;
1279		}
1280	}
1281
1282	async tag(name: string, message?: string): Promise<void> {
1283		let args = ['tag'];
1284
1285		if (message) {
1286			args = [...args, '-a', name, '-m', message];
1287		} else {
1288			args = [...args, name];
1289		}
1290
1291		await this.run(args);
1292	}
1293
1294	async clean(paths: string[]): Promise<void> {
1295		const pathsByGroup = groupBy(paths, p => path.dirname(p));
1296		const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]);
1297
1298		const limiter = new Limiter(5);
1299		const promises: Promise<any>[] = [];
1300
1301		for (const paths of groups) {
1302			for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
1303				promises.push(limiter.queue(() => this.run(['clean', '-f', '-q', '--', ...chunk])));
1304			}
1305		}
1306
1307		await Promise.all(promises);
1308	}
1309
1310	async undo(): Promise<void> {
1311		await this.run(['clean', '-fd']);
1312
1313		try {
1314			await this.run(['checkout', '--', '.']);
1315		} catch (err) {
1316			if (/did not match any file\(s\) known to git\./.test(err.stderr || '')) {
1317				return;
1318			}
1319
1320			throw err;
1321		}
1322	}
1323
1324	async reset(treeish: string, hard: boolean = false): Promise<void> {
1325		const args = ['reset', hard ? '--hard' : '--soft', treeish];
1326		await this.run(args);
1327	}
1328
1329	async revert(treeish: string, paths: string[]): Promise<void> {
1330		const result = await this.run(['branch']);
1331		let args: string[];
1332
1333		// In case there are no branches, we must use rm --cached
1334		if (!result.stdout) {
1335			args = ['rm', '--cached', '-r', '--'];
1336		} else {
1337			args = ['reset', '-q', treeish, '--'];
1338		}
1339
1340		if (paths && paths.length) {
1341			args.push.apply(args, paths);
1342		} else {
1343			args.push('.');
1344		}
1345
1346		try {
1347			await this.run(args);
1348		} catch (err) {
1349			// In case there are merge conflicts to be resolved, git reset will output
1350			// some "needs merge" data. We try to get around that.
1351			if (/([^:]+: needs merge\n)+/m.test(err.stdout || '')) {
1352				return;
1353			}
1354
1355			throw err;
1356		}
1357	}
1358
1359	async addRemote(name: string, url: string): Promise<void> {
1360		const args = ['remote', 'add', name, url];
1361		await this.run(args);
1362	}
1363
1364	async removeRemote(name: string): Promise<void> {
1365		const args = ['remote', 'rm', name];
1366		await this.run(args);
1367	}
1368
1369	async fetch(options: { remote?: string, ref?: string, all?: boolean, prune?: boolean, depth?: number } = {}): Promise<void> {
1370		const args = ['fetch'];
1371
1372		if (options.remote) {
1373			args.push(options.remote);
1374
1375			if (options.ref) {
1376				args.push(options.ref);
1377			}
1378		} else if (options.all) {
1379			args.push('--all');
1380		}
1381
1382		if (options.prune) {
1383			args.push('--prune');
1384		}
1385
1386		if (typeof options.depth === 'number') {
1387			args.push(`--depth=${options.depth}`);
1388		}
1389
1390		try {
1391			await this.run(args);
1392		} catch (err) {
1393			if (/No remote repository specified\./.test(err.stderr || '')) {
1394				err.gitErrorCode = GitErrorCodes.NoRemoteRepositorySpecified;
1395			} else if (/Could not read from remote repository/.test(err.stderr || '')) {
1396				err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
1397			}
1398
1399			throw err;
1400		}
1401	}
1402
1403	async pull(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> {
1404		const args = ['pull'];
1405
1406		if (options.tags) {
1407			args.push('--tags');
1408		}
1409
1410		if (options.unshallow) {
1411			args.push('--unshallow');
1412		}
1413
1414		if (rebase) {
1415			args.push('-r');
1416		}
1417
1418		if (remote && branch) {
1419			args.push(remote);
1420			args.push(branch);
1421		}
1422
1423		try {
1424			await this.run(args, options);
1425		} catch (err) {
1426			if (/^CONFLICT \([^)]+\): \b/m.test(err.stdout || '')) {
1427				err.gitErrorCode = GitErrorCodes.Conflict;
1428			} else if (/Please tell me who you are\./.test(err.stderr || '')) {
1429				err.gitErrorCode = GitErrorCodes.NoUserNameConfigured;
1430			} else if (/Could not read from remote repository/.test(err.stderr || '')) {
1431				err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
1432			} else if (/Pull is not possible because you have unmerged files|Cannot pull with rebase: You have unstaged changes|Your local changes to the following files would be overwritten|Please, commit your changes before you can merge/i.test(err.stderr)) {
1433				err.stderr = err.stderr.replace(/Cannot pull with rebase: You have unstaged changes/i, 'Cannot pull with rebase, you have unstaged changes');
1434				err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
1435			} else if (/cannot lock ref|unable to update local ref/i.test(err.stderr || '')) {
1436				err.gitErrorCode = GitErrorCodes.CantLockRef;
1437			} else if (/cannot rebase onto multiple branches/i.test(err.stderr || '')) {
1438				err.gitErrorCode = GitErrorCodes.CantRebaseMultipleBranches;
1439			}
1440
1441			throw err;
1442		}
1443	}
1444
1445	async push(remote?: string, name?: string, setUpstream: boolean = false, tags = false, forcePushMode?: ForcePushMode): Promise<void> {
1446		const args = ['push'];
1447
1448		if (forcePushMode === ForcePushMode.ForceWithLease) {
1449			args.push('--force-with-lease');
1450		} else if (forcePushMode === ForcePushMode.Force) {
1451			args.push('--force');
1452		}
1453
1454		if (setUpstream) {
1455			args.push('-u');
1456		}
1457
1458		if (tags) {
1459			args.push('--follow-tags');
1460		}
1461
1462		if (remote) {
1463			args.push(remote);
1464		}
1465
1466		if (name) {
1467			args.push(name);
1468		}
1469
1470		try {
1471			await this.run(args);
1472		} catch (err) {
1473			if (/^error: failed to push some refs to\b/m.test(err.stderr || '')) {
1474				err.gitErrorCode = GitErrorCodes.PushRejected;
1475			} else if (/Could not read from remote repository/.test(err.stderr || '')) {
1476				err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
1477			} else if (/^fatal: The current branch .* has no upstream branch/.test(err.stderr || '')) {
1478				err.gitErrorCode = GitErrorCodes.NoUpstreamBranch;
1479			}
1480
1481			throw err;
1482		}
1483	}
1484
1485	async blame(path: string): Promise<string> {
1486		try {
1487			const args = ['blame'];
1488			args.push(path);
1489
1490			let result = await this.run(args);
1491
1492			return result.stdout.trim();
1493		} catch (err) {
1494			if (/^fatal: no such path/.test(err.stderr || '')) {
1495				err.gitErrorCode = GitErrorCodes.NoPathFound;
1496			}
1497
1498			throw err;
1499		}
1500	}
1501
1502	async createStash(message?: string, includeUntracked?: boolean): Promise<void> {
1503		try {
1504			const args = ['stash', 'push'];
1505
1506			if (includeUntracked) {
1507				args.push('-u');
1508			}
1509
1510			if (message) {
1511				args.push('-m', message);
1512			}
1513
1514			await this.run(args);
1515		} catch (err) {
1516			if (/No local changes to save/.test(err.stderr || '')) {
1517				err.gitErrorCode = GitErrorCodes.NoLocalChanges;
1518			}
1519
1520			throw err;
1521		}
1522	}
1523
1524	async popStash(index?: number): Promise<void> {
1525		const args = ['stash', 'pop'];
1526		await this.popOrApplyStash(args, index);
1527	}
1528
1529	async applyStash(index?: number): Promise<void> {
1530		const args = ['stash', 'apply'];
1531		await this.popOrApplyStash(args, index);
1532	}
1533
1534	private async popOrApplyStash(args: string[], index?: number): Promise<void> {
1535		try {
1536			if (typeof index === 'number') {
1537				args.push(`stash@{${index}}`);
1538			}
1539
1540			await this.run(args);
1541		} catch (err) {
1542			if (/No stash found/.test(err.stderr || '')) {
1543				err.gitErrorCode = GitErrorCodes.NoStashFound;
1544			} else if (/error: Your local changes to the following files would be overwritten/.test(err.stderr || '')) {
1545				err.gitErrorCode = GitErrorCodes.LocalChangesOverwritten;
1546			} else if (/^CONFLICT/m.test(err.stdout || '')) {
1547				err.gitErrorCode = GitErrorCodes.StashConflict;
1548			}
1549
1550			throw err;
1551		}
1552	}
1553
1554	getStatus(limit = 5000): Promise<{ status: IFileStatus[]; didHitLimit: boolean; }> {
1555		return new Promise<{ status: IFileStatus[]; didHitLimit: boolean; }>((c, e) => {
1556			const parser = new GitStatusParser();
1557			const env = { GIT_OPTIONAL_LOCKS: '0' };
1558			const child = this.stream(['status', '-z', '-u'], { env });
1559
1560			const onExit = (exitCode: number) => {
1561				if (exitCode !== 0) {
1562					const stderr = stderrData.join('');
1563					return e(new GitError({
1564						message: 'Failed to execute git',
1565						stderr,
1566						exitCode,
1567						gitErrorCode: getGitErrorCode(stderr),
1568						gitCommand: 'status'
1569					}));
1570				}
1571
1572				c({ status: parser.status, didHitLimit: false });
1573			};
1574
1575			const onStdoutData = (raw: string) => {
1576				parser.update(raw);
1577
1578				if (parser.status.length > limit) {
1579					child.removeListener('exit', onExit);
1580					child.stdout.removeListener('data', onStdoutData);
1581					child.kill();
1582
1583					c({ status: parser.status.slice(0, limit), didHitLimit: true });
1584				}
1585			};
1586
1587			child.stdout.setEncoding('utf8');
1588			child.stdout.on('data', onStdoutData);
1589
1590			const stderrData: string[] = [];
1591			child.stderr.setEncoding('utf8');
1592			child.stderr.on('data', raw => stderrData.push(raw as string));
1593
1594			child.on('error', cpErrorHandler(e));
1595			child.on('exit', onExit);
1596		});
1597	}
1598
1599	async getHEAD(): Promise<Ref> {
1600		try {
1601			const result = await this.run(['symbolic-ref', '--short', 'HEAD']);
1602
1603			if (!result.stdout) {
1604				throw new Error('Not in a branch');
1605			}
1606
1607			return { name: result.stdout.trim(), commit: undefined, type: RefType.Head };
1608		} catch (err) {
1609			const result = await this.run(['rev-parse', 'HEAD']);
1610
1611			if (!result.stdout) {
1612				throw new Error('Error parsing HEAD');
1613			}
1614
1615			return { name: undefined, commit: result.stdout.trim(), type: RefType.Head };
1616		}
1617	}
1618
1619	async findTrackingBranches(upstreamBranch: string): Promise<Branch[]> {
1620		const result = await this.run(['for-each-ref', '--format', '%(refname:short)%00%(upstream:short)', 'refs/heads']);
1621		return result.stdout.trim().split('\n')
1622			.map(line => line.trim().split('\0'))
1623			.filter(([_, upstream]) => upstream === upstreamBranch)
1624			.map(([ref]) => ({ name: ref, type: RefType.Head } as Branch));
1625	}
1626
1627	async getRefs(opts?: { sort?: 'alphabetically' | 'committerdate' }): Promise<Ref[]> {
1628		const args = ['for-each-ref', '--format', '%(refname) %(objectname)'];
1629
1630		if (opts && opts.sort && opts.sort !== 'alphabetically') {
1631			args.push('--sort', opts.sort);
1632		}
1633
1634		const result = await this.run(args);
1635
1636		const fn = (line: string): Ref | null => {
1637			let match: RegExpExecArray | null;
1638
1639			if (match = /^refs\/heads\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
1640				return { name: match[1], commit: match[2], type: RefType.Head };
1641			} else if (match = /^refs\/remotes\/([^/]+)\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
1642				return { name: `${match[1]}/${match[2]}`, commit: match[3], type: RefType.RemoteHead, remote: match[1] };
1643			} else if (match = /^refs\/tags\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
1644				return { name: match[1], commit: match[2], type: RefType.Tag };
1645			}
1646
1647			return null;
1648		};
1649
1650		return result.stdout.trim().split('\n')
1651			.filter(line => !!line)
1652			.map(fn)
1653			.filter(ref => !!ref) as Ref[];
1654	}
1655
1656	async getStashes(): Promise<Stash[]> {
1657		const result = await this.run(['stash', 'list']);
1658		const regex = /^stash@{(\d+)}:(.+)$/;
1659		const rawStashes = result.stdout.trim().split('\n')
1660			.filter(b => !!b)
1661			.map(line => regex.exec(line) as RegExpExecArray)
1662			.filter(g => !!g)
1663			.map(([, index, description]: RegExpExecArray) => ({ index: parseInt(index), description }));
1664
1665		return rawStashes;
1666	}
1667
1668	async getRemotes(): Promise<Remote[]> {
1669		const result = await this.run(['remote', '--verbose']);
1670		const lines = result.stdout.trim().split('\n').filter(l => !!l);
1671		const remotes: MutableRemote[] = [];
1672
1673		for (const line of lines) {
1674			const parts = line.split(/\s/);
1675			const [name, url, type] = parts;
1676
1677			let remote = remotes.find(r => r.name === name);
1678
1679			if (!remote) {
1680				remote = { name, isReadOnly: false };
1681				remotes.push(remote);
1682			}
1683
1684			if (/fetch/i.test(type)) {
1685				remote.fetchUrl = url;
1686			} else if (/push/i.test(type)) {
1687				remote.pushUrl = url;
1688			} else {
1689				remote.fetchUrl = url;
1690				remote.pushUrl = url;
1691			}
1692
1693			// https://github.com/Microsoft/vscode/issues/45271
1694			remote.isReadOnly = remote.pushUrl === undefined || remote.pushUrl === 'no_push';
1695		}
1696
1697		return remotes;
1698	}
1699
1700	async getBranch(name: string): Promise<Branch> {
1701		if (name === 'HEAD') {
1702			return this.getHEAD();
1703		}
1704
1705		let result = await this.run(['rev-parse', name]);
1706
1707		if (!result.stdout && /^@/.test(name)) {
1708			const symbolicFullNameResult = await this.run(['rev-parse', '--symbolic-full-name', name]);
1709			name = symbolicFullNameResult.stdout.trim();
1710
1711			result = await this.run(['rev-parse', name]);
1712		}
1713
1714		if (!result.stdout) {
1715			return Promise.reject<Branch>(new Error('No such branch'));
1716		}
1717
1718		const commit = result.stdout.trim();
1719
1720		try {
1721			const res2 = await this.run(['rev-parse', '--symbolic-full-name', name + '@{u}']);
1722			const fullUpstream = res2.stdout.trim();
1723			const match = /^refs\/remotes\/([^/]+)\/(.+)$/.exec(fullUpstream);
1724
1725			if (!match) {
1726				throw new Error(`Could not parse upstream branch: ${fullUpstream}`);
1727			}
1728
1729			const upstream = { remote: match[1], name: match[2] };
1730			const res3 = await this.run(['rev-list', '--left-right', name + '...' + fullUpstream]);
1731
1732			let ahead = 0, behind = 0;
1733			let i = 0;
1734
1735			while (i < res3.stdout.length) {
1736				switch (res3.stdout.charAt(i)) {
1737					case '<': ahead++; break;
1738					case '>': behind++; break;
1739					default: i++; break;
1740				}
1741
1742				while (res3.stdout.charAt(i++) !== '\n') { /* no-op */ }
1743			}
1744
1745			return { name, type: RefType.Head, commit, upstream, ahead, behind };
1746		} catch (err) {
1747			return { name, type: RefType.Head, commit };
1748		}
1749	}
1750
1751	async getCommitTemplate(): Promise<string> {
1752		try {
1753			const result = await this.run(['config', '--get', 'commit.template']);
1754
1755			if (!result.stdout) {
1756				return '';
1757			}
1758
1759			// https://github.com/git/git/blob/3a0f269e7c82aa3a87323cb7ae04ac5f129f036b/path.c#L612
1760			const homedir = os.homedir();
1761			let templatePath = result.stdout.trim()
1762				.replace(/^~([^\/]*)\//, (_, user) => `${user ? path.join(path.dirname(homedir), user) : homedir}/`);
1763
1764			if (!path.isAbsolute(templatePath)) {
1765				templatePath = path.join(this.repositoryRoot, templatePath);
1766			}
1767
1768			const raw = await readfile(templatePath, 'utf8');
1769			return raw.replace(/\n?#.*/g, '');
1770
1771		} catch (err) {
1772			return '';
1773		}
1774	}
1775
1776	async getCommit(ref: string): Promise<Commit> {
1777		const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, ref]);
1778		return parseGitCommit(result.stdout) || Promise.reject<Commit>('bad commit format');
1779	}
1780
1781	async updateSubmodules(paths: string[]): Promise<void> {
1782		const args = ['submodule', 'update', '--'];
1783
1784		for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
1785			await this.run([...args, ...chunk]);
1786		}
1787	}
1788
1789	async getSubmodules(): Promise<Submodule[]> {
1790		const gitmodulesPath = path.join(this.root, '.gitmodules');
1791
1792		try {
1793			const gitmodulesRaw = await readfile(gitmodulesPath, 'utf8');
1794			return parseGitmodules(gitmodulesRaw);
1795		} catch (err) {
1796			if (/ENOENT/.test(err.message)) {
1797				return [];
1798			}
1799
1800			throw err;
1801		}
1802	}
1803}
1804