git.ts 48.4 KB
Newer Older
J
Joao Moreno 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

J
Joao Moreno 已提交
6
import { promises as fs, exists } from 'fs';
J
Joao Moreno 已提交
7
import * as path from 'path';
J
Joao Moreno 已提交
8
import * as os from 'os';
J
Joao Moreno 已提交
9
import * as cp from 'child_process';
10
import * as which from 'which';
11
import { EventEmitter } from 'events';
12
import iconv = require('iconv-lite');
J
Joao Moreno 已提交
13
import * as filetype from 'file-type';
J
Joao Moreno 已提交
14
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util';
15
import { CancellationToken, Progress } from 'vscode';
J
Joao Moreno 已提交
16
import { URI } from 'vscode-uri';
J
Joao Moreno 已提交
17
import { detectEncoding } from './encoding';
J
Joao Moreno 已提交
18
import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git';
J
Joao Moreno 已提交
19 20
import * as byline from 'byline';
import { StringDecoder } from 'string_decoder';
J
Joao Moreno 已提交
21

J
Joao Moreno 已提交
22 23
// https://github.com/microsoft/vscode/issues/65693
const MAX_CLI_LENGTH = 30000;
J
Joao Moreno 已提交
24 25 26 27 28 29

export interface IGit {
	path: string;
	version: string;
}

J
Joao Moreno 已提交
30 31 32 33 34 35 36
export interface IFileStatus {
	x: string;
	y: string;
	path: string;
	rename?: string;
}

37
export interface Stash {
J
Joao Moreno 已提交
38
	index: number;
39 40 41
	description: string;
}

J
Joao Moreno 已提交
42 43 44 45
interface MutableRemote extends Remote {
	fetchUrl?: string;
	pushUrl?: string;
	isReadOnly: boolean;
J
Joao Moreno 已提交
46 47
}

J
Joao Moreno 已提交
48 49 50 51
function parseVersion(raw: string): string {
	return raw.replace(/^git version /, '');
}

J
Joao Moreno 已提交
52
function findSpecificGit(path: string, onLookup: (path: string) => void): Promise<IGit> {
J
Joao Moreno 已提交
53
	return new Promise<IGit>((c, e) => {
J
Joao Moreno 已提交
54 55
		onLookup(path);

J
Joao Moreno 已提交
56 57
		const buffers: Buffer[] = [];
		const child = cp.spawn(path, ['--version']);
58
		child.stdout.on('data', (b: Buffer) => buffers.push(b));
J
Joao Moreno 已提交
59
		child.on('error', cpErrorHandler(e));
J
Joao Moreno 已提交
60 61 62 63
		child.on('exit', code => code ? e(new Error('Not found')) : c({ path, version: parseVersion(Buffer.concat(buffers).toString('utf8').trim()) }));
	});
}

J
Joao Moreno 已提交
64
function findGitDarwin(onLookup: (path: string) => void): Promise<IGit> {
J
Joao Moreno 已提交
65 66 67 68 69 70 71 72 73
	return new Promise<IGit>((c, e) => {
		cp.exec('which git', (err, gitPathBuffer) => {
			if (err) {
				return e('git not found');
			}

			const path = gitPathBuffer.toString().replace(/^\s+|\s+$/g, '');

			function getVersion(path: string) {
J
Joao Moreno 已提交
74 75
				onLookup(path);

J
Joao Moreno 已提交
76
				// make sure git executes
J
Joao 已提交
77
				cp.exec('git --version', (err, stdout) => {
J
Joao Moreno 已提交
78

J
Joao Moreno 已提交
79 80 81 82
					if (err) {
						return e('git not found');
					}

J
Joao 已提交
83
					return c({ path, version: parseVersion(stdout.trim()) });
J
Joao Moreno 已提交
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
				});
			}

			if (path !== '/usr/bin/git') {
				return getVersion(path);
			}

			// must check if XCode is installed
			cp.exec('xcode-select -p', (err: any) => {
				if (err && err.code === 2) {
					// git is not installed, and launching /usr/bin/git
					// will prompt the user to install it

					return e('git not found');
				}

				getVersion(path);
			});
		});
	});
}

J
Joao Moreno 已提交
106
function findSystemGitWin32(base: string, onLookup: (path: string) => void): Promise<IGit> {
J
Joao Moreno 已提交
107 108 109 110
	if (!base) {
		return Promise.reject<IGit>('Not found');
	}

J
Joao Moreno 已提交
111
	return findSpecificGit(path.join(base, 'Git', 'cmd', 'git.exe'), onLookup);
J
Joao Moreno 已提交
112 113
}

114 115 116 117 118
function findGitWin32InPath(onLookup: (path: string) => void): Promise<IGit> {
	const whichPromise = new Promise<string>((c, e) => which('git.exe', (err, path) => err ? e(err) : c(path)));
	return whichPromise.then(path => findSpecificGit(path, onLookup));
}

J
Joao Moreno 已提交
119 120
function findGitWin32(onLookup: (path: string) => void): Promise<IGit> {
	return findSystemGitWin32(process.env['ProgramW6432'] as string, onLookup)
R
Rob Lourens 已提交
121 122 123 124
		.then(undefined, () => findSystemGitWin32(process.env['ProgramFiles(x86)'] as string, onLookup))
		.then(undefined, () => findSystemGitWin32(process.env['ProgramFiles'] as string, onLookup))
		.then(undefined, () => findSystemGitWin32(path.join(process.env['LocalAppData'] as string, 'Programs'), onLookup))
		.then(undefined, () => findGitWin32InPath(onLookup));
J
Joao Moreno 已提交
125 126
}

J
Joao Moreno 已提交
127
export function findGit(hint: string | undefined, onLookup: (path: string) => void): Promise<IGit> {
J
Joao Moreno 已提交
128
	const first = hint ? findSpecificGit(hint, onLookup) : Promise.reject<IGit>(null);
J
Joao Moreno 已提交
129

J
Joao Moreno 已提交
130
	return first
R
Rob Lourens 已提交
131
		.then(undefined, () => {
J
Joao Moreno 已提交
132
			switch (process.platform) {
J
Joao Moreno 已提交
133 134 135
				case 'darwin': return findGitDarwin(onLookup);
				case 'win32': return findGitWin32(onLookup);
				default: return findSpecificGit('git', onLookup);
J
Joao Moreno 已提交
136 137 138
			}
		})
		.then(null, () => Promise.reject(new Error('Git installation not found.')));
J
Joao Moreno 已提交
139 140
}

J
Joao Moreno 已提交
141
export interface IExecutionResult<T extends string | Buffer> {
J
Joao Moreno 已提交
142
	exitCode: number;
J
Joao Moreno 已提交
143
	stdout: T;
J
Joao Moreno 已提交
144 145 146
	stderr: string;
}

J
Joao Moreno 已提交
147 148 149 150 151 152 153 154 155 156 157 158 159 160
function cpErrorHandler(cb: (reason?: any) => void): (reason?: any) => void {
	return err => {
		if (/ENOENT/.test(err.message)) {
			err = new GitError({
				error: err,
				message: 'Failed to execute git (ENOENT)',
				gitErrorCode: GitErrorCodes.NotAGitRepository
			});
		}

		cb(err);
	};
}

J
Joao Moreno 已提交
161
export interface SpawnOptions extends cp.SpawnOptions {
J
Joao Moreno 已提交
162 163 164
	input?: string;
	encoding?: string;
	log?: boolean;
J
Joao Moreno 已提交
165
	cancellationToken?: CancellationToken;
J
Joao Moreno 已提交
166
	onSpawn?: (childProcess: cp.ChildProcess) => void;
J
Joao Moreno 已提交
167 168
}

J
Joao Moreno 已提交
169
async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToken): Promise<IExecutionResult<Buffer>> {
J
Joao Moreno 已提交
170
	if (!child.stdout || !child.stderr) {
J
Joao Moreno 已提交
171 172 173 174 175
		throw new GitError({ message: 'Failed to get stdout or stderr from git process.' });
	}

	if (cancellationToken && cancellationToken.isCancellationRequested) {
		throw new GitError({ message: 'Cancelled' });
J
Joao Moreno 已提交
176 177
	}

J
Joao Moreno 已提交
178 179
	const disposables: IDisposable[] = [];

M
Matt Bierner 已提交
180
	const once = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void) => {
J
Joao Moreno 已提交
181 182 183 184
		ee.once(name, fn);
		disposables.push(toDisposable(() => ee.removeListener(name, fn)));
	};

M
Matt Bierner 已提交
185
	const on = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void) => {
J
Joao Moreno 已提交
186 187 188 189
		ee.on(name, fn);
		disposables.push(toDisposable(() => ee.removeListener(name, fn)));
	};

J
Joao Moreno 已提交
190
	let result = Promise.all<any>([
J
Joao Moreno 已提交
191
		new Promise<number>((c, e) => {
J
Joao Moreno 已提交
192
			once(child, 'error', cpErrorHandler(e));
J
Joao Moreno 已提交
193 194
			once(child, 'exit', c);
		}),
J
Joao Moreno 已提交
195
		new Promise<Buffer>(c => {
196
			const buffers: Buffer[] = [];
197 198
			on(child.stdout!, 'data', (b: Buffer) => buffers.push(b));
			once(child.stdout!, 'close', () => c(Buffer.concat(buffers)));
J
Joao Moreno 已提交
199 200
		}),
		new Promise<string>(c => {
201
			const buffers: Buffer[] = [];
202 203
			on(child.stderr!, 'data', (b: Buffer) => buffers.push(b));
			once(child.stderr!, 'close', () => c(Buffer.concat(buffers).toString('utf8')));
J
Joao Moreno 已提交
204
		})
J
Joao Moreno 已提交
205 206 207 208 209 210 211 212 213 214
	]) as Promise<[number, Buffer, string]>;

	if (cancellationToken) {
		const cancellationPromise = new Promise<[number, Buffer, string]>((_, e) => {
			onceEvent(cancellationToken.onCancellationRequested)(() => {
				try {
					child.kill();
				} catch (err) {
					// noop
				}
J
Joao Moreno 已提交
215

J
Joao Moreno 已提交
216 217 218 219 220 221
				e(new GitError({ message: 'Cancelled' }));
			});
		});

		result = Promise.race([result, cancellationPromise]);
	}
J
Joao Moreno 已提交
222

J
Joao Moreno 已提交
223 224 225 226 227 228
	try {
		const [exitCode, stdout, stderr] = await result;
		return { exitCode, stdout, stderr };
	} finally {
		dispose(disposables);
	}
J
Joao Moreno 已提交
229 230 231 232 233 234 235 236 237 238 239 240 241 242
}

export interface IGitErrorData {
	error?: Error;
	message?: string;
	stdout?: string;
	stderr?: string;
	exitCode?: number;
	gitErrorCode?: string;
	gitCommand?: string;
}

export class GitError {

J
Joao Moreno 已提交
243
	error?: Error;
J
Joao Moreno 已提交
244
	message: string;
J
Joao Moreno 已提交
245 246 247 248 249
	stdout?: string;
	stderr?: string;
	exitCode?: number;
	gitErrorCode?: string;
	gitCommand?: string;
J
Joao Moreno 已提交
250 251 252 253 254 255

	constructor(data: IGitErrorData) {
		if (data.error) {
			this.error = data.error;
			this.message = data.error.message;
		} else {
R
Rob Lourens 已提交
256
			this.error = undefined;
M
Matt Bierner 已提交
257
			this.message = '';
J
Joao Moreno 已提交
258 259 260
		}

		this.message = this.message || data.message || 'Git error';
J
Joao Moreno 已提交
261 262 263 264 265
		this.stdout = data.stdout;
		this.stderr = data.stderr;
		this.exitCode = data.exitCode;
		this.gitErrorCode = data.gitErrorCode;
		this.gitCommand = data.gitCommand;
J
Joao Moreno 已提交
266 267 268 269 270 271 272 273 274
	}

	toString(): string {
		let result = this.message + ' ' + JSON.stringify({
			exitCode: this.exitCode,
			gitErrorCode: this.gitErrorCode,
			gitCommand: this.gitCommand,
			stdout: this.stdout,
			stderr: this.stderr
275
		}, null, 2);
J
Joao Moreno 已提交
276 277 278 279 280 281 282 283 284 285 286 287

		if (this.error) {
			result += (<any>this.error).stack;
		}

		return result;
	}
}

export interface IGitOptions {
	gitPath: string;
	version: string;
288
	env?: any;
J
Joao Moreno 已提交
289 290
}

291 292 293 294 295
function getGitErrorCode(stderr: string): string | undefined {
	if (/Another git process seems to be running in this repository|If no other git process is currently running/.test(stderr)) {
		return GitErrorCodes.RepositoryIsLocked;
	} else if (/Authentication failed/.test(stderr)) {
		return GitErrorCodes.AuthenticationFailed;
J
Joao Moreno 已提交
296
	} else if (/Not a git repository/i.test(stderr)) {
297 298 299 300 301 302 303 304 305
		return GitErrorCodes.NotAGitRepository;
	} else if (/bad config file/.test(stderr)) {
		return GitErrorCodes.BadConfigFile;
	} else if (/cannot make pipe for command substitution|cannot create standard input pipe/.test(stderr)) {
		return GitErrorCodes.CantCreatePipe;
	} else if (/Repository not found/.test(stderr)) {
		return GitErrorCodes.RepositoryNotFound;
	} else if (/unable to access/.test(stderr)) {
		return GitErrorCodes.CantAccessRemote;
306 307
	} else if (/branch '.+' is not fully merged/.test(stderr)) {
		return GitErrorCodes.BranchNotFullyMerged;
308 309
	} else if (/Couldn\'t find remote ref/.test(stderr)) {
		return GitErrorCodes.NoRemoteReference;
310 311 312 313
	} else if (/A branch named '.+' already exists/.test(stderr)) {
		return GitErrorCodes.BranchAlreadyExists;
	} else if (/'.+' is not a valid branch name/.test(stderr)) {
		return GitErrorCodes.InvalidBranchName;
314 315
	} else if (/Please,? commit your changes or stash them/.test(stderr)) {
		return GitErrorCodes.DirtyWorkTree;
316 317
	}

R
Rob Lourens 已提交
318
	return undefined;
319 320
}

321 322
const COMMIT_FORMAT = '%H\n%ae\n%P\n%B';

J
Joao Moreno 已提交
323 324
export class Git {

J
Joao Moreno 已提交
325
	readonly path: string;
326
	private env: any;
J
Joao Moreno 已提交
327

328 329
	private _onOutput = new EventEmitter();
	get onOutput(): EventEmitter { return this._onOutput; }
J
Joao Moreno 已提交
330

J
Joao Moreno 已提交
331
	constructor(options: IGitOptions) {
J
Joao Moreno 已提交
332
		this.path = options.gitPath;
333
		this.env = options.env || {};
J
Joao Moreno 已提交
334 335
	}

J
Joao Moreno 已提交
336 337
	open(repository: string, dotGit: string): Repository {
		return new Repository(this, repository, dotGit);
J
Joao Moreno 已提交
338 339
	}

J
Joao Moreno 已提交
340 341 342 343 344
	async init(repository: string): Promise<void> {
		await this.exec(repository, ['init']);
		return;
	}

J
Joao Moreno 已提交
345
	async clone(url: string, parentPath: string, progress: Progress<{ increment: number }>, cancellationToken?: CancellationToken): Promise<string> {
346
		let baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository';
347 348 349 350
		let folderName = baseFolderName;
		let folderPath = path.join(parentPath, folderName);
		let count = 1;

J
Joao Moreno 已提交
351
		while (count < 20 && await new Promise(c => exists(folderPath, c))) {
352 353 354
			folderName = `${baseFolderName}-${count++}`;
			folderPath = path.join(parentPath, folderName);
		}
J
Joao Moreno 已提交
355

J
Joao Moreno 已提交
356
		await mkdirp(parentPath);
J
Joao Moreno 已提交
357

J
Joao Moreno 已提交
358 359 360
		const onSpawn = (child: cp.ChildProcess) => {
			const decoder = new StringDecoder('utf8');
			const lineStream = new byline.LineStream({ encoding: 'utf8' });
361
			child.stderr!.on('data', (buffer: Buffer) => lineStream.write(decoder.write(buffer)));
J
Joao Moreno 已提交
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385

			let totalProgress = 0;
			let previousProgress = 0;

			lineStream.on('data', (line: string) => {
				let match: RegExpMatchArray | null = null;

				if (match = /Counting objects:\s*(\d+)%/i.exec(line)) {
					totalProgress = Math.floor(parseInt(match[1]) * 0.1);
				} else if (match = /Compressing objects:\s*(\d+)%/i.exec(line)) {
					totalProgress = 10 + Math.floor(parseInt(match[1]) * 0.1);
				} else if (match = /Receiving objects:\s*(\d+)%/i.exec(line)) {
					totalProgress = 20 + Math.floor(parseInt(match[1]) * 0.4);
				} else if (match = /Resolving deltas:\s*(\d+)%/i.exec(line)) {
					totalProgress = 60 + Math.floor(parseInt(match[1]) * 0.4);
				}

				if (totalProgress !== previousProgress) {
					progress.report({ increment: totalProgress - previousProgress });
					previousProgress = totalProgress;
				}
			});
		};

J
Joao Moreno 已提交
386
		try {
J
Joao Moreno 已提交
387
			await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'], { cancellationToken, onSpawn });
J
Joao Moreno 已提交
388 389 390 391 392 393 394 395 396
		} catch (err) {
			if (err.stderr) {
				err.stderr = err.stderr.replace(/^Cloning.+$/m, '').trim();
				err.stderr = err.stderr.replace(/^ERROR:\s+/, '').trim();
			}

			throw err;
		}

J
Joao Moreno 已提交
397 398 399
		return folderPath;
	}

J
Joao Moreno 已提交
400 401
	async getRepositoryRoot(repositoryPath: string): Promise<string> {
		const result = await this.exec(repositoryPath, ['rev-parse', '--show-toplevel']);
J
jeanp413 已提交
402 403
		// Keep trailing spaces which are part of the directory name
		return path.normalize(result.stdout.trimLeft().replace(/(\r\n|\r|\n)+$/, ''));
J
Joao Moreno 已提交
404 405
	}

J
Joao Moreno 已提交
406
	async getRepositoryDotGit(repositoryPath: string): Promise<string> {
407 408 409 410 411 412 413 414
		const result = await this.exec(repositoryPath, ['rev-parse', '--git-dir']);
		let dotGitPath = result.stdout.trim();

		if (!path.isAbsolute(dotGitPath)) {
			dotGitPath = path.join(repositoryPath, dotGitPath);
		}

		return path.normalize(dotGitPath);
J
Joao Moreno 已提交
415 416
	}

J
Joao Moreno 已提交
417
	async exec(cwd: string, args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
J
Joao Moreno 已提交
418
		options = assign({ cwd }, options || {});
J
Joao Moreno 已提交
419
		return await this._exec(args, options);
J
Joao Moreno 已提交
420 421
	}

J
Joao Moreno 已提交
422 423 424 425
	async exec2(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
		return await this._exec(args, options);
	}

J
Joao Moreno 已提交
426
	stream(cwd: string, args: string[], options: SpawnOptions = {}): cp.ChildProcess {
J
Joao Moreno 已提交
427
		options = assign({ cwd }, options || {});
J
Joao Moreno 已提交
428
		return this.spawn(args, options);
J
Joao Moreno 已提交
429 430
	}

J
Joao Moreno 已提交
431
	private async _exec(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
J
Joao Moreno 已提交
432
		const child = this.spawn(args, options);
J
Joao Moreno 已提交
433

J
Joao Moreno 已提交
434 435 436 437
		if (options.onSpawn) {
			options.onSpawn(child);
		}

J
Joao Moreno 已提交
438
		if (options.input) {
439
			child.stdin!.end(options.input, 'utf8');
J
Joao Moreno 已提交
440 441
		}

J
Joao Moreno 已提交
442
		const bufferResult = await exec(child, options.cancellationToken);
J
Joao Moreno 已提交
443

J
Joao Moreno 已提交
444 445
		if (options.log !== false && bufferResult.stderr.length > 0) {
			this.log(`${bufferResult.stderr}\n`);
J
Joao Moreno 已提交
446 447
		}

J
Joao Moreno 已提交
448 449 450 451 452 453 454 455 456 457 458
		let encoding = options.encoding || 'utf8';
		encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';

		const result: IExecutionResult<string> = {
			exitCode: bufferResult.exitCode,
			stdout: iconv.decode(bufferResult.stdout, encoding),
			stderr: bufferResult.stderr
		};

		if (bufferResult.exitCode) {
			return Promise.reject<IExecutionResult<string>>(new GitError({
J
Joao Moreno 已提交
459 460 461 462
				message: 'Failed to execute git',
				stdout: result.stdout,
				stderr: result.stderr,
				exitCode: result.exitCode,
463
				gitErrorCode: getGitErrorCode(result.stderr),
J
Joao Moreno 已提交
464 465 466 467 468
				gitCommand: args[0]
			}));
		}

		return result;
J
Joao Moreno 已提交
469 470
	}

J
Joao Moreno 已提交
471
	spawn(args: string[], options: SpawnOptions = {}): cp.ChildProcess {
J
Joao Moreno 已提交
472
		if (!this.path) {
J
Joao Moreno 已提交
473 474 475 476 477 478 479 480 481 482 483
			throw new Error('git could not be found in the system.');
		}

		if (!options) {
			options = {};
		}

		if (!options.stdio && !options.input) {
			options.stdio = ['ignore', null, null]; // Unless provided, ignore stdin and leave default streams for stdout and stderr
		}

484
		options.env = assign({}, process.env, this.env, options.env || {}, {
J
Joao Moreno 已提交
485
			VSCODE_GIT_COMMAND: args[0],
486
			LC_ALL: 'en_US.UTF-8',
J
Joao Moreno 已提交
487 488
			LANG: 'en_US.UTF-8'
		});
J
Joao Moreno 已提交
489 490

		if (options.log !== false) {
J
Joao Moreno 已提交
491
			this.log(`> git ${args.join(' ')}\n`);
J
Joao Moreno 已提交
492 493
		}

J
Joao Moreno 已提交
494
		return cp.spawn(this.path, args, options);
J
Joao Moreno 已提交
495 496 497
	}

	private log(output: string): void {
498
		this._onOutput.emit('log', output);
J
Joao Moreno 已提交
499
	}
J
Joao Moreno 已提交
500 501
}

J
Joao Moreno 已提交
502
export interface Commit {
J
Joao Moreno 已提交
503 504
	hash: string;
	message: string;
J
Joao Moreno 已提交
505
	parents: string[];
506
	authorEmail?: string | undefined;
J
Joao Moreno 已提交
507 508
}

509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
export class GitStatusParser {

	private lastRaw = '';
	private result: IFileStatus[] = [];

	get status(): IFileStatus[] {
		return this.result;
	}

	update(raw: string): void {
		let i = 0;
		let nextI: number | undefined;

		raw = this.lastRaw + raw;

		while ((nextI = this.parseEntry(raw, i)) !== undefined) {
			i = nextI;
		}

		this.lastRaw = raw.substr(i);
	}

	private parseEntry(raw: string, i: number): number | undefined {
		if (i + 4 >= raw.length) {
			return;
		}

		let lastIndex: number;
		const entry: IFileStatus = {
			x: raw.charAt(i++),
			y: raw.charAt(i++),
			rename: undefined,
			path: ''
		};

		// space
		i++;

J
Joao Moreno 已提交
547
		if (entry.x === 'R' || entry.x === 'C') {
548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
			lastIndex = raw.indexOf('\0', i);

			if (lastIndex === -1) {
				return;
			}

			entry.rename = raw.substring(i, lastIndex);
			i = lastIndex + 1;
		}

		lastIndex = raw.indexOf('\0', i);

		if (lastIndex === -1) {
			return;
		}

		entry.path = raw.substring(i, lastIndex);

		// If path ends with slash, it must be a nested git repo
		if (entry.path[entry.path.length - 1] !== '/') {
			this.result.push(entry);
		}

		return lastIndex + 1;
	}
}

575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608
export interface Submodule {
	name: string;
	path: string;
	url: string;
}

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;
			}
		}

		if (!submodule) {
			return;
		}

J
Joao Moreno 已提交
609
		const propertyMatch = /^\s*(\w+)\s+=\s+(.*)$/.exec(line);
610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636

		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;
}

637
export function parseGitCommit(raw: string): Commit | null {
J
Joao Moreno 已提交
638
	const match = /^([0-9a-f]{40})\n(.*)\n(.*)(\n([^]*))?$/m.exec(raw.trim());
639 640 641 642
	if (!match) {
		return null;
	}

643
	const parents = match[3] ? match[3].split(' ') : [];
J
Joao Moreno 已提交
644
	return { hash: match[1], message: match[5], parents, authorEmail: match[2] };
645 646
}

647 648 649 650
interface LsTreeElement {
	mode: string;
	type: string;
	object: string;
J
Joao Moreno 已提交
651
	size: string;
652 653 654 655 656 657
	file: string;
}

export function parseLsTree(raw: string): LsTreeElement[] {
	return raw.split('\n')
		.filter(l => !!l)
J
Joao Moreno 已提交
658
		.map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!)
659
		.filter(m => !!m)
J
Joao Moreno 已提交
660
		.map(([, mode, type, object, size, file]) => ({ mode, type, object, size, file }));
661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677
}

interface LsFilesElement {
	mode: string;
	object: string;
	stage: string;
	file: string;
}

export function parseLsFiles(raw: string): LsFilesElement[] {
	return raw.split('\n')
		.filter(l => !!l)
		.map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!)
		.filter(m => !!m)
		.map(([, mode, object, stage, file]) => ({ mode, object, stage, file }));
}

J
Joao Moreno 已提交
678
export interface CommitOptions {
J
Joao Moreno 已提交
679
	all?: boolean | 'tracked';
J
Joao Moreno 已提交
680 681 682 683 684 685
	amend?: boolean;
	signoff?: boolean;
	signCommit?: boolean;
	empty?: boolean;
}

686 687
export interface PullOptions {
	unshallow?: boolean;
J
Joao Moreno 已提交
688
	tags?: boolean;
689
	readonly cancellationToken?: CancellationToken;
690 691
}

J
Joao Moreno 已提交
692 693
export enum ForcePushMode {
	Force,
J
Joao Moreno 已提交
694
	ForceWithLease
J
Joao Moreno 已提交
695 696
}

J
Joao Moreno 已提交
697 698 699 700
export class Repository {

	constructor(
		private _git: Git,
J
Joao Moreno 已提交
701 702
		private repositoryRoot: string,
		readonly dotGit: string
J
Joao Moreno 已提交
703 704 705 706 707 708
	) { }

	get git(): Git {
		return this._git;
	}

J
Joao Moreno 已提交
709 710
	get root(): string {
		return this.repositoryRoot;
J
Joao Moreno 已提交
711 712 713
	}

	// TODO@Joao: rename to exec
J
Joao Moreno 已提交
714
	async run(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
J
Joao Moreno 已提交
715
		return await this.git.exec(this.repositoryRoot, args, options);
J
Joao Moreno 已提交
716 717
	}

J
Joao Moreno 已提交
718
	stream(args: string[], options: SpawnOptions = {}): cp.ChildProcess {
J
Joao Moreno 已提交
719
		return this.git.stream(this.repositoryRoot, args, options);
J
Joao Moreno 已提交
720 721
	}

J
Joao Moreno 已提交
722
	spawn(args: string[], options: SpawnOptions = {}): cp.ChildProcess {
J
Joao Moreno 已提交
723 724 725
		return this.git.spawn(args, options);
	}

J
Joao Moreno 已提交
726
	async config(scope: string, key: string, value: any = null, options: SpawnOptions = {}): Promise<string> {
J
Joao Moreno 已提交
727 728 729 730 731 732 733 734 735 736 737 738 739
		const args = ['config'];

		if (scope) {
			args.push('--' + scope);
		}

		args.push(key);

		if (value) {
			args.push(value);
		}

		const result = await this.run(args, options);
J
Joao Moreno 已提交
740
		return result.stdout.trim();
J
Joao Moreno 已提交
741 742
	}

J
Joao Moreno 已提交
743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760
	async getConfigs(scope: string): Promise<{ key: string; value: string; }[]> {
		const args = ['config'];

		if (scope) {
			args.push('--' + scope);
		}

		args.push('-l');

		const result = await this.run(args);
		const lines = result.stdout.trim().split(/\r|\r\n|\n/);

		return lines.map(entry => {
			const equalsIndex = entry.indexOf('=');
			return { key: entry.substr(0, equalsIndex), value: entry.substr(equalsIndex + 1) };
		});
	}

761 762 763
	async log(options?: LogOptions): Promise<Commit[]> {
		const maxEntries = options && typeof options.maxEntries === 'number' && options.maxEntries > 0 ? options.maxEntries : 32;
		const args = ['log', '-' + maxEntries, `--pretty=format:${COMMIT_FORMAT}%x00%x00`];
764

J
Joao Moreno 已提交
765 766
		const gitResult = await this.run(args);
		if (gitResult.exitCode) {
767
			// An empty repo
768 769 770
			return [];
		}

771
		const s = gitResult.stdout;
772
		const result: Commit[] = [];
773 774 775 776 777 778 779 780
		let index = 0;
		while (index < s.length) {
			let nextIndex = s.indexOf('\x00\x00', index);
			if (nextIndex === -1) {
				nextIndex = s.length;
			}

			let entry = s.substr(index, nextIndex - index);
781 782 783
			if (entry.startsWith('\n')) {
				entry = entry.substring(1);
			}
784

785 786 787 788 789 790
			const commit = parseGitCommit(entry);
			if (!commit) {
				break;
			}

			result.push(commit);
791
			index = nextIndex + 2;
792 793 794 795 796
		}

		return result;
	}

797
	async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise<string> {
J
Joao Moreno 已提交
798
		const stdout = await this.buffer(object);
799 800 801 802 803

		if (autoGuessEncoding) {
			encoding = detectEncoding(stdout) || encoding;
		}

J
Joao Moreno 已提交
804 805 806
		encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';

		return iconv.decode(stdout, encoding);
J
Joao Moreno 已提交
807 808 809
	}

	async buffer(object: string): Promise<Buffer> {
J
Joao Moreno 已提交
810 811 812
		const child = this.stream(['show', object]);

		if (!child.stdout) {
J
Joao Moreno 已提交
813
			return Promise.reject<Buffer>('Can\'t open file from git');
J
Joao Moreno 已提交
814 815
		}

816
		const { exitCode, stdout, stderr } = await exec(child);
J
Joao Moreno 已提交
817 818

		if (exitCode) {
819
			const err = new GitError({
J
Joao Moreno 已提交
820 821
				message: 'Could not show object.',
				exitCode
822 823 824 825 826 827 828
			});

			if (/exists on disk, but not in/.test(stderr)) {
				err.gitErrorCode = GitErrorCodes.WrongCase;
			}

			return Promise.reject<Buffer>(err);
J
Joao Moreno 已提交
829 830 831
		}

		return stdout;
J
Joao Moreno 已提交
832 833
	}

J
Joao Moreno 已提交
834
	async getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }> {
J
Joao Moreno 已提交
835
		if (!treeish) { // index
J
Joao Moreno 已提交
836
			const elements = await this.lsfiles(path);
J
Joao Moreno 已提交
837

J
Joao Moreno 已提交
838
			if (elements.length === 0) {
J
Joao Moreno 已提交
839
				throw new GitError({ message: 'Path not known by git', gitErrorCode: GitErrorCodes.UnknownPath });
J
Joao Moreno 已提交
840 841
			}

J
Joao Moreno 已提交
842
			const { mode, object } = elements[0];
J
Joao Moreno 已提交
843 844 845
			const catFile = await this.run(['cat-file', '-s', object]);
			const size = parseInt(catFile.stdout);

J
Joao Moreno 已提交
846
			return { mode, object, size };
J
Joao Moreno 已提交
847 848
		}

J
Joao Moreno 已提交
849
		const elements = await this.lstree(treeish, path);
J
Joao Moreno 已提交
850

J
Joao Moreno 已提交
851
		if (elements.length === 0) {
J
Joao Moreno 已提交
852
			throw new GitError({ message: 'Path not known by git', gitErrorCode: GitErrorCodes.UnknownPath });
J
Joao Moreno 已提交
853 854
		}

J
Joao Moreno 已提交
855
		const { mode, object, size } = elements[0];
J
Joao Moreno 已提交
856
		return { mode, object, size: parseInt(size) };
J
Joao Moreno 已提交
857 858
	}

J
Joao Moreno 已提交
859 860
	async lstree(treeish: string, path: string): Promise<LsTreeElement[]> {
		const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', path]);
861 862
		return parseLsTree(stdout);
	}
863

864 865 866
	async lsfiles(path: string): Promise<LsFilesElement[]> {
		const { stdout } = await this.run(['ls-files', '--stage', '--', path]);
		return parseLsFiles(stdout);
867 868
	}

J
Joao Moreno 已提交
869
	async getGitRelativePath(ref: string, relativePath: string): Promise<string> {
870 871
		const relativePathLowercase = relativePath.toLowerCase();
		const dirname = path.posix.dirname(relativePath) + '/';
J
Joao Moreno 已提交
872
		const elements: { file: string; }[] = ref ? await this.lstree(ref, dirname) : await this.lsfiles(dirname);
873 874 875 876
		const element = elements.filter(file => file.file.toLowerCase() === relativePathLowercase)[0];

		if (!element) {
			throw new GitError({ message: 'Git relative path not found.' });
877
		}
878 879

		return element.file;
880 881
	}

J
Joao Moreno 已提交
882 883
	async detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> {
		const child = await this.stream(['show', object]);
884
		const buffer = await readBytes(child.stdout!, 4100);
J
Joao Moreno 已提交
885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921

		try {
			child.kill();
		} catch (err) {
			// noop
		}

		const encoding = detectUnicodeEncoding(buffer);
		let isText = true;

		if (encoding !== Encoding.UTF16be && encoding !== Encoding.UTF16le) {
			for (let i = 0; i < buffer.length; i++) {
				if (buffer.readInt8(i) === 0) {
					isText = false;
					break;
				}
			}
		}

		if (!isText) {
			const result = filetype(buffer);

			if (!result) {
				return { mimetype: 'application/octet-stream' };
			} else {
				return { mimetype: result.mime };
			}
		}

		if (encoding) {
			return { mimetype: 'text/plain', encoding };
		} else {
			// TODO@JOAO: read the setting OUTSIDE!
			return { mimetype: 'text/plain' };
		}
	}

922 923 924 925 926 927 928
	async apply(patch: string, reverse?: boolean): Promise<void> {
		const args = ['apply', patch];

		if (reverse) {
			args.push('-R');
		}

929 930 931 932 933 934 935 936 937
		try {
			await this.run(args);
		} catch (err) {
			if (/patch does not apply/.test(err.stderr)) {
				err.gitErrorCode = GitErrorCodes.PatchDoesNotApply;
			}

			throw err;
		}
938 939 940
	}

	async diff(cached = false): Promise<string> {
941 942
		const args = ['diff'];

J
Joao Moreno 已提交
943
		if (cached) {
944 945 946 947 948 949 950
			args.push('--cached');
		}

		const result = await this.run(args);
		return result.stdout;
	}

951 952 953 954
	diffWithHEAD(): Promise<Change[]>;
	diffWithHEAD(path: string): Promise<string>;
	diffWithHEAD(path?: string | undefined): Promise<string | Change[]>;
	async diffWithHEAD(path?: string | undefined): Promise<string | Change[]> {
955 956 957 958
		if (!path) {
			return await this.diffFiles(false);
		}

J
Joao Moreno 已提交
959 960 961 962 963
		const args = ['diff', '--', path];
		const result = await this.run(args);
		return result.stdout;
	}

964 965 966
	diffWith(ref: string): Promise<Change[]>;
	diffWith(ref: string, path: string): Promise<string>;
	diffWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
967 968 969 970 971
	async diffWith(ref: string, path?: string): Promise<string | Change[]> {
		if (!path) {
			return await this.diffFiles(false, ref);
		}

J
Joao Moreno 已提交
972 973 974 975 976
		const args = ['diff', ref, '--', path];
		const result = await this.run(args);
		return result.stdout;
	}

977 978 979
	diffIndexWithHEAD(): Promise<Change[]>;
	diffIndexWithHEAD(path: string): Promise<string>;
	diffIndexWithHEAD(path?: string | undefined): Promise<string | Change[]>;
980 981 982 983 984
	async diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
		if (!path) {
			return await this.diffFiles(true);
		}

J
Joao Moreno 已提交
985 986 987 988 989
		const args = ['diff', '--cached', '--', path];
		const result = await this.run(args);
		return result.stdout;
	}

990 991 992
	diffIndexWith(ref: string): Promise<Change[]>;
	diffIndexWith(ref: string, path: string): Promise<string>;
	diffIndexWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
993 994 995 996 997
	async diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
		if (!path) {
			return await this.diffFiles(true, ref);
		}

J
Joao Moreno 已提交
998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
		const args = ['diff', '--cached', ref, '--', path];
		const result = await this.run(args);
		return result.stdout;
	}

	async diffBlobs(object1: string, object2: string): Promise<string> {
		const args = ['diff', object1, object2];
		const result = await this.run(args);
		return result.stdout;
	}

1009 1010 1011
	diffBetween(ref1: string, ref2: string): Promise<Change[]>;
	diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
	diffBetween(ref1: string, ref2: string, path?: string | undefined): Promise<string | Change[]>;
1012 1013 1014 1015 1016 1017 1018
	async diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
		const range = `${ref1}...${ref2}`;
		if (!path) {
			return await this.diffFiles(false, range);
		}

		const args = ['diff', range, '--', path];
J
Joao Moreno 已提交
1019 1020 1021 1022 1023
		const result = await this.run(args);

		return result.stdout.trim();
	}

1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050
	private async diffFiles(cached: boolean, ref?: string): Promise<Change[]> {
		const args = ['diff', '--name-status', '-z', '--diff-filter=ADMR'];
		if (cached) {
			args.push('--cached');
		}

		if (ref) {
			args.push(ref);
		}

		const gitResult = await this.run(args);
		if (gitResult.exitCode) {
			return [];
		}

		const entries = gitResult.stdout.split('\x00');
		let index = 0;
		const result: Change[] = [];

		entriesLoop:
		while (index < entries.length - 1) {
			const change = entries[index++];
			const resourcePath = entries[index++];
			if (!change || !resourcePath) {
				break;
			}

J
Joao Moreno 已提交
1051
			const originalUri = URI.file(path.isAbsolute(resourcePath) ? resourcePath : path.join(this.repositoryRoot, resourcePath));
1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078
			let status: Status = Status.UNTRACKED;

			// 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.
			switch (change[0]) {
				case 'M':
					status = Status.MODIFIED;
					break;

				case 'A':
					status = Status.INDEX_ADDED;
					break;

				case 'D':
					status = Status.DELETED;
					break;

				// Rename contains two paths, the second one is what the file is renamed/copied to.
				case 'R':
					if (index >= entries.length) {
						break;
					}

					const newPath = entries[index++];
					if (!newPath) {
						break;
					}

J
Joao Moreno 已提交
1079
					const uri = URI.file(path.isAbsolute(newPath) ? newPath : path.join(this.repositoryRoot, newPath));
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104
					result.push({
						uri,
						renameUri: uri,
						originalUri,
						status: Status.INDEX_RENAMED
					});

					continue;

				default:
					// Unknown status
					break entriesLoop;
			}

			result.push({
				status,
				originalUri,
				uri: originalUri,
				renameUri: originalUri,
			});
		}

		return result;
	}

J
Joao Moreno 已提交
1105 1106 1107 1108 1109 1110 1111
	async getMergeBase(ref1: string, ref2: string): Promise<string> {
		const args = ['merge-base', ref1, ref2];
		const result = await this.run(args);

		return result.stdout.trim();
	}

J
Joao Moreno 已提交
1112 1113 1114 1115 1116 1117 1118
	async hashObject(data: string): Promise<string> {
		const args = ['hash-object', '-w', '--stdin'];
		const result = await this.run(args, { input: data });

		return result.stdout.trim();
	}

J
Joao Moreno 已提交
1119 1120 1121 1122 1123 1124 1125 1126 1127 1128
	async add(paths: string[], opts?: { update?: boolean }): Promise<void> {
		const args = ['add'];

		if (opts && opts.update) {
			args.push('-u');
		} else {
			args.push('-A');
		}

		args.push('--');
J
Joao Moreno 已提交
1129 1130 1131 1132 1133 1134 1135 1136 1137 1138

		if (paths && paths.length) {
			args.push.apply(args, paths);
		} else {
			args.push('.');
		}

		await this.run(args);
	}

J
Joao Moreno 已提交
1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150
	async rm(paths: string[]): Promise<void> {
		const args = ['rm', '--'];

		if (!paths || !paths.length) {
			return;
		}

		args.push(...paths);

		await this.run(args);
	}

J
Joao Moreno 已提交
1151
	async stage(path: string, data: string): Promise<void> {
1152
		const child = this.stream(['hash-object', '--stdin', '-w', '--path', path], { stdio: [null, null, null] });
1153
		child.stdin!.end(data, 'utf8');
J
Joao Moreno 已提交
1154 1155

		const { exitCode, stdout } = await exec(child);
J
Joao Moreno 已提交
1156
		const hash = stdout.toString('utf8');
J
Joao Moreno 已提交
1157 1158 1159 1160 1161 1162 1163 1164

		if (exitCode) {
			throw new GitError({
				message: 'Could not hash object.',
				exitCode: exitCode
			});
		}

J
Joao Moreno 已提交
1165
		const treeish = await this.getCommit('HEAD').then(() => 'HEAD', () => '');
J
Joao Moreno 已提交
1166
		let mode: string;
D
Darrien Singleton 已提交
1167
		let add: string = '';
J
Joao Moreno 已提交
1168 1169

		try {
1170
			const details = await this.getObjectDetails(treeish, path);
J
Joao Moreno 已提交
1171 1172
			mode = details.mode;
		} catch (err) {
J
Joao Moreno 已提交
1173 1174 1175 1176
			if (err.gitErrorCode !== GitErrorCodes.UnknownPath) {
				throw err;
			}

J
Joao Moreno 已提交
1177
			mode = '100644';
D
Darrien Singleton 已提交
1178
			add = '--add';
J
Joao Moreno 已提交
1179 1180
		}

D
Darrien Singleton 已提交
1181
		await this.run(['update-index', add, '--cacheinfo', mode, hash, path]);
J
Joao Moreno 已提交
1182 1183
	}

1184
	async checkout(treeish: string, paths: string[], opts: { track?: boolean } = Object.create(null)): Promise<void> {
J
Joao Moreno 已提交
1185 1186
		const args = ['checkout', '-q'];

1187 1188 1189 1190
		if (opts.track) {
			args.push('--track');
		}

J
Joao Moreno 已提交
1191 1192 1193 1194 1195
		if (treeish) {
			args.push(treeish);
		}

		try {
J
Joao Moreno 已提交
1196 1197 1198 1199 1200 1201 1202
			if (paths && paths.length > 0) {
				for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
					await this.run([...args, '--', ...chunk]);
				}
			} else {
				await this.run(args);
			}
J
Joao Moreno 已提交
1203
		} catch (err) {
J
Joao Moreno 已提交
1204
			if (/Please,? commit your changes or stash them/.test(err.stderr || '')) {
J
Joao Moreno 已提交
1205 1206 1207 1208 1209 1210 1211
				err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
			}

			throw err;
		}
	}

J
Joao Moreno 已提交
1212
	async commit(message: string, opts: CommitOptions = Object.create(null)): Promise<void> {
J
Joao Moreno 已提交
1213 1214
		const args = ['commit', '--quiet', '--allow-empty-message', '--file', '-'];

J
Joao Moreno 已提交
1215
		if (opts.all) {
J
Joao Moreno 已提交
1216 1217 1218
			args.push('--all');
		}

J
Joao Moreno 已提交
1219
		if (opts.amend) {
J
Joao Moreno 已提交
1220 1221 1222
			args.push('--amend');
		}

J
Joao Moreno 已提交
1223
		if (opts.signoff) {
J
Joao Moreno 已提交
1224 1225 1226
			args.push('--signoff');
		}

1227 1228 1229
		if (opts.signCommit) {
			args.push('-S');
		}
T
Tom Basche 已提交
1230 1231 1232
		if (opts.empty) {
			args.push('--allow-empty');
		}
1233

J
Joao Moreno 已提交
1234 1235 1236
		try {
			await this.run(args, { input: message || '' });
		} catch (commitErr) {
1237 1238 1239
			await this.handleCommitError(commitErr);
		}
	}
J
Joao Moreno 已提交
1240

1241 1242
	async rebaseContinue(): Promise<void> {
		const args = ['rebase', '--continue'];
J
Joao Moreno 已提交
1243

1244 1245 1246 1247 1248 1249
		try {
			await this.run(args);
		} catch (commitErr) {
			await this.handleCommitError(commitErr);
		}
	}
J
Joao Moreno 已提交
1250

1251 1252 1253
	private async handleCommitError(commitErr: any): Promise<void> {
		if (/not possible because you have unmerged files/.test(commitErr.stderr || '')) {
			commitErr.gitErrorCode = GitErrorCodes.UnmergedChanges;
J
Joao Moreno 已提交
1254 1255
			throw commitErr;
		}
1256 1257

		try {
J
Joao Moreno 已提交
1258 1259 1260 1261 1262
			await this.run(['config', '--get-all', 'user.name']);
		} catch (err) {
			err.gitErrorCode = GitErrorCodes.NoUserNameConfigured;
			throw err;
		}
1263 1264

		try {
J
Joao Moreno 已提交
1265 1266 1267 1268
			await this.run(['config', '--get-all', 'user.email']);
		} catch (err) {
			err.gitErrorCode = GitErrorCodes.NoUserEmailConfigured;
			throw err;
1269 1270 1271
		}

		throw commitErr;
J
Joao Moreno 已提交
1272 1273
	}

J
Joao Moreno 已提交
1274
	async branch(name: string, checkout: boolean, ref?: string): Promise<void> {
J
Joao Moreno 已提交
1275
		const args = checkout ? ['checkout', '-q', '-b', name, '--no-track'] : ['branch', '-q', name];
J
Joao Moreno 已提交
1276 1277 1278 1279 1280

		if (ref) {
			args.push(ref);
		}

J
Joao Moreno 已提交
1281 1282 1283
		await this.run(args);
	}

1284 1285
	async deleteBranch(name: string, force?: boolean): Promise<void> {
		const args = ['branch', force ? '-D' : '-d', name];
M
Maik Riechert 已提交
1286 1287 1288
		await this.run(args);
	}

1289 1290 1291 1292 1293
	async renameBranch(name: string): Promise<void> {
		const args = ['branch', '-m', name];
		await this.run(args);
	}

J
Joao Moreno 已提交
1294 1295 1296 1297 1298
	async setBranchUpstream(name: string, upstream: string): Promise<void> {
		const args = ['branch', '--set-upstream-to', upstream, name];
		await this.run(args);
	}

1299 1300 1301 1302 1303
	async deleteRef(ref: string): Promise<void> {
		const args = ['update-ref', '-d', ref];
		await this.run(args);
	}

J
Joao Moreno 已提交
1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315
	async merge(ref: string): Promise<void> {
		const args = ['merge', ref];

		try {
			await this.run(args);
		} catch (err) {
			if (/^CONFLICT /m.test(err.stdout || '')) {
				err.gitErrorCode = GitErrorCodes.Conflict;
			}

			throw err;
		}
1316 1317
	}

J
Joao Moreno 已提交
1318
	async tag(name: string, message?: string): Promise<void> {
1319 1320
		let args = ['tag'];

J
Joao Moreno 已提交
1321 1322
		if (message) {
			args = [...args, '-a', name, '-m', message];
1323
		} else {
J
Joao Moreno 已提交
1324
			args = [...args, name];
1325 1326 1327 1328 1329
		}

		await this.run(args);
	}

X
Xhulio Hasani 已提交
1330 1331 1332 1333 1334
	async deleteTag(name: string): Promise<void> {
		let args = ['tag', '-d', name];
		await this.run(args);
	}

J
Joao Moreno 已提交
1335
	async clean(paths: string[]): Promise<void> {
J
Joao Moreno 已提交
1336 1337
		const pathsByGroup = groupBy(paths, p => path.dirname(p));
		const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]);
J
Joao Moreno 已提交
1338

J
Joao Moreno 已提交
1339 1340 1341 1342 1343 1344 1345
		const limiter = new Limiter(5);
		const promises: Promise<any>[] = [];

		for (const paths of groups) {
			for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
				promises.push(limiter.queue(() => this.run(['clean', '-f', '-q', '--', ...chunk])));
			}
J
Joao Moreno 已提交
1346
		}
J
Joao Moreno 已提交
1347 1348

		await Promise.all(promises);
J
Joao Moreno 已提交
1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365
	}

	async undo(): Promise<void> {
		await this.run(['clean', '-fd']);

		try {
			await this.run(['checkout', '--', '.']);
		} catch (err) {
			if (/did not match any file\(s\) known to git\./.test(err.stderr || '')) {
				return;
			}

			throw err;
		}
	}

	async reset(treeish: string, hard: boolean = false): Promise<void> {
1366
		const args = ['reset', hard ? '--hard' : '--soft', treeish];
J
Joao Moreno 已提交
1367 1368 1369
		await this.run(args);
	}

J
Joao Moreno 已提交
1370
	async revert(treeish: string, paths: string[]): Promise<void> {
J
Joao Moreno 已提交
1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399
		const result = await this.run(['branch']);
		let args: string[];

		// In case there are no branches, we must use rm --cached
		if (!result.stdout) {
			args = ['rm', '--cached', '-r', '--'];
		} else {
			args = ['reset', '-q', treeish, '--'];
		}

		if (paths && paths.length) {
			args.push.apply(args, paths);
		} else {
			args.push('.');
		}

		try {
			await this.run(args);
		} catch (err) {
			// In case there are merge conflicts to be resolved, git reset will output
			// some "needs merge" data. We try to get around that.
			if (/([^:]+: needs merge\n)+/m.test(err.stdout || '')) {
				return;
			}

			throw err;
		}
	}

J
Joao Moreno 已提交
1400 1401 1402 1403 1404
	async addRemote(name: string, url: string): Promise<void> {
		const args = ['remote', 'add', name, url];
		await this.run(args);
	}

J
Joao Moreno 已提交
1405 1406 1407 1408 1409
	async removeRemote(name: string): Promise<void> {
		const args = ['remote', 'rm', name];
		await this.run(args);
	}

J
Joao Moreno 已提交
1410
	async fetch(options: { remote?: string, ref?: string, all?: boolean, prune?: boolean, depth?: number, silent?: boolean } = {}): Promise<void> {
J
Joao Moreno 已提交
1411
		const args = ['fetch'];
J
Joao Moreno 已提交
1412
		const spawnOptions: SpawnOptions = {};
J
Joao Moreno 已提交
1413

J
Joao Moreno 已提交
1414 1415
		if (options.remote) {
			args.push(options.remote);
J
Joao Moreno 已提交
1416

J
Joao Moreno 已提交
1417 1418
			if (options.ref) {
				args.push(options.ref);
J
Joao Moreno 已提交
1419
			}
J
Joao Moreno 已提交
1420 1421
		} else if (options.all) {
			args.push('--all');
J
Joao Moreno 已提交
1422 1423
		}

R
Ryan Scott 已提交
1424
		if (options.prune) {
R
Ryan Scott 已提交
1425
			args.push('--prune');
R
Ryan Scott 已提交
1426 1427
		}

1428
		if (typeof options.depth === 'number') {
1429 1430
			args.push(`--depth=${options.depth}`);
		}
R
Ryan Scott 已提交
1431

J
Joao Moreno 已提交
1432 1433 1434 1435
		if (options.silent) {
			spawnOptions.env = { 'VSCODE_GIT_FETCH_SILENT': 'true' };
		}

J
Joao Moreno 已提交
1436
		try {
J
Joao Moreno 已提交
1437
			await this.run(args, spawnOptions);
J
Joao Moreno 已提交
1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448
		} catch (err) {
			if (/No remote repository specified\./.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.NoRemoteRepositorySpecified;
			} else if (/Could not read from remote repository/.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
			}

			throw err;
		}
	}

1449
	async pull(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> {
H
Hao Hu 已提交
1450 1451
		const args = ['pull'];

J
Joao Moreno 已提交
1452
		if (options.tags) {
H
Hao Hu 已提交
1453 1454
			args.push('--tags');
		}
1455 1456 1457 1458

		if (options.unshallow) {
			args.push('--unshallow');
		}
J
Joao Moreno 已提交
1459 1460 1461 1462 1463

		if (rebase) {
			args.push('-r');
		}

M
Matt Shirley 已提交
1464
		if (remote && branch) {
1465
			args.push(remote);
M
Matt Shirley 已提交
1466
			args.push(branch);
1467 1468
		}

J
Joao Moreno 已提交
1469
		try {
1470
			await this.run(args, options);
J
Joao Moreno 已提交
1471 1472 1473 1474 1475 1476 1477
		} catch (err) {
			if (/^CONFLICT \([^)]+\): \b/m.test(err.stdout || '')) {
				err.gitErrorCode = GitErrorCodes.Conflict;
			} else if (/Please tell me who you are\./.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.NoUserNameConfigured;
			} else if (/Could not read from remote repository/.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
J
Joao Moreno 已提交
1478 1479
			} 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)) {
				err.stderr = err.stderr.replace(/Cannot pull with rebase: You have unstaged changes/i, 'Cannot pull with rebase, you have unstaged changes');
J
Joao Moreno 已提交
1480
				err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
J
Joao Moreno 已提交
1481 1482 1483 1484
			} else if (/cannot lock ref|unable to update local ref/i.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.CantLockRef;
			} else if (/cannot rebase onto multiple branches/i.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.CantRebaseMultipleBranches;
J
Joao Moreno 已提交
1485 1486 1487 1488 1489 1490
			}

			throw err;
		}
	}

1491
	async push(remote?: string, name?: string, setUpstream: boolean = false, tags = false, forcePushMode?: ForcePushMode): Promise<void> {
J
Joao Moreno 已提交
1492 1493
		const args = ['push'];

J
Joao Moreno 已提交
1494 1495 1496 1497
		if (forcePushMode === ForcePushMode.ForceWithLease) {
			args.push('--force-with-lease');
		} else if (forcePushMode === ForcePushMode.Force) {
			args.push('--force');
1498 1499
		}

J
Joao Moreno 已提交
1500
		if (setUpstream) {
J
Joao Moreno 已提交
1501 1502 1503
			args.push('-u');
		}

1504
		if (tags) {
1505
			args.push('--follow-tags');
J
Joao Moreno 已提交
1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522
		}

		if (remote) {
			args.push(remote);
		}

		if (name) {
			args.push(name);
		}

		try {
			await this.run(args);
		} catch (err) {
			if (/^error: failed to push some refs to\b/m.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.PushRejected;
			} else if (/Could not read from remote repository/.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
1523 1524
			} else if (/^fatal: The current branch .* has no upstream branch/.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.NoUpstreamBranch;
J
Joao Moreno 已提交
1525 1526 1527 1528 1529 1530
			}

			throw err;
		}
	}

R
rebornix 已提交
1531 1532 1533 1534 1535 1536 1537 1538 1539
	async blame(path: string): Promise<string> {
		try {
			const args = ['blame'];
			args.push(path);

			let result = await this.run(args);

			return result.stdout.trim();
		} catch (err) {
R
rebornix 已提交
1540 1541 1542 1543
			if (/^fatal: no such path/.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.NoPathFound;
			}

R
rebornix 已提交
1544 1545 1546 1547
			throw err;
		}
	}

1548
	async createStash(message?: string, includeUntracked?: boolean): Promise<void> {
1549
		try {
J
Joao Moreno 已提交
1550
			const args = ['stash', 'push'];
1551

1552 1553 1554 1555
			if (includeUntracked) {
				args.push('-u');
			}

J
Joao Moreno 已提交
1556
			if (message) {
J
Joao Moreno 已提交
1557
				args.push('-m', message);
1558 1559 1560 1561 1562 1563 1564
			}

			await this.run(args);
		} catch (err) {
			if (/No local changes to save/.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.NoLocalChanges;
			}
J
Joao Moreno 已提交
1565 1566 1567 1568 1569 1570

			throw err;
		}
	}

	async popStash(index?: number): Promise<void> {
1571
		const args = ['stash', 'pop'];
J
Joao Moreno 已提交
1572
		await this.popOrApplyStash(args, index);
1573 1574 1575 1576
	}

	async applyStash(index?: number): Promise<void> {
		const args = ['stash', 'apply'];
J
Joao Moreno 已提交
1577
		await this.popOrApplyStash(args, index);
1578
	}
J
Joao Moreno 已提交
1579

1580 1581
	private async popOrApplyStash(args: string[], index?: number): Promise<void> {
		try {
1582
			if (typeof index === 'number') {
J
Joao Moreno 已提交
1583
				args.push(`stash@{${index}}`);
1584
			}
J
Joao Moreno 已提交
1585 1586 1587 1588 1589 1590

			await this.run(args);
		} catch (err) {
			if (/No stash found/.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.NoStashFound;
			} else if (/error: Your local changes to the following files would be overwritten/.test(err.stderr || '')) {
1591
				err.gitErrorCode = GitErrorCodes.LocalChangesOverwritten;
J
Joao Moreno 已提交
1592 1593
			} else if (/^CONFLICT/m.test(err.stdout || '')) {
				err.gitErrorCode = GitErrorCodes.StashConflict;
1594
			}
J
Joao Moreno 已提交
1595

1596 1597 1598 1599
			throw err;
		}
	}

J
Joao Moreno 已提交
1600
	async dropStash(index?: number): Promise<void> {
1601 1602
		const args = ['stash', 'drop'];

D
Drew Cross 已提交
1603 1604
		if (typeof index === 'number') {
			args.push(`stash@{${index}}`);
J
Joao Moreno 已提交
1605
		}
1606

J
Joao Moreno 已提交
1607 1608
		try {
			await this.run(args);
1609 1610 1611 1612 1613 1614 1615 1616 1617
		} catch (err) {
			if (/No stash found/.test(err.stderr || '')) {
				err.gitErrorCode = GitErrorCodes.NoStashFound;
			}

			throw err;
		}
	}

1618 1619
	getStatus(limit = 5000): Promise<{ status: IFileStatus[]; didHitLimit: boolean; }> {
		return new Promise<{ status: IFileStatus[]; didHitLimit: boolean; }>((c, e) => {
1620
			const parser = new GitStatusParser();
J
Joao Moreno 已提交
1621 1622
			const env = { GIT_OPTIONAL_LOCKS: '0' };
			const child = this.stream(['status', '-z', '-u'], { env });
1623

M
Matt Bierner 已提交
1624
			const onExit = (exitCode: number) => {
1625
				if (exitCode !== 0) {
1626 1627 1628 1629 1630 1631 1632 1633
					const stderr = stderrData.join('');
					return e(new GitError({
						message: 'Failed to execute git',
						stderr,
						exitCode,
						gitErrorCode: getGitErrorCode(stderr),
						gitCommand: 'status'
					}));
1634
				}
J
Joao Moreno 已提交
1635

1636 1637 1638
				c({ status: parser.status, didHitLimit: false });
			};

1639
			const onStdoutData = (raw: string) => {
1640 1641
				parser.update(raw);

J
Joao Moreno 已提交
1642
				if (parser.status.length > limit) {
1643
					child.removeListener('exit', onExit);
1644
					child.stdout!.removeListener('data', onStdoutData);
1645 1646
					child.kill();

J
Joao Moreno 已提交
1647
					c({ status: parser.status.slice(0, limit), didHitLimit: true });
1648 1649 1650
				}
			};

1651 1652
			child.stdout!.setEncoding('utf8');
			child.stdout!.on('data', onStdoutData);
1653 1654

			const stderrData: string[] = [];
1655 1656
			child.stderr!.setEncoding('utf8');
			child.stderr!.on('data', raw => stderrData.push(raw as string));
1657

J
Joao Moreno 已提交
1658
			child.on('error', cpErrorHandler(e));
1659
			child.on('exit', onExit);
1660
		});
J
Joao Moreno 已提交
1661 1662
	}

J
Joao Moreno 已提交
1663
	async getHEAD(): Promise<Ref> {
J
Joao Moreno 已提交
1664
		try {
J
Joao Moreno 已提交
1665
			const result = await this.run(['symbolic-ref', '--short', 'HEAD']);
J
Joao Moreno 已提交
1666 1667 1668 1669 1670

			if (!result.stdout) {
				throw new Error('Not in a branch');
			}

R
Rob Lourens 已提交
1671
			return { name: result.stdout.trim(), commit: undefined, type: RefType.Head };
J
Joao Moreno 已提交
1672
		} catch (err) {
J
Joao Moreno 已提交
1673
			const result = await this.run(['rev-parse', 'HEAD']);
J
Joao Moreno 已提交
1674 1675 1676 1677 1678

			if (!result.stdout) {
				throw new Error('Error parsing HEAD');
			}

R
Rob Lourens 已提交
1679
			return { name: undefined, commit: result.stdout.trim(), type: RefType.Head };
J
Joao Moreno 已提交
1680 1681 1682
		}
	}

J
Joao Moreno 已提交
1683 1684
	async findTrackingBranches(upstreamBranch: string): Promise<Branch[]> {
		const result = await this.run(['for-each-ref', '--format', '%(refname:short)%00%(upstream:short)', 'refs/heads']);
1685
		return result.stdout.trim().split('\n')
J
Joao Moreno 已提交
1686 1687 1688
			.map(line => line.trim().split('\0'))
			.filter(([_, upstream]) => upstream === upstreamBranch)
			.map(([ref]) => ({ name: ref, type: RefType.Head } as Branch));
1689 1690
	}

J
Joao Moreno 已提交
1691
	async getRefs(opts?: { sort?: 'alphabetically' | 'committerdate' }): Promise<Ref[]> {
S
skprabhanjan 已提交
1692 1693
		const args = ['for-each-ref', '--format', '%(refname) %(objectname)'];

J
Joao Moreno 已提交
1694
		if (opts && opts.sort && opts.sort !== 'alphabetically') {
1695
			args.push('--sort', `-${opts.sort}`);
S
skprabhanjan 已提交
1696 1697 1698
		}

		const result = await this.run(args);
J
Joao Moreno 已提交
1699

M
Matt Bierner 已提交
1700
		const fn = (line: string): Ref | null => {
J
Joao Moreno 已提交
1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716
			let match: RegExpExecArray | null;

			if (match = /^refs\/heads\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
				return { name: match[1], commit: match[2], type: RefType.Head };
			} else if (match = /^refs\/remotes\/([^/]+)\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
				return { name: `${match[1]}/${match[2]}`, commit: match[3], type: RefType.RemoteHead, remote: match[1] };
			} else if (match = /^refs\/tags\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
				return { name: match[1], commit: match[2], type: RefType.Tag };
			}

			return null;
		};

		return result.stdout.trim().split('\n')
			.filter(line => !!line)
			.map(fn)
J
Joao Moreno 已提交
1717
			.filter(ref => !!ref) as Ref[];
J
Joao Moreno 已提交
1718 1719
	}

1720 1721
	async getStashes(): Promise<Stash[]> {
		const result = await this.run(['stash', 'list']);
J
Joao Moreno 已提交
1722
		const regex = /^stash@{(\d+)}:(.+)$/;
1723 1724
		const rawStashes = result.stdout.trim().split('\n')
			.filter(b => !!b)
M
Matt Bierner 已提交
1725
			.map(line => regex.exec(line) as RegExpExecArray)
1726
			.filter(g => !!g)
J
Joao Moreno 已提交
1727
			.map(([, index, description]: RegExpExecArray) => ({ index: parseInt(index), description }));
1728

J
Joao Moreno 已提交
1729 1730
		return rawStashes;
	}
1731

J
Joao Moreno 已提交
1732
	async getRemotes(): Promise<Remote[]> {
J
Joao Moreno 已提交
1733
		const result = await this.run(['remote', '--verbose']);
1734
		const lines = result.stdout.trim().split('\n').filter(l => !!l);
J
Joao Moreno 已提交
1735
		const remotes: MutableRemote[] = [];
J
Joao Moreno 已提交
1736

1737 1738
		for (const line of lines) {
			const parts = line.split(/\s/);
J
Joao Moreno 已提交
1739 1740 1741 1742
			const [name, url, type] = parts;

			let remote = remotes.find(r => r.name === name);

1743
			if (!remote) {
J
Joao Moreno 已提交
1744
				remote = { name, isReadOnly: false };
1745 1746 1747
				remotes.push(remote);
			}

J
Joao Moreno 已提交
1748 1749 1750 1751 1752 1753 1754
			if (/fetch/i.test(type)) {
				remote.fetchUrl = url;
			} else if (/push/i.test(type)) {
				remote.pushUrl = url;
			} else {
				remote.fetchUrl = url;
				remote.pushUrl = url;
1755 1756 1757
			}

			// https://github.com/Microsoft/vscode/issues/45271
J
Joao Moreno 已提交
1758
			remote.isReadOnly = remote.pushUrl === undefined || remote.pushUrl === 'no_push';
1759
		}
J
Joao Moreno 已提交
1760

1761
		return remotes;
J
Joao Moreno 已提交
1762 1763
	}

J
Joao Moreno 已提交
1764
	async getBranch(name: string): Promise<Branch> {
J
Joao Moreno 已提交
1765 1766 1767 1768
		if (name === 'HEAD') {
			return this.getHEAD();
		}

1769 1770 1771 1772 1773 1774 1775 1776
		let result = await this.run(['rev-parse', name]);

		if (!result.stdout && /^@/.test(name)) {
			const symbolicFullNameResult = await this.run(['rev-parse', '--symbolic-full-name', name]);
			name = symbolicFullNameResult.stdout.trim();

			result = await this.run(['rev-parse', name]);
		}
J
Joao Moreno 已提交
1777 1778

		if (!result.stdout) {
J
Joao Moreno 已提交
1779
			return Promise.reject<Branch>(new Error('No such branch'));
J
Joao Moreno 已提交
1780 1781 1782 1783 1784
		}

		const commit = result.stdout.trim();

		try {
J
Joao Moreno 已提交
1785 1786 1787 1788 1789 1790 1791
			const res2 = await this.run(['rev-parse', '--symbolic-full-name', name + '@{u}']);
			const fullUpstream = res2.stdout.trim();
			const match = /^refs\/remotes\/([^/]+)\/(.+)$/.exec(fullUpstream);

			if (!match) {
				throw new Error(`Could not parse upstream branch: ${fullUpstream}`);
			}
J
Joao Moreno 已提交
1792

J
Joao Moreno 已提交
1793 1794
			const upstream = { remote: match[1], name: match[2] };
			const res3 = await this.run(['rev-list', '--left-right', name + '...' + fullUpstream]);
J
Joao Moreno 已提交
1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814

			let ahead = 0, behind = 0;
			let i = 0;

			while (i < res3.stdout.length) {
				switch (res3.stdout.charAt(i)) {
					case '<': ahead++; break;
					case '>': behind++; break;
					default: i++; break;
				}

				while (res3.stdout.charAt(i++) !== '\n') { /* no-op */ }
			}

			return { name, type: RefType.Head, commit, upstream, ahead, behind };
		} catch (err) {
			return { name, type: RefType.Head, commit };
		}
	}

1815 1816 1817
	// TODO: Support core.commentChar
	stripCommitMessageComments(message: string): string {
		return message.replace(/^\s*#.*$\n?/gm, '').trim();
1818 1819 1820 1821
	}

	async getMergeMessage(): Promise<string | undefined> {
		const mergeMsgPath = path.join(this.repositoryRoot, '.git', 'MERGE_MSG');
1822

1823
		try {
J
Joao Moreno 已提交
1824
			const raw = await fs.readFile(mergeMsgPath, 'utf8');
1825
			return this.stripCommitMessageComments(raw);
J
Joao Moreno 已提交
1826
		} catch {
1827 1828 1829 1830
			return undefined;
		}
	}

J
Joao Moreno 已提交
1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844
	async getCommitTemplate(): Promise<string> {
		try {
			const result = await this.run(['config', '--get', 'commit.template']);

			if (!result.stdout) {
				return '';
			}

			// https://github.com/git/git/blob/3a0f269e7c82aa3a87323cb7ae04ac5f129f036b/path.c#L612
			const homedir = os.homedir();
			let templatePath = result.stdout.trim()
				.replace(/^~([^\/]*)\//, (_, user) => `${user ? path.join(path.dirname(homedir), user) : homedir}/`);

			if (!path.isAbsolute(templatePath)) {
J
Joao Moreno 已提交
1845
				templatePath = path.join(this.repositoryRoot, templatePath);
J
Joao Moreno 已提交
1846 1847
			}

J
Joao Moreno 已提交
1848
			const raw = await fs.readFile(templatePath, 'utf8');
1849
			return this.stripCommitMessageComments(raw);
J
Joao Moreno 已提交
1850 1851 1852 1853 1854
		} catch (err) {
			return '';
		}
	}

J
Joao Moreno 已提交
1855
	async getCommit(ref: string): Promise<Commit> {
1856
		const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, ref]);
1857
		return parseGitCommit(result.stdout) || Promise.reject<Commit>('bad commit format');
J
Joao Moreno 已提交
1858
	}
1859 1860

	async updateSubmodules(paths: string[]): Promise<void> {
J
Joao Moreno 已提交
1861 1862 1863 1864 1865
		const args = ['submodule', 'update', '--'];

		for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
			await this.run([...args, ...chunk]);
		}
1866 1867 1868 1869 1870 1871
	}

	async getSubmodules(): Promise<Submodule[]> {
		const gitmodulesPath = path.join(this.root, '.gitmodules');

		try {
J
Joao Moreno 已提交
1872
			const gitmodulesRaw = await fs.readFile(gitmodulesPath, 'utf8');
1873 1874 1875 1876 1877 1878 1879 1880 1881
			return parseGitmodules(gitmodulesRaw);
		} catch (err) {
			if (/ENOENT/.test(err.message)) {
				return [];
			}

			throw err;
		}
	}
1882
}