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

'use strict';

8
import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor, MessageOptions } from 'vscode';
J
Joao Moreno 已提交
9
import { Ref, RefType, Git, GitErrorCodes, Branch } from './git';
J
Joao Moreno 已提交
10
import { Repository, Resource, Status, CommitOptions, ResourceGroupType } from './repository';
J
Joao Moreno 已提交
11
import { Model } from './model';
J
Joao Moreno 已提交
12
import { toGitUri, fromGitUri } from './uri';
13
import { grep, isDescendant, pathEquals } from './util';
J
Joao Moreno 已提交
14
import { applyLineChanges, intersectDiffWithRange, toLineRanges, invertLineChange, getModifiedRange } from './staging';
J
Joao Moreno 已提交
15
import * as path from 'path';
16
import { lstat, Stats } from 'fs';
J
Joao Moreno 已提交
17
import * as os from 'os';
J
Joao Moreno 已提交
18
import TelemetryReporter from 'vscode-extension-telemetry';
J
Joao Moreno 已提交
19 20 21
import * as nls from 'vscode-nls';

const localize = nls.loadMessageBundle();
J
Joao Moreno 已提交
22

J
Joao Moreno 已提交
23 24 25 26 27 28 29
class CheckoutItem implements QuickPickItem {

	protected get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); }
	protected get treeish(): string | undefined { return this.ref.name; }
	get label(): string { return this.ref.name || this.shortCommit; }
	get description(): string { return this.shortCommit; }

J
Joao Moreno 已提交
30
	constructor(protected ref: Ref) { }
J
Joao Moreno 已提交
31

J
Joao Moreno 已提交
32
	async run(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
33 34 35 36 37 38
		const ref = this.treeish;

		if (!ref) {
			return;
		}

J
Joao Moreno 已提交
39
		await repository.checkout(ref);
J
Joao Moreno 已提交
40 41 42 43 44
	}
}

class CheckoutTagItem extends CheckoutItem {

J
Joao Moreno 已提交
45 46 47
	get description(): string {
		return localize('tag at', "Tag at {0}", this.shortCommit);
	}
J
Joao Moreno 已提交
48 49 50 51
}

class CheckoutRemoteHeadItem extends CheckoutItem {

J
Joao Moreno 已提交
52 53 54
	get description(): string {
		return localize('remote branch at', "Remote branch at {0}", this.shortCommit);
	}
J
Joao Moreno 已提交
55 56 57 58 59 60 61 62 63 64 65

	protected get treeish(): string | undefined {
		if (!this.ref.name) {
			return;
		}

		const match = /^[^/]+\/(.*)$/.exec(this.ref.name);
		return match ? match[1] : this.ref.name;
	}
}

M
Maik Riechert 已提交
66 67
class BranchDeleteItem implements QuickPickItem {

68 69 70
	private get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); }
	get branchName(): string | undefined { return this.ref.name; }
	get label(): string { return this.branchName || ''; }
M
Maik Riechert 已提交
71 72
	get description(): string { return this.shortCommit; }

73
	constructor(private ref: Ref) { }
M
Maik Riechert 已提交
74

J
Joao Moreno 已提交
75
	async run(repository: Repository, force?: boolean): Promise<void> {
76
		if (!this.branchName) {
M
Maik Riechert 已提交
77 78
			return;
		}
J
Joao Moreno 已提交
79
		await repository.deleteBranch(this.branchName, force);
M
Maik Riechert 已提交
80 81 82
	}
}

83 84 85 86 87 88
class MergeItem implements QuickPickItem {

	get label(): string { return this.ref.name || ''; }
	get description(): string { return this.ref.name || ''; }

	constructor(protected ref: Ref) { }
J
Joao Moreno 已提交
89

J
Joao Moreno 已提交
90 91
	async run(repository: Repository): Promise<void> {
		await repository.merge(this.ref.name! || this.ref.commit!);
J
Joao Moreno 已提交
92
	}
93 94
}

J
Joao Moreno 已提交
95 96
class CreateBranchItem implements QuickPickItem {

J
Joao Moreno 已提交
97 98
	constructor(private cc: CommandCenter) { }

J
Joao Moreno 已提交
99 100 101
	get label(): string { return localize('create branch', '$(plus) Create new branch'); }
	get description(): string { return ''; }

J
Joao Moreno 已提交
102
	async run(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
103
		await this.cc.branch(repository);
J
Joao Moreno 已提交
104 105 106
	}
}

J
Joao Moreno 已提交
107
interface CommandOptions {
J
Joao Moreno 已提交
108
	repository?: boolean;
J
Joao Moreno 已提交
109 110 111
	diff?: boolean;
}

112 113 114 115
interface Command {
	commandId: string;
	key: string;
	method: Function;
J
Joao Moreno 已提交
116
	options: CommandOptions;
117 118 119
}

const Commands: Command[] = [];
J
Joao Moreno 已提交
120

J
Joao Moreno 已提交
121
function command(commandId: string, options: CommandOptions = {}): Function {
J
Joao Moreno 已提交
122
	return (target: any, key: string, descriptor: any) => {
J
Joao Moreno 已提交
123 124 125 126
		if (!(typeof descriptor.value === 'function')) {
			throw new Error('not supported');
		}

J
Joao Moreno 已提交
127
		Commands.push({ commandId, key, method: descriptor.value, options });
J
Joao Moreno 已提交
128 129
	};
}
J
Joao Moreno 已提交
130

J
Joao Moreno 已提交
131 132 133 134 135 136 137 138 139
const ImageMimetypes = [
	'image/png',
	'image/gif',
	'image/jpeg',
	'image/webp',
	'image/tiff',
	'image/bmp'
];

140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
async function categorizeResourceByResolution(resources: Resource[]): Promise<{ merge: Resource[], resolved: Resource[], unresolved: Resource[] }> {
	const selection = resources.filter(s => s instanceof Resource) as Resource[];
	const merge = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge);
	const isBothAddedOrModified = (s: Resource) => s.type === Status.BOTH_MODIFIED || s.type === Status.BOTH_ADDED;
	const possibleUnresolved = merge.filter(isBothAddedOrModified);
	const promises = possibleUnresolved.map(s => grep(s.resourceUri.fsPath, /^<{7}|^={7}|^>{7}/));
	const unresolvedBothModified = await Promise.all<boolean>(promises);
	const resolved = possibleUnresolved.filter((s, i) => !unresolvedBothModified[i]);
	const unresolved = [
		...merge.filter(s => !isBothAddedOrModified(s)),
		...possibleUnresolved.filter((s, i) => unresolvedBothModified[i])
	];

	return { merge, resolved, unresolved };
}

J
Joao Moreno 已提交
156
export class CommandCenter {
J
Joao Moreno 已提交
157 158

	private disposables: Disposable[];
J
Joao Moreno 已提交
159

J
Joao Moreno 已提交
160
	constructor(
J
Joao Moreno 已提交
161
		private git: Git,
J
Joao Moreno 已提交
162
		private model: Model,
J
Joao Moreno 已提交
163 164
		private outputChannel: OutputChannel,
		private telemetryReporter: TelemetryReporter
J
Joao Moreno 已提交
165
	) {
J
Joao Moreno 已提交
166 167
		this.disposables = Commands.map(({ commandId, key, method, options }) => {
			const command = this.createCommand(commandId, key, method, options);
168

J
Joao Moreno 已提交
169
			if (options.diff) {
J
Joao Moreno 已提交
170 171 172 173 174
				return commands.registerDiffInformationCommand(commandId, command);
			} else {
				return commands.registerCommand(commandId, command);
			}
		});
J
Joao Moreno 已提交
175 176
	}

J
Joao Moreno 已提交
177 178
	@command('git.refresh', { repository: true })
	async refresh(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
179
		await repository.status();
J
Joao Moreno 已提交
180
	}
J
Joao Moreno 已提交
181

J
Joao Moreno 已提交
182 183
	@command('git.openResource')
	async openResource(resource: Resource): Promise<void> {
J
Joao 已提交
184
		await this._openResource(resource, undefined, true, false);
J
Joao Moreno 已提交
185 186
	}

J
Joao 已提交
187
	private async _openResource(resource: Resource, preview?: boolean, preserveFocus?: boolean, preserveSelection?: boolean): Promise<void> {
188
		let stat: Stats | undefined;
189

190 191 192 193
		try {
			stat = await new Promise<Stats>((c, e) => lstat(resource.resourceUri.fsPath, (err, stat) => err ? e(err) : c(stat)));
		} catch (err) {
			// noop
194
		}
195

196 197 198 199
		let left: Uri | undefined;
		let right: Uri | undefined;

		if (stat && stat.isDirectory()) {
200 201 202 203
			const repository = this.model.getRepositoryForSubmodule(resource.resourceUri);

			if (repository) {
				right = toGitUri(resource.resourceUri, resource.resourceGroupType === ResourceGroupType.Index ? 'index' : 'wt', { submoduleOf: repository.root });
204 205 206 207 208
			}
		} else {
			left = await this.getLeftResource(resource);
			right = await this.getRightResource(resource);
		}
209

J
Joao Moreno 已提交
210 211
		const title = this.getTitle(resource);

J
Joao Moreno 已提交
212 213 214 215 216
		if (!right) {
			// TODO
			console.error('oh no');
			return;
		}
J
Joao Moreno 已提交
217

J
Joao Moreno 已提交
218
		const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
219 220
			preserveFocus,
			preview,
221
			viewColumn: ViewColumn.Active
J
Joao Moreno 已提交
222 223
		};

J
Joao Moreno 已提交
224 225
		const activeTextEditor = window.activeTextEditor;

226 227 228
		// Check if active text editor has same path as other editor. we cannot compare via
		// URI.toString() here because the schemas can be different. Instead we just go by path.
		if (preserveSelection && activeTextEditor && activeTextEditor.document.uri.path === right.path) {
J
Joao Moreno 已提交
229 230 231
			opts.selection = activeTextEditor.selection;
		}

232
		if (!left) {
J
Joao Moreno 已提交
233 234 235 236 237
			await commands.executeCommand<void>('vscode.open', right, opts);
		} else {
			await commands.executeCommand<void>('vscode.diff', left, right, title, opts);
		}
	}
J
Joao Moreno 已提交
238

J
Joao Moreno 已提交
239 240
	private async getURI(uri: Uri, ref: string): Promise<Uri | undefined> {
		const repository = this.model.getRepository(uri);
J
Joao Moreno 已提交
241

J
Joao Moreno 已提交
242 243 244
		if (!repository) {
			return toGitUri(uri, ref);
		}
J
Joao Moreno 已提交
245

J
Joao Moreno 已提交
246
		try {
J
Joao Moreno 已提交
247 248 249
			let gitRef = ref;

			if (gitRef === '~') {
J
Joao Moreno 已提交
250
				const uriString = uri.toString();
J
Joao Moreno 已提交
251
				const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
J
Joao Moreno 已提交
252
				gitRef = indexStatus ? '' : 'HEAD';
J
Joao Moreno 已提交
253 254
			}

J
Joao Moreno 已提交
255
			const { size, object } = await repository.lstree(gitRef, uri.fsPath);
J
Joao Moreno 已提交
256
			const { mimetype } = await repository.detectObjectType(object);
J
Joao Moreno 已提交
257

J
Joao Moreno 已提交
258 259 260
			if (mimetype === 'text/plain') {
				return toGitUri(uri, ref);
			}
261

J
Joao Moreno 已提交
262
			if (size > 1000000) { // 1 MB
J
Joao Moreno 已提交
263
				return Uri.parse(`data:;label:${path.basename(uri.fsPath)};description:${gitRef},`);
J
Joao Moreno 已提交
264 265
			}

J
Joao Moreno 已提交
266
			if (ImageMimetypes.indexOf(mimetype) > -1) {
J
Joao Moreno 已提交
267 268
				const contents = await repository.buffer(gitRef, uri.fsPath);
				return Uri.parse(`data:${mimetype};label:${path.basename(uri.fsPath)};description:${gitRef};size:${size};base64,${contents.toString('base64')}`);
J
Joao Moreno 已提交
269 270
			}

J
Joao Moreno 已提交
271
			return Uri.parse(`data:;label:${path.basename(uri.fsPath)};description:${gitRef},`);
J
Joao Moreno 已提交
272 273 274 275

		} catch (err) {
			return toGitUri(uri, ref);
		}
J
Joao Moreno 已提交
276 277
	}

J
Joao Moreno 已提交
278
	private async getLeftResource(resource: Resource): Promise<Uri | undefined> {
J
Joao Moreno 已提交
279 280 281
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
J
Joao Moreno 已提交
282
				return this.getURI(resource.original, 'HEAD');
J
Joao Moreno 已提交
283 284

			case Status.MODIFIED:
J
Joao Moreno 已提交
285
				return this.getURI(resource.resourceUri, '~');
286 287

			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
288
				return this.getURI(resource.resourceUri, '');
J
Joao Moreno 已提交
289
		}
J
Joao Moreno 已提交
290
	}
J
Joao Moreno 已提交
291

J
Joao Moreno 已提交
292
	private async getRightResource(resource: Resource): Promise<Uri | undefined> {
J
Joao Moreno 已提交
293 294 295 296 297
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_ADDED:
			case Status.INDEX_COPIED:
			case Status.INDEX_RENAMED:
J
Joao Moreno 已提交
298
				return this.getURI(resource.resourceUri, '');
J
Joao Moreno 已提交
299 300

			case Status.INDEX_DELETED:
301
			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
302
			case Status.DELETED:
J
Joao Moreno 已提交
303
				return this.getURI(resource.resourceUri, 'HEAD');
J
Joao Moreno 已提交
304 305 306 307

			case Status.MODIFIED:
			case Status.UNTRACKED:
			case Status.IGNORED:
J
Joao Moreno 已提交
308 309 310 311 312 313
				const repository = this.model.getRepository(resource.resourceUri);

				if (!repository) {
					return;
				}

J
Joao Moreno 已提交
314
				const uriString = resource.resourceUri.toString();
J
Joao Moreno 已提交
315
				const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
J
Joao Moreno 已提交
316

J
Joao Moreno 已提交
317 318
				if (indexStatus && indexStatus.renameResourceUri) {
					return indexStatus.renameResourceUri;
J
Joao Moreno 已提交
319 320
				}

J
Joao Moreno 已提交
321
				return resource.resourceUri;
J
Joao Moreno 已提交
322

323
			case Status.BOTH_ADDED:
J
Joao Moreno 已提交
324
			case Status.BOTH_MODIFIED:
J
Joao Moreno 已提交
325
				return resource.resourceUri;
J
Joao Moreno 已提交
326 327 328 329
		}
	}

	private getTitle(resource: Resource): string {
J
Joao Moreno 已提交
330
		const basename = path.basename(resource.resourceUri.fsPath);
J
Joao Moreno 已提交
331 332 333 334

		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
M
Marc Kassay 已提交
335
			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
336 337 338
				return `${basename} (Index)`;

			case Status.MODIFIED:
M
Marc Kassay 已提交
339 340
			case Status.BOTH_ADDED:
			case Status.BOTH_MODIFIED:
J
Joao Moreno 已提交
341 342 343 344 345 346
				return `${basename} (Working Tree)`;
		}

		return '';
	}

J
Joao Moreno 已提交
347
	@command('git.clone')
348 349 350 351 352 353 354
	async clone(url?: string): Promise<void> {
		if (!url) {
			url = await window.showInputBox({
				prompt: localize('repourl', "Repository URL"),
				ignoreFocusOut: true
			});
		}
J
Joao Moreno 已提交
355 356

		if (!url) {
K
kieferrm 已提交
357
			/* __GDPR__
K
kieferrm 已提交
358 359 360 361
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
362 363
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
			return;
J
Joao Moreno 已提交
364 365
		}

366
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
367 368 369 370 371 372 373 374 375
		let defaultCloneDirectory = config.get<string>('defaultCloneDirectory') || os.homedir();
		defaultCloneDirectory = defaultCloneDirectory.replace(/^~/, os.homedir());

		const uris = await window.showOpenDialog({
			canSelectFiles: false,
			canSelectFolders: true,
			canSelectMany: false,
			defaultUri: Uri.file(defaultCloneDirectory),
			openLabel: localize('selectFolder', "Select Repository Location")
J
Joao Moreno 已提交
376 377
		});

J
Joao Moreno 已提交
378
		if (!uris || uris.length === 0) {
K
kieferrm 已提交
379
			/* __GDPR__
K
kieferrm 已提交
380 381 382 383
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
384 385
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
			return;
J
Joao Moreno 已提交
386 387
		}

J
Joao Moreno 已提交
388 389 390
		const uri = uris[0];
		const parentPath = uri.fsPath;

391
		try {
392 393 394 395 396
			const opts = {
				location: ProgressLocation.Notification,
				title: localize('cloning', "Cloning git repository '{0}'...", url),
				cancellable: true
			};
M
Maryam Archie 已提交
397

398 399
			const repositoryPath = await window.withProgress(
				opts,
J
Joao Moreno 已提交
400
				(_, token) => this.git.clone(url!, parentPath, token)
401
			);
402

403 404
			const choices = [];
			let message = localize('proposeopen', "Would you like to open the cloned repository?");
405
			const open = localize('openrepo', "Open Repository");
406 407 408 409 410 411 412 413 414
			choices.push(open);

			const addToWorkspace = localize('add', "Add to Workspace");
			if (workspace.workspaceFolders) {
				message = localize('proposeopen2', "Would you like to open the cloned repository, or add it to the current workspace?");
				choices.push(addToWorkspace);
			}

			const result = await window.showInformationMessage(message, ...choices);
415 416

			const openFolder = result === open;
K
kieferrm 已提交
417
			/* __GDPR__
K
kieferrm 已提交
418 419 420 421 422
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
					"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
				}
			*/
423
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: openFolder ? 1 : 0 });
424 425 426

			const uri = Uri.file(repositoryPath);

427
			if (openFolder) {
428 429 430
				commands.executeCommand('vscode.openFolder', uri);
			} else if (result === addToWorkspace) {
				workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
431
			}
432 433
		} catch (err) {
			if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
K
kieferrm 已提交
434
				/* __GDPR__
K
kieferrm 已提交
435 436 437 438
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
439
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
J
Joao Moreno 已提交
440 441
			} else if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
				return;
442
			} else {
K
kieferrm 已提交
443
				/* __GDPR__
K
kieferrm 已提交
444 445 446 447
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
448
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
449
			}
J
Joao Moreno 已提交
450

451
			throw err;
J
Joao Moreno 已提交
452 453 454
		}
	}

J
Joao Moreno 已提交
455
	@command('git.init')
J
Joao Moreno 已提交
456
	async init(): Promise<void> {
J
Joao Moreno 已提交
457
		let path: string | undefined;
J
Joao Moreno 已提交
458

J
Joao Moreno 已提交
459 460 461 462 463 464 465 466
		if (workspace.workspaceFolders && workspace.workspaceFolders.length > 1) {
			const placeHolder = localize('init', "Pick workspace folder to initialize git repo in");
			const items = workspace.workspaceFolders.map(folder => ({ label: folder.name, description: folder.uri.fsPath, folder }));
			const item = await window.showQuickPick(items, { placeHolder, ignoreFocusOut: true });

			if (!item) {
				return;
			}
J
Joao Moreno 已提交
467

J
Joao Moreno 已提交
468 469
			path = item.folder.uri.fsPath;
		}
J
Joao Moreno 已提交
470

J
Joao Moreno 已提交
471 472 473 474 475 476 477 478 479 480 481 482 483
		if (!path) {
			const homeUri = Uri.file(os.homedir());
			const defaultUri = workspace.workspaceFolders && workspace.workspaceFolders.length > 0
				? Uri.file(workspace.workspaceFolders[0].uri.fsPath)
				: homeUri;

			const result = await window.showOpenDialog({
				canSelectFiles: false,
				canSelectFolders: true,
				canSelectMany: false,
				defaultUri,
				openLabel: localize('init repo', "Initialize Repository")
			});
J
Joao Moreno 已提交
484

J
Joao Moreno 已提交
485
			if (!result || result.length === 0) {
J
Joao Moreno 已提交
486 487
				return;
			}
J
Joao Moreno 已提交
488 489 490 491 492 493 494 495 496 497 498 499 500

			const uri = result[0];

			if (homeUri.toString().startsWith(uri.toString())) {
				const yes = localize('create repo', "Initialize Repository");
				const answer = await window.showWarningMessage(localize('are you sure', "This will create a Git repository in '{0}'. Are you sure you want to continue?", uri.fsPath), yes);

				if (answer !== yes) {
					return;
				}
			}

			path = uri.fsPath;
J
Joao Moreno 已提交
501 502
		}

J
Joao Moreno 已提交
503 504
		await this.git.init(path);
		await this.model.tryOpenRepository(path);
J
Joao Moreno 已提交
505 506
	}

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
	@command('git.loadRepo', { repository: false })
	async loadRepo(path?: string): Promise<void> {

		if (!path) {
			path = await window.showInputBox({
				prompt: localize('repopath', "Repository Path"),
				ignoreFocusOut: true
			});
		}

		if (path) {

			try {

				if (this.model.getRepository(path)) {
					await this.model.tryOpenRepository(path);
				}

				else {
					window.showInformationMessage(localize('notfound', "Could not find a repository at this path"));
				}

				return;
			}

			catch (err) {
				//If something went wrong, tryOpenRepository should have already given error
			}
		}
	}

J
Joao Moreno 已提交
538 539 540 541 542
	@command('git.close', { repository: true })
	async close(repository: Repository): Promise<void> {
		this.model.close(repository);
	}

J
Joao Moreno 已提交
543 544
	@command('git.openFile')
	async openFile(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
545
		const preserveFocus = arg instanceof Resource;
J
Joao Moreno 已提交
546

547
		let uris: Uri[] | undefined;
548 549 550

		if (arg instanceof Uri) {
			if (arg.scheme === 'git') {
551
				uris = [Uri.file(fromGitUri(arg).path)];
552
			} else if (arg.scheme === 'file') {
553
				uris = [arg];
554 555 556 557 558 559
			}
		} else {
			let resource = arg;

			if (!(resource instanceof Resource)) {
				// can happen when called from a keybinding
560
				resource = this.getSCMResource();
561 562 563
			}

			if (resource) {
J
Joao Moreno 已提交
564 565 566 567
				const resources = ([resource, ...resourceStates] as Resource[])
					.filter(r => r.type !== Status.DELETED && r.type !== Status.INDEX_DELETED);

				uris = resources.map(r => r.resourceUri);
568
			}
J
Joao Moreno 已提交
569 570
		}

571
		if (!uris) {
J
Joao Moreno 已提交
572
			return;
J
Joao Moreno 已提交
573 574
		}

575 576 577 578
		const preview = uris.length === 1 ? true : false;
		const activeTextEditor = window.activeTextEditor;
		for (const uri of uris) {
			const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
579
				preserveFocus,
580
				preview,
581
				viewColumn: ViewColumn.Active
582 583
			};

584 585 586
			// Check if active text editor has same path as other editor. we cannot compare via
			// URI.toString() here because the schemas can be different. Instead we just go by path.
			if (activeTextEditor && activeTextEditor.document.uri.path === uri.path) {
B
Benjamin Pasero 已提交
587 588 589
				opts.selection = activeTextEditor.selection;
			}

590
			await commands.executeCommand<void>('vscode.open', uri, opts);
J
Joao Moreno 已提交
591
		}
J
Joao Moreno 已提交
592 593
	}

J
Joao Moreno 已提交
594 595 596 597 598
	@command('git.openFile2')
	async openFile2(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
		this.openFile(arg, ...resourceStates);
	}

J
Joao Moreno 已提交
599 600
	@command('git.openHEADFile')
	async openHEADFile(arg?: Resource | Uri): Promise<void> {
D
Duroktar 已提交
601 602 603 604 605
		let resource: Resource | undefined = undefined;

		if (arg instanceof Resource) {
			resource = arg;
		} else if (arg instanceof Uri) {
606
			resource = this.getSCMResource(arg);
D
Duroktar 已提交
607
		} else {
608
			resource = this.getSCMResource();
D
Duroktar 已提交
609 610 611 612 613 614
		}

		if (!resource) {
			return;
		}

J
Joao Moreno 已提交
615
		const HEAD = await this.getLeftResource(resource);
D
Duroktar 已提交
616

J
Joao Moreno 已提交
617 618 619
		if (!HEAD) {
			window.showWarningMessage(localize('HEAD not available', "HEAD version of '{0}' is not available.", path.basename(resource.resourceUri.fsPath)));
			return;
D
Duroktar 已提交
620
		}
J
Joao Moreno 已提交
621 622

		return await commands.executeCommand<void>('vscode.open', HEAD);
D
Duroktar 已提交
623 624
	}

J
Joao Moreno 已提交
625 626
	@command('git.openChange')
	async openChange(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
627
		const preserveFocus = arg instanceof Resource;
J
Joao 已提交
628
		const preserveSelection = arg instanceof Uri || !arg;
629
		let resources: Resource[] | undefined = undefined;
630

631
		if (arg instanceof Uri) {
632
			const resource = this.getSCMResource(arg);
633 634 635
			if (resource !== undefined) {
				resources = [resource];
			}
636
		} else {
637
			let resource: Resource | undefined = undefined;
J
Joao Moreno 已提交
638

639 640 641
			if (arg instanceof Resource) {
				resource = arg;
			} else {
642
				resource = this.getSCMResource();
643 644 645 646 647
			}

			if (resource) {
				resources = [...resourceStates as Resource[], resource];
			}
J
Joao Moreno 已提交
648 649
		}

650
		if (!resources) {
J
Joao Moreno 已提交
651
			return;
J
Joao Moreno 已提交
652
		}
J
Joao Moreno 已提交
653

654 655
		const preview = resources.length === 1 ? undefined : false;
		for (const resource of resources) {
J
Joao 已提交
656
			await this._openResource(resource, preview, preserveFocus, preserveSelection);
657
		}
J
Joao Moreno 已提交
658 659
	}

660 661
	@command('git.stage')
	async stage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
662 663
		this.outputChannel.appendLine(`git.stage ${resourceStates.length}`);

664 665
		resourceStates = resourceStates.filter(s => !!s);

J
Joao Moreno 已提交
666
		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
667
			const resource = this.getSCMResource();
668

J
Joao Moreno 已提交
669 670
			this.outputChannel.appendLine(`git.stage.getSCMResource ${resource ? resource.resourceUri.toString() : null}`);

671 672 673 674 675 676 677
			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

678
		const selection = resourceStates.filter(s => s instanceof Resource) as Resource[];
679 680 681 682 683 684
		const { resolved, unresolved } = await categorizeResourceByResolution(selection);

		if (unresolved.length > 0) {
			const message = unresolved.length > 1
				? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", unresolved.length)
				: localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(unresolved[0].resourceUri.fsPath));
685

686 687 688 689 690 691 692 693
			const yes = localize('yes', "Yes");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

			if (pick !== yes) {
				return;
			}
		}

J
Joao Moreno 已提交
694
		const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree);
695
		const scmResources = [...workingTree, ...resolved, ...unresolved];
696

J
Joao Moreno 已提交
697
		this.outputChannel.appendLine(`git.stage.scmResources ${scmResources.length}`);
698
		if (!scmResources.length) {
J
Joao Moreno 已提交
699 700
			return;
		}
J
Joao Moreno 已提交
701

702
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
703
		await this.runByRepository(resources, async (repository, resources) => repository.add(resources));
J
Joao Moreno 已提交
704 705
	}

J
Joao Moreno 已提交
706 707
	@command('git.stageAll', { repository: true })
	async stageAll(repository: Repository): Promise<void> {
708
		const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[];
709 710 711 712
		const { merge, unresolved } = await categorizeResourceByResolution(resources);

		if (unresolved.length > 0) {
			const message = unresolved.length > 1
J
Joao Moreno 已提交
713 714
				? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", merge.length)
				: localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(merge[0].resourceUri.fsPath));
715 716 717 718 719 720 721 722 723

			const yes = localize('yes', "Yes");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

			if (pick !== yes) {
				return;
			}
		}

J
Joao Moreno 已提交
724
		await repository.add([]);
J
Joao Moreno 已提交
725 726
	}

J
Joao Moreno 已提交
727
	@command('git.stageChange')
J
Joao Moreno 已提交
728 729 730 731 732 733 734 735
	async stageChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
		const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];

		if (!textEditor) {
			return;
		}

		await this._stageChanges(textEditor, [changes[index]]);
J
Joao Moreno 已提交
736 737
	}

J
Joao Moreno 已提交
738
	@command('git.stageSelectedRanges', { diff: true })
J
Joao Moreno 已提交
739
	async stageSelectedChanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
740 741 742 743 744 745 746
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
J
Joao Moreno 已提交
747 748 749 750
		const selectedLines = toLineRanges(textEditor.selections, modifiedDocument);
		const selectedChanges = changes
			.map(diff => selectedLines.reduce<LineChange | null>((result, range) => result || intersectDiffWithRange(modifiedDocument, diff, range), null))
			.filter(d => !!d) as LineChange[];
J
Joao Moreno 已提交
751

J
Joao Moreno 已提交
752
		if (!selectedChanges.length) {
J
Joao Moreno 已提交
753 754 755
			return;
		}

J
Joao Moreno 已提交
756 757
		await this._stageChanges(textEditor, selectedChanges);
	}
J
Joao Moreno 已提交
758

J
Joao Moreno 已提交
759
	private async _stageChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
760 761 762
		const modifiedDocument = textEditor.document;
		const modifiedUri = modifiedDocument.uri;

J
Joao Moreno 已提交
763
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
764 765 766
			return;
		}

J
Joao Moreno 已提交
767
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
768
		const originalDocument = await workspace.openTextDocument(originalUri);
J
Joao Moreno 已提交
769
		const result = applyLineChanges(originalDocument, modifiedDocument, changes);
J
Joao Moreno 已提交
770

J
Joao Moreno 已提交
771
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
772
	}
J
Joao Moreno 已提交
773

J
Joao Moreno 已提交
774
	@command('git.revertChange')
J
Joao Moreno 已提交
775 776 777 778
	async revertChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
		const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];

		if (!textEditor) {
J
Joao Moreno 已提交
779 780 781
			return;
		}

J
Joao Moreno 已提交
782
		await this._revertChanges(textEditor, [...changes.slice(0, index), ...changes.slice(index + 1)]);
J
Joao Moreno 已提交
783
	}
J
Joao Moreno 已提交
784

J
Joao Moreno 已提交
785
	@command('git.revertSelectedRanges', { diff: true })
J
Joao Moreno 已提交
786
	async revertSelectedRanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
787 788 789 790 791 792 793
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
J
Joao Moreno 已提交
794 795
		const selections = textEditor.selections;
		const selectedChanges = changes.filter(change => {
J
Joao Moreno 已提交
796
			const modifiedRange = getModifiedRange(modifiedDocument, change);
J
Joao Moreno 已提交
797 798 799 800
			return selections.every(selection => !selection.intersection(modifiedRange));
		});

		if (selectedChanges.length === changes.length) {
J
Joao Moreno 已提交
801 802 803
			return;
		}

J
Joao Moreno 已提交
804 805
		await this._revertChanges(textEditor, selectedChanges);
	}
J
Joao Moreno 已提交
806

J
Joao Moreno 已提交
807 808 809
	private async _revertChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
		const modifiedDocument = textEditor.document;
		const modifiedUri = modifiedDocument.uri;
J
Joao Moreno 已提交
810

J
Joao Moreno 已提交
811
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
812 813 814
			return;
		}

J
Joao Moreno 已提交
815 816
		const originalUri = toGitUri(modifiedUri, '~');
		const originalDocument = await workspace.openTextDocument(originalUri);
817 818
		const selectionsBeforeRevert = textEditor.selections;
		const visibleRangesBeforeRevert = textEditor.visibleRanges;
J
Joao Moreno 已提交
819
		const result = applyLineChanges(originalDocument, modifiedDocument, changes);
820

J
Joao Moreno 已提交
821 822 823
		const edit = new WorkspaceEdit();
		edit.replace(modifiedUri, new Range(new Position(0, 0), modifiedDocument.lineAt(modifiedDocument.lineCount - 1).range.end), result);
		workspace.applyEdit(edit);
824

J
Joao Moreno 已提交
825 826
		await modifiedDocument.save();

827 828
		textEditor.selections = selectionsBeforeRevert;
		textEditor.revealRange(visibleRangesBeforeRevert[0]);
J
Joao Moreno 已提交
829 830
	}

J
Joao Moreno 已提交
831 832
	@command('git.unstage')
	async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
833 834
		resourceStates = resourceStates.filter(s => !!s);

J
Joao Moreno 已提交
835
		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
836
			const resource = this.getSCMResource();
837 838 839 840 841 842 843 844

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

845
		const scmResources = resourceStates
J
Joao Moreno 已提交
846
			.filter(s => s instanceof Resource && s.resourceGroupType === ResourceGroupType.Index) as Resource[];
847

848
		if (!scmResources.length) {
J
Joao Moreno 已提交
849 850 851
			return;
		}

852
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
853
		await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
J
Joao Moreno 已提交
854 855
	}

J
Joao Moreno 已提交
856 857
	@command('git.unstageAll', { repository: true })
	async unstageAll(repository: Repository): Promise<void> {
858
		await repository.revert([]);
J
Joao Moreno 已提交
859
	}
J
Joao Moreno 已提交
860

J
Joao Moreno 已提交
861 862
	@command('git.unstageSelectedRanges', { diff: true })
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
863 864 865 866 867 868 869 870 871
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
		const modifiedUri = modifiedDocument.uri;

872 873 874 875 876 877 878
		if (modifiedUri.scheme !== 'git') {
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
J
Joao Moreno 已提交
879 880 881
			return;
		}

J
Joao Moreno 已提交
882
		const originalUri = toGitUri(modifiedUri, 'HEAD');
J
Joao Moreno 已提交
883
		const originalDocument = await workspace.openTextDocument(originalUri);
884 885 886 887
		const selectedLines = toLineRanges(textEditor.selections, modifiedDocument);
		const selectedDiffs = diffs
			.map(diff => selectedLines.reduce<LineChange | null>((result, range) => result || intersectDiffWithRange(modifiedDocument, diff, range), null))
			.filter(d => !!d) as LineChange[];
J
Joao Moreno 已提交
888 889 890 891 892

		if (!selectedDiffs.length) {
			return;
		}

893 894
		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
J
Joao Moreno 已提交
895

J
Joao Moreno 已提交
896
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
897 898
	}

J
Joao Moreno 已提交
899 900
	@command('git.clean')
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
901 902
		resourceStates = resourceStates.filter(s => !!s);

J
Joao Moreno 已提交
903
		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
904
			const resource = this.getSCMResource();
905 906 907 908 909 910 911 912

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

J
Joao Moreno 已提交
913
		const scmResources = resourceStates
J
Joao Moreno 已提交
914
			.filter(s => s instanceof Resource && s.resourceGroupType === ResourceGroupType.WorkingTree) as Resource[];
915

J
Joao Moreno 已提交
916
		if (!scmResources.length) {
J
Joao Moreno 已提交
917 918
			return;
		}
J
Joao Moreno 已提交
919

J
Joao Moreno 已提交
920
		const untrackedCount = scmResources.reduce((s, r) => s + (r.type === Status.UNTRACKED ? 1 : 0), 0);
J
Joao Moreno 已提交
921 922 923
		let message: string;
		let yes = localize('discard', "Discard Changes");

J
Joao Moreno 已提交
924
		if (scmResources.length === 1) {
J
Joao Moreno 已提交
925
			if (untrackedCount > 0) {
J
Joao Moreno 已提交
926
				message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
927 928
				yes = localize('delete file', "Delete file");
			} else {
J
Joao Moreno 已提交
929
				message = localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
930 931
			}
		} else {
J
Joao Moreno 已提交
932
			message = localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", scmResources.length);
J
Joao Moreno 已提交
933 934 935 936 937

			if (untrackedCount > 0) {
				message = `${message}\n\n${localize('warn untracked', "This will DELETE {0} untracked files!", untrackedCount)}`;
			}
		}
938

J
Joao Moreno 已提交
939
		const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
940

J
Joao Moreno 已提交
941 942 943 944
		if (pick !== yes) {
			return;
		}

J
Joao Moreno 已提交
945
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
946
		await this.runByRepository(resources, async (repository, resources) => repository.clean(resources));
J
Joao Moreno 已提交
947
	}
J
Joao Moreno 已提交
948

J
Joao Moreno 已提交
949 950
	@command('git.cleanAll', { repository: true })
	async cleanAll(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
951
		let resources = repository.workingTreeGroup.resourceStates;
J
Joao Moreno 已提交
952 953 954 955 956

		if (resources.length === 0) {
			return;
		}

J
Joao Moreno 已提交
957 958
		const trackedResources = resources.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
		const untrackedResources = resources.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);
J
Joao Moreno 已提交
959

J
Joao Moreno 已提交
960 961 962 963 964 965
		if (untrackedResources.length === 0) {
			const message = resources.length === 1
				? localize('confirm discard all single', "Are you sure you want to discard changes in {0}?", path.basename(resources[0].resourceUri.fsPath))
				: localize('confirm discard all', "Are you sure you want to discard ALL changes in {0} files?\nThis is IRREVERSIBLE!\nYour current working set will be FOREVER LOST.", resources.length);
			const yes = resources.length === 1
				? localize('discardAll multiple', "Discard 1 File")
J
Joao Moreno 已提交
966
				: localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
967
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
968

J
Joao Moreno 已提交
969
			if (pick !== yes) {
J
Joao Moreno 已提交
970 971 972
				return;
			}

J
Joao 已提交
973
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
974 975 976 977 978 979 980 981
			return;
		} else if (resources.length === 1) {
			const message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(resources[0].resourceUri.fsPath));
			const yes = localize('delete file', "Delete file");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

			if (pick !== yes) {
				return;
J
Joao Moreno 已提交
982 983
			}

J
Joao 已提交
984
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
985 986 987 988
		} else if (trackedResources.length === 0) {
			const message = localize('confirm delete multiple', "Are you sure you want to DELETE {0} files?", resources.length);
			const yes = localize('delete files', "Delete Files");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
989

J
Joao Moreno 已提交
990 991 992
			if (pick !== yes) {
				return;
			}
J
Joao Moreno 已提交
993

J
Joao 已提交
994
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
995

J
Joao Moreno 已提交
996 997 998 999
		} else { // resources.length > 1 && untrackedResources.length > 0 && trackedResources.length > 0
			const untrackedMessage = untrackedResources.length === 1
				? localize('there are untracked files single', "The following untracked file will be DELETED FROM DISK if discarded: {0}.", path.basename(untrackedResources[0].resourceUri.fsPath))
				: localize('there are untracked files', "There are {0} untracked files which will be DELETED FROM DISK if discarded.", untrackedResources.length);
J
Joao Moreno 已提交
1000

J
Joao Moreno 已提交
1001 1002 1003 1004 1005
			const message = localize('confirm discard all 2', "{0}\n\nThis is IRREVERSIBLE, your current working set will be FOREVER LOST.", untrackedMessage, resources.length);

			const yesTracked = trackedResources.length === 1
				? localize('yes discard tracked', "Discard 1 Tracked File", trackedResources.length)
				: localize('yes discard tracked multiple', "Discard {0} Tracked Files", trackedResources.length);
J
Joao Moreno 已提交
1006

J
Joao Moreno 已提交
1007
			const yesAll = localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
1008 1009 1010 1011 1012 1013 1014 1015
			const pick = await window.showWarningMessage(message, { modal: true }, yesTracked, yesAll);

			if (pick === yesTracked) {
				resources = trackedResources;
			} else if (pick !== yesAll) {
				return;
			}

J
Joao 已提交
1016
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
1017
		}
J
Joao Moreno 已提交
1018 1019
	}

J
Joao Moreno 已提交
1020
	private async smartCommit(
J
Joao Moreno 已提交
1021
		repository: Repository,
1022
		getCommitMessage: () => Promise<string | undefined>,
J
Joao Moreno 已提交
1023 1024
		opts?: CommitOptions
	): Promise<boolean> {
1025
		const config = workspace.getConfiguration('git', Uri.file(repository.root));
J
Joao Moreno 已提交
1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045
		const promptToSaveFilesBeforeCommit = config.get<boolean>('promptToSaveFilesBeforeCommit') === true;

		if (promptToSaveFilesBeforeCommit) {
			const unsavedTextDocuments = workspace.textDocuments
				.filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath));

			if (unsavedTextDocuments.length > 0) {
				const message = unsavedTextDocuments.length === 1
					? localize('unsaved files single', "The following file is unsaved: {0}.\n\nWould you like to save it before comitting?", path.basename(unsavedTextDocuments[0].uri.fsPath))
					: localize('unsaved files', "There are {0} unsaved files.\n\nWould you like to save them before comitting?", unsavedTextDocuments.length);
				const saveAndCommit = localize('save and commit', "Save All & Commit");
				const commit = localize('commit', "Commit Anyway");
				const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit);

				if (pick === saveAndCommit) {
					await Promise.all(unsavedTextDocuments.map(d => d.save()));
					await repository.status();
				} else if (pick !== commit) {
					return false; // do not commit on cancel
				}
1046 1047 1048
			}
		}

1049
		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
1050
		const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
J
Joao Moreno 已提交
1051 1052
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
1053 1054

		// no changes, and the user has not configured to commit all in this case
1055
		if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit) {
1056

J
Joao Moreno 已提交
1057 1058
			// prompt the user if we want to commit all or not
			const message = localize('no staged changes', "There are no staged changes to commit.\n\nWould you like to automatically stage all your changes and commit them directly?");
1059 1060 1061 1062 1063
			const yes = localize('yes', "Yes");
			const always = localize('always', "Always");
			const pick = await window.showWarningMessage(message, { modal: true }, yes, always);

			if (pick === always) {
J
Joao Moreno 已提交
1064 1065 1066
				config.update('enableSmartCommit', true, true);
			} else if (pick !== yes) {
				return false; // do not commit on cancel
1067 1068 1069
			}
		}

J
Joao Moreno 已提交
1070
		if (!opts) {
1071
			opts = { all: noStagedChanges };
J
Joao Moreno 已提交
1072 1073
		} else if (!opts.all && noStagedChanges) {
			opts = { ...opts, all: true };
J
Joao Moreno 已提交
1074 1075
		}

1076 1077 1078
		// enable signing of commits if configurated
		opts.signCommit = enableCommitSigning;

1079 1080 1081 1082
		if (config.get<boolean>('alwaysSignOff')) {
			opts.signoff = true;
		}

J
Joao Moreno 已提交
1083 1084
		if (
			// no changes
1085
			(noStagedChanges && noUnstagedChanges)
J
Joao Moreno 已提交
1086
			// or no staged changes and not `all`
1087
			|| (!opts.all && noStagedChanges)
J
Joao Moreno 已提交
1088
		) {
J
Joao Moreno 已提交
1089 1090 1091 1092
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
1093
		const message = await getCommitMessage();
J
Joao Moreno 已提交
1094 1095 1096 1097 1098

		if (!message) {
			return false;
		}

J
Joao Moreno 已提交
1099
		await repository.commit(message, opts);
J
Joao Moreno 已提交
1100 1101 1102 1103

		return true;
	}

J
Joao Moreno 已提交
1104
	private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
J
Joao Moreno 已提交
1105
		const message = repository.inputBox.value;
J
Joao Moreno 已提交
1106
		const getCommitMessage = async () => {
J
Joao Moreno 已提交
1107 1108 1109 1110
			if (message) {
				return message;
			}

1111 1112 1113 1114 1115 1116
			let value: string | undefined = undefined;

			if (opts && opts.amend && repository.HEAD && repository.HEAD.commit) {
				value = (await repository.getCommit(repository.HEAD.commit)).message;
			}

J
Joao Moreno 已提交
1117
			return await window.showInputBox({
1118
				value,
J
Joao Moreno 已提交
1119
				placeHolder: localize('commit message', "Commit message"),
J
Joao Moreno 已提交
1120 1121
				prompt: localize('provide commit message', "Please provide a commit message"),
				ignoreFocusOut: true
J
Joao Moreno 已提交
1122
			});
J
Joao Moreno 已提交
1123 1124
		};

J
Joao Moreno 已提交
1125
		const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
J
Joao Moreno 已提交
1126 1127

		if (message && didCommit) {
J
Joao Moreno 已提交
1128
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
1129
		}
J
Joao Moreno 已提交
1130 1131
	}

J
Joao Moreno 已提交
1132
	@command('git.commit', { repository: true })
J
Joao Moreno 已提交
1133 1134
	async commit(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository);
J
Joao Moreno 已提交
1135 1136
	}

J
Joao Moreno 已提交
1137
	@command('git.commitWithInput', { repository: true })
J
Joao Moreno 已提交
1138
	async commitWithInput(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1139
		if (!repository.inputBox.value) {
J
Joao Moreno 已提交
1140 1141 1142
			return;
		}

J
Joao Moreno 已提交
1143
		const didCommit = await this.smartCommit(repository, async () => repository.inputBox.value);
J
Joao Moreno 已提交
1144 1145

		if (didCommit) {
J
Joao Moreno 已提交
1146
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
1147
		}
J
Joao Moreno 已提交
1148 1149
	}

J
Joao Moreno 已提交
1150
	@command('git.commitStaged', { repository: true })
J
Joao Moreno 已提交
1151 1152
	async commitStaged(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false });
J
Joao Moreno 已提交
1153 1154
	}

J
Joao Moreno 已提交
1155
	@command('git.commitStagedSigned', { repository: true })
J
Joao Moreno 已提交
1156 1157
	async commitStagedSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, signoff: true });
J
Joao Moreno 已提交
1158 1159
	}

J
Joao Moreno 已提交
1160
	@command('git.commitStagedAmend', { repository: true })
J
Joao Moreno 已提交
1161
	async commitStagedAmend(repository: Repository): Promise<void> {
1162
		await this.commitWithAnyInput(repository, { all: false, amend: true });
K
Krzysztof Cieślak 已提交
1163 1164
	}

J
Joao Moreno 已提交
1165
	@command('git.commitAll', { repository: true })
J
Joao Moreno 已提交
1166 1167
	async commitAll(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true });
J
Joao Moreno 已提交
1168 1169
	}

J
Joao Moreno 已提交
1170
	@command('git.commitAllSigned', { repository: true })
J
Joao Moreno 已提交
1171 1172
	async commitAllSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, signoff: true });
J
Joao Moreno 已提交
1173 1174
	}

J
Joao Moreno 已提交
1175
	@command('git.commitAllAmend', { repository: true })
J
Joao Moreno 已提交
1176 1177
	async commitAllAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, amend: true });
K
Krzysztof Cieślak 已提交
1178 1179
	}

J
Joao Moreno 已提交
1180
	@command('git.undoCommit', { repository: true })
J
Joao Moreno 已提交
1181 1182
	async undoCommit(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
1183 1184 1185 1186 1187

		if (!HEAD || !HEAD.commit) {
			return;
		}

J
Joao Moreno 已提交
1188 1189
		const commit = await repository.getCommit('HEAD');
		await repository.reset('HEAD~');
J
Joao Moreno 已提交
1190
		repository.inputBox.value = commit.message;
J
Joao Moreno 已提交
1191 1192
	}

J
Joao Moreno 已提交
1193
	@command('git.checkout', { repository: true })
J
Joao Moreno 已提交
1194
	async checkout(repository: Repository, treeish: string): Promise<void> {
J
Joao Moreno 已提交
1195
		if (typeof treeish === 'string') {
J
Joao Moreno 已提交
1196
			return await repository.checkout(treeish);
J
Joao Moreno 已提交
1197 1198
		}

J
Joao Moreno 已提交
1199
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
1200
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
1201 1202 1203
		const includeTags = checkoutType === 'all' || checkoutType === 'tags';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1204
		const createBranch = new CreateBranchItem(this);
J
Joao Moreno 已提交
1205

J
Joao Moreno 已提交
1206
		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
1207 1208
			.map(ref => new CheckoutItem(ref));

J
Joao Moreno 已提交
1209
		const tags = (includeTags ? repository.refs.filter(ref => ref.type === RefType.Tag) : [])
J
Joao Moreno 已提交
1210 1211
			.map(ref => new CheckoutTagItem(ref));

J
Joao Moreno 已提交
1212
		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
1213 1214
			.map(ref => new CheckoutRemoteHeadItem(ref));

J
Joao Moreno 已提交
1215
		const picks = [createBranch, ...heads, ...tags, ...remoteHeads];
1216
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
J
Joao Moreno 已提交
1217
		const choice = await window.showQuickPick(picks, { placeHolder });
J
Joao Moreno 已提交
1218 1219 1220 1221 1222

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1223
		await choice.run(repository);
J
Joao Moreno 已提交
1224 1225
	}

J
Joao Moreno 已提交
1226
	@command('git.branch', { repository: true })
J
Joao Moreno 已提交
1227
	async branch(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1228
		const result = await window.showInputBox({
J
Joao Moreno 已提交
1229
			placeHolder: localize('branch name', "Branch name"),
J
Joao Moreno 已提交
1230 1231
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true
J
Joao Moreno 已提交
1232
		});
J
Joao Moreno 已提交
1233

J
Joao Moreno 已提交
1234 1235 1236
		if (!result) {
			return;
		}
J
Joao Moreno 已提交
1237

J
Joao Moreno 已提交
1238
		const name = result.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-');
J
Joao Moreno 已提交
1239
		await repository.branch(name);
J
Joao Moreno 已提交
1240 1241
	}

J
Joao Moreno 已提交
1242
	@command('git.deleteBranch', { repository: true })
J
Joao Moreno 已提交
1243
	async deleteBranch(repository: Repository, name: string, force?: boolean): Promise<void> {
1244 1245
		let run: (force?: boolean) => Promise<void>;
		if (typeof name === 'string') {
J
Joao Moreno 已提交
1246
			run = force => repository.deleteBranch(name, force);
1247
		} else {
J
Joao Moreno 已提交
1248 1249
			const currentHead = repository.HEAD && repository.HEAD.name;
			const heads = repository.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead)
1250
				.map(ref => new BranchDeleteItem(ref));
M
Maik Riechert 已提交
1251

M
Maik Riechert 已提交
1252
			const placeHolder = localize('select branch to delete', 'Select a branch to delete');
1253
			const choice = await window.showQuickPick<BranchDeleteItem>(heads, { placeHolder });
M
Maik Riechert 已提交
1254

M
Maik Riechert 已提交
1255
			if (!choice || !choice.branchName) {
1256 1257
				return;
			}
M
Maik Riechert 已提交
1258
			name = choice.branchName;
J
Joao Moreno 已提交
1259
			run = force => choice.run(repository, force);
M
Maik Riechert 已提交
1260 1261
		}

1262 1263 1264 1265 1266 1267 1268 1269 1270
		try {
			await run(force);
		} catch (err) {
			if (err.gitErrorCode !== GitErrorCodes.BranchNotFullyMerged) {
				throw err;
			}

			const message = localize('confirm force delete branch', "The branch '{0}' is not fully merged. Delete anyway?", name);
			const yes = localize('delete branch', "Delete Branch");
1271
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
1272 1273 1274 1275 1276

			if (pick === yes) {
				await run(true);
			}
		}
M
Maik Riechert 已提交
1277 1278
	}

1279 1280
	@command('git.renameBranch', { repository: true })
	async renameBranch(repository: Repository): Promise<void> {
J
Justin Horner 已提交
1281 1282 1283 1284 1285 1286 1287 1288
		const placeHolder = localize('provide branch name', "Please provide a branch name");
		const name = await window.showInputBox({ placeHolder });

		if (!name || name.trim().length === 0) {
			return;
		}

		try {
1289
			await repository.renameBranch(name);
J
Justin Horner 已提交
1290 1291 1292
		} catch (err) {
			switch (err.gitErrorCode) {
				case GitErrorCodes.InvalidBranchName:
1293 1294
					window.showErrorMessage(localize('invalid branch name', 'Invalid branch name'));
					return;
J
Justin Horner 已提交
1295
				case GitErrorCodes.BranchAlreadyExists:
J
Joao Moreno 已提交
1296
					window.showErrorMessage(localize('branch already exists', "A branch named '{0}' already exists", name));
1297 1298 1299
					return;
				default:
					throw err;
J
Justin Horner 已提交
1300 1301 1302 1303
			}
		}
	}

J
Joao Moreno 已提交
1304
	@command('git.merge', { repository: true })
J
Joao Moreno 已提交
1305
	async merge(repository: Repository): Promise<void> {
1306 1307 1308 1309
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string>('checkoutType') || 'all';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1310
		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
1311 1312
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1313

J
Joao Moreno 已提交
1314
		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
1315 1316
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1317 1318

		const picks = [...heads, ...remoteHeads];
1319 1320
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
1321 1322 1323 1324 1325

		if (!choice) {
			return;
		}

1326
		await choice.run(repository);
1327 1328
	}

J
Joao Moreno 已提交
1329
	@command('git.createTag', { repository: true })
J
Joao Moreno 已提交
1330
	async createTag(repository: Repository): Promise<void> {
1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342
		const inputTagName = await window.showInputBox({
			placeHolder: localize('tag name', "Tag name"),
			prompt: localize('provide tag name', "Please provide a tag name"),
			ignoreFocusOut: true
		});

		if (!inputTagName) {
			return;
		}

		const inputMessage = await window.showInputBox({
			placeHolder: localize('tag message', "Message"),
J
Joao Moreno 已提交
1343
			prompt: localize('provide tag message', "Please provide a message to annotate the tag"),
1344 1345 1346 1347 1348
			ignoreFocusOut: true
		});

		const name = inputTagName.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-');
		const message = inputMessage || name;
J
Joao Moreno 已提交
1349
		await repository.tag(name, message);
1350 1351
	}

J
Joao Moreno 已提交
1352 1353 1354 1355 1356 1357 1358 1359 1360 1361
	@command('git.fetch', { repository: true })
	async fetch(repository: Repository): Promise<void> {
		if (repository.remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
			return;
		}

		await repository.fetch();
	}

J
Joao Moreno 已提交
1362
	@command('git.pullFrom', { repository: true })
J
Joao Moreno 已提交
1363 1364
	async pullFrom(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1365 1366 1367 1368 1369 1370

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from."));
			return;
		}

1371
		const remotePicks = remotes.filter(r => r.fetchUrl !== undefined).map(r => ({ label: r.name, description: r.fetchUrl! }));
1372
		const placeHolder = localize('pick remote pull repo', "Pick a remote to pull the branch from");
D
Dozed12 已提交
1373
		const remotePick = await window.showQuickPick(remotePicks, { placeHolder });
1374

D
Dozed12 已提交
1375
		if (!remotePick) {
1376 1377 1378
			return;
		}

D
Dozed12 已提交
1379 1380
		const remoteRefs = repository.refs;
		const remoteRefsFiltered = remoteRefs.filter(r => (r.remote === remotePick.label));
J
Joao Moreno 已提交
1381
		const branchPicks = remoteRefsFiltered.map(r => ({ label: r.name })) as { label: string; description: string }[];
D
Dozed12 已提交
1382
		const branchPick = await window.showQuickPick(branchPicks, { placeHolder });
1383

D
Dozed12 已提交
1384
		if (!branchPick) {
1385 1386 1387
			return;
		}

F
Francisco Moreira 已提交
1388 1389
		const remoteCharCnt = remotePick.label.length;

1390
		await repository.pullFrom(false, remotePick.label, branchPick.label.slice(remoteCharCnt + 1));
1391 1392
	}

J
Joao Moreno 已提交
1393
	@command('git.pull', { repository: true })
J
Joao Moreno 已提交
1394 1395
	async pull(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1396 1397 1398 1399 1400 1401

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from."));
			return;
		}

J
Joao Moreno 已提交
1402
		await repository.pull(repository.HEAD);
J
Joao Moreno 已提交
1403 1404
	}

J
Joao Moreno 已提交
1405
	@command('git.pullRebase', { repository: true })
J
Joao Moreno 已提交
1406 1407
	async pullRebase(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1408 1409 1410 1411 1412 1413

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from."));
			return;
		}

J
Joao Moreno 已提交
1414
		await repository.pullWithRebase(repository.HEAD);
J
Joao Moreno 已提交
1415 1416
	}

J
Joao Moreno 已提交
1417
	@command('git.push', { repository: true })
J
Joao Moreno 已提交
1418 1419
	async push(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1420 1421 1422 1423 1424 1425

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
			return;
		}

1426 1427 1428 1429 1430 1431
		if (!repository.HEAD || !repository.HEAD.name) {
			window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			return;
		}

		try {
J
Joao Moreno 已提交
1432
			await repository.push(repository.HEAD);
1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446
		} catch (err) {
			if (err.gitErrorCode !== GitErrorCodes.NoUpstreamBranch) {
				throw err;
			}

			const branchName = repository.HEAD.name;
			const message = localize('confirm publish branch', "The branch '{0}' has no upstream branch. Would you like to publish this branch?", branchName);
			const yes = localize('ok', "OK");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

			if (pick === yes) {
				await this.publish(repository);
			}
		}
J
Joao Moreno 已提交
1447 1448
	}

J
Joao Moreno 已提交
1449
	@command('git.pushWithTags', { repository: true })
J
Joao Moreno 已提交
1450 1451
	async pushWithTags(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1452 1453 1454 1455 1456 1457

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
			return;
		}

J
Joao Moreno 已提交
1458
		await repository.pushTags();
1459 1460 1461 1462

		window.showInformationMessage(localize('push with tags success', "Successfully pushed with tags."));
	}

J
Joao Moreno 已提交
1463
	@command('git.pushTo', { repository: true })
J
Joao Moreno 已提交
1464 1465
	async pushTo(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1466 1467 1468 1469 1470 1471

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
			return;
		}

J
Joao Moreno 已提交
1472
		if (!repository.HEAD || !repository.HEAD.name) {
J
Joao Moreno 已提交
1473 1474 1475 1476
			window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			return;
		}

J
Joao Moreno 已提交
1477
		const branchName = repository.HEAD.name;
1478
		const picks = remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl! }));
J
Joao Moreno 已提交
1479 1480 1481 1482 1483 1484 1485
		const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
		const pick = await window.showQuickPick(picks, { placeHolder });

		if (!pick) {
			return;
		}

1486
		await repository.pushTo(pick.label, branchName);
J
Joao Moreno 已提交
1487 1488
	}

J
Joao Moreno 已提交
1489
	private async _sync(repository: Repository, rebase: boolean): Promise<void> {
J
Joao Moreno 已提交
1490
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
1491 1492 1493 1494 1495

		if (!HEAD || !HEAD.upstream) {
			return;
		}

J
Joao Moreno 已提交
1496 1497 1498 1499
		const remoteName = HEAD.remote || HEAD.upstream.remote;
		const remote = repository.remotes.find(r => r.name === remoteName);
		const isReadonly = remote && remote.isReadOnly;

J
Joao Moreno 已提交
1500
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
1501
		const shouldPrompt = !isReadonly && config.get<boolean>('confirmSync') === true;
J
Joao Moreno 已提交
1502 1503

		if (shouldPrompt) {
J
Joao Moreno 已提交
1504
			const message = localize('sync is unpredictable', "This action will push and pull commits to and from '{0}/{1}'.", HEAD.upstream.remote, HEAD.upstream.name);
J
Joao Moreno 已提交
1505
			const yes = localize('ok', "OK");
B
Benjamin Pasero 已提交
1506
			const neverAgain = localize('never again', "OK, Don't Show Again");
J
Joao Moreno 已提交
1507 1508 1509 1510 1511 1512 1513 1514 1515
			const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);

			if (pick === neverAgain) {
				await config.update('confirmSync', false, true);
			} else if (pick !== yes) {
				return;
			}
		}

J
Joao Moreno 已提交
1516
		if (rebase) {
J
Joao Moreno 已提交
1517
			await repository.syncRebase(HEAD);
J
Joao Moreno 已提交
1518
		} else {
J
Joao Moreno 已提交
1519
			await repository.sync(HEAD);
J
Joao Moreno 已提交
1520 1521 1522 1523 1524 1525
		}
	}

	@command('git.sync', { repository: true })
	sync(repository: Repository): Promise<void> {
		return this._sync(repository, false);
J
Joao Moreno 已提交
1526 1527
	}

J
Joao 已提交
1528 1529 1530 1531 1532 1533 1534 1535 1536
	@command('git._syncAll')
	async syncAll(): Promise<void> {
		await Promise.all(this.model.repositories.map(async repository => {
			const HEAD = repository.HEAD;

			if (!HEAD || !HEAD.upstream) {
				return;
			}

J
Joao Moreno 已提交
1537
			await repository.sync(HEAD);
J
Joao 已提交
1538 1539 1540
		}));
	}

1541
	@command('git.syncRebase', { repository: true })
J
Joao Moreno 已提交
1542 1543
	syncRebase(repository: Repository): Promise<void> {
		return this._sync(repository, true);
J
Joao Moreno 已提交
1544 1545
	}

J
Joao Moreno 已提交
1546
	@command('git.publish', { repository: true })
J
Joao Moreno 已提交
1547 1548
	async publish(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1549 1550 1551 1552 1553 1554

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to publish', "Your repository has no remotes configured to publish to."));
			return;
		}

J
Joao Moreno 已提交
1555
		const branchName = repository.HEAD && repository.HEAD.name || '';
M
Markus Wolf 已提交
1556 1557 1558 1559 1560 1561
		const selectRemote = async () => {
			const picks = repository.remotes.map(r => r.name);
			const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
			return await window.showQuickPick(picks, { placeHolder });
		};
		const choice = remotes.length === 1 ? remotes[0].name : await selectRemote();
J
Joao Moreno 已提交
1562 1563 1564 1565 1566

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1567
		await repository.pushTo(choice, branchName, true);
J
Joao Moreno 已提交
1568 1569
	}

1570 1571
	@command('git.ignore')
	async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
1572 1573
		resourceStates = resourceStates.filter(s => !!s);

J
Joao Moreno 已提交
1574
		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
1575
			const resource = this.getSCMResource();
N
NKumar2 已提交
1576

1577
			if (!resource) {
J
Joao Moreno 已提交
1578 1579 1580
				return;
			}

1581
			resourceStates = [resource];
J
Joao Moreno 已提交
1582 1583
		}

1584
		const resources = resourceStates
J
Joao Moreno 已提交
1585 1586 1587
			.filter(s => s instanceof Resource)
			.map(r => r.resourceUri);

1588
		if (!resources.length) {
N
NKumar2 已提交
1589 1590 1591
			return;
		}

1592
		await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
N
NKumar2 已提交
1593 1594
	}

J
Joao Moreno 已提交
1595
	private async _stash(repository: Repository, includeUntracked = false): Promise<void> {
1596 1597
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
1598

1599
		if (noUnstagedChanges && noStagedChanges) {
K
Krzysztof Cieślak 已提交
1600 1601 1602
			window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
			return;
		}
J
Joao Moreno 已提交
1603

1604
		const message = await this.getStashMessage();
J
Joao Moreno 已提交
1605 1606 1607 1608 1609

		if (typeof message === 'undefined') {
			return;
		}

J
Joao Moreno 已提交
1610
		await repository.createStash(message, includeUntracked);
K
Krzysztof Cieślak 已提交
1611 1612
	}

1613 1614 1615 1616 1617 1618 1619
	private async getStashMessage(): Promise<string | undefined> {
		return await window.showInputBox({
			prompt: localize('provide stash message', "Optionally provide a stash message"),
			placeHolder: localize('stash message', "Stash message")
		});
	}

J
Joao Moreno 已提交
1620 1621 1622 1623 1624 1625 1626 1627 1628 1629
	@command('git.stash', { repository: true })
	stash(repository: Repository): Promise<void> {
		return this._stash(repository);
	}

	@command('git.stashIncludeUntracked', { repository: true })
	stashIncludeUntracked(repository: Repository): Promise<void> {
		return this._stash(repository, true);
	}

J
Joao Moreno 已提交
1630
	@command('git.stashPop', { repository: true })
J
Joao Moreno 已提交
1631 1632
	async stashPop(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
1633 1634

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1635 1636 1637 1638
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

J
Joao Moreno 已提交
1639 1640
		const picks = stashes.map(r => ({ label: `#${r.index}:  ${r.description}`, description: '', details: '', id: r.index }));
		const placeHolder = localize('pick stash to pop', "Pick a stash to pop");
K
Krzysztof Cieślak 已提交
1641 1642 1643 1644 1645
		const choice = await window.showQuickPick(picks, { placeHolder });

		if (!choice) {
			return;
		}
J
Joao Moreno 已提交
1646

J
Joao Moreno 已提交
1647
		await repository.popStash(choice.id);
K
Krzysztof Cieślak 已提交
1648 1649
	}

J
Joao Moreno 已提交
1650
	@command('git.stashPopLatest', { repository: true })
J
Joao Moreno 已提交
1651 1652
	async stashPopLatest(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
1653 1654

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1655 1656 1657 1658
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

J
Joao Moreno 已提交
1659
		await repository.popStash();
J
Joao Moreno 已提交
1660
	}
K
Krzysztof Cieślak 已提交
1661

J
Joao Moreno 已提交
1662
	private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any {
1663
		const result = (...args: any[]) => {
J
Joao Moreno 已提交
1664 1665
			let result: Promise<any>;

J
Joao Moreno 已提交
1666
			if (!options.repository) {
J
Joao Moreno 已提交
1667 1668
				result = Promise.resolve(method.apply(this, args));
			} else {
J
Joao Moreno 已提交
1669 1670
				// try to guess the repository based on the first argument
				const repository = this.model.getRepository(args[0]);
J
Joao Moreno 已提交
1671 1672 1673 1674 1675 1676 1677 1678 1679
				let repositoryPromise: Promise<Repository | undefined>;

				if (repository) {
					repositoryPromise = Promise.resolve(repository);
				} else if (this.model.repositories.length === 1) {
					repositoryPromise = Promise.resolve(this.model.repositories[0]);
				} else {
					repositoryPromise = this.model.pickRepository();
				}
1680

J
Joao Moreno 已提交
1681
				result = repositoryPromise.then(repository => {
J
Joao Moreno 已提交
1682
					if (!repository) {
J
Joao 已提交
1683
						return Promise.resolve();
J
Joao Moreno 已提交
1684 1685
					}

J
Joao Moreno 已提交
1686
					return Promise.resolve(method.apply(this, [repository, ...args]));
J
Joao Moreno 已提交
1687
				});
J
Joao Moreno 已提交
1688 1689
			}

K
kieferrm 已提交
1690
			/* __GDPR__
K
kieferrm 已提交
1691 1692 1693 1694
				"git.command" : {
					"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
J
Joao Moreno 已提交
1695 1696
			this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

J
Joao Moreno 已提交
1697
			return result.catch(async err => {
J
Joao Moreno 已提交
1698
				const options: MessageOptions = {
1699
					modal: true
J
Joao Moreno 已提交
1700 1701
				};

J
Joao Moreno 已提交
1702
				let message: string;
1703
				let type: 'error' | 'warning' = 'error';
J
Joao Moreno 已提交
1704 1705

				switch (err.gitErrorCode) {
1706
					case GitErrorCodes.DirtyWorkTree:
J
Joao Moreno 已提交
1707 1708
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
1709
					case GitErrorCodes.PushRejected:
J
Joao Moreno 已提交
1710
						message = localize('cant push', "Can't push refs to remote. Try running 'Pull' first to integrate your changes.");
1711
						break;
1712 1713 1714 1715 1716
					case GitErrorCodes.Conflict:
						message = localize('merge conflicts', "There are merge conflicts. Resolve them before committing.");
						type = 'warning';
						options.modal = false;
						break;
J
Joao Moreno 已提交
1717
					default:
1718 1719 1720
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
J
Joao Moreno 已提交
1721
							.split(/[\r\n]/)
J
João Moreno 已提交
1722
							.filter((line: string) => !!line)
1723 1724 1725 1726 1727
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738

						break;
				}

				if (!message) {
					console.error(err);
					return;
				}

				const outputChannel = this.outputChannel as OutputChannel;
				const openOutputChannelChoice = localize('open git log', "Open Git Log");
1739 1740 1741
				const choice = type === 'error'
					? await window.showErrorMessage(message, options, openOutputChannelChoice)
					: await window.showWarningMessage(message, options, openOutputChannelChoice);
J
Joao Moreno 已提交
1742 1743 1744 1745 1746 1747

				if (choice === openOutputChannelChoice) {
					outputChannel.show();
				}
			});
		};
1748 1749

		// patch this object, so people can call methods directly
1750
		(this as any)[key] = result;
1751 1752

		return result;
J
Joao Moreno 已提交
1753 1754
	}

1755
	private getSCMResource(uri?: Uri): Resource | undefined {
J
Joao Moreno 已提交
1756
		uri = uri ? uri : (window.activeTextEditor && window.activeTextEditor.document.uri);
J
Joao Moreno 已提交
1757

J
Joao Moreno 已提交
1758
		this.outputChannel.appendLine(`git.getSCMResource.uri ${uri && uri.toString()}`);
J
Joao Moreno 已提交
1759 1760 1761
		for (const r of this.model.repositories.map(r => r.root)) {
			this.outputChannel.appendLine(`repo root ${r}`);
		}
J
Joao Moreno 已提交
1762

J
Joao Moreno 已提交
1763
		if (!uri) {
1764
			return undefined;
J
Joao Moreno 已提交
1765 1766 1767
		}

		if (uri.scheme === 'git') {
J
Joao Moreno 已提交
1768 1769
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
1770 1771 1772 1773
		}

		if (uri.scheme === 'file') {
			const uriString = uri.toString();
J
Joao Moreno 已提交
1774
			const repository = this.model.getRepository(uri);
1775

J
Joao Moreno 已提交
1776
			if (!repository) {
1777 1778
				return undefined;
			}
J
Joao Moreno 已提交
1779

J
Joao Moreno 已提交
1780 1781
			return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
				|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
J
Joao Moreno 已提交
1782 1783 1784
		}
	}

J
Joao Moreno 已提交
1785
	private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;
J
Joao Moreno 已提交
1786 1787 1788 1789 1790 1791
	private runByRepository<T>(resources: Uri[], fn: (repository: Repository, resources: Uri[]) => Promise<T>): Promise<T[]>;
	private async runByRepository<T>(arg: Uri | Uri[], fn: (repository: Repository, resources: any) => Promise<T>): Promise<T[]> {
		const resources = arg instanceof Uri ? [arg] : arg;
		const isSingleResource = arg instanceof Uri;

		const groups = resources.reduce((result, resource) => {
1792
			let repository = this.model.getRepository(resource);
J
Joao Moreno 已提交
1793 1794 1795 1796 1797 1798

			if (!repository) {
				console.warn('Could not find git repository for ', resource);
				return result;
			}

1799
			// Could it be a submodule?
1800
			if (pathEquals(resource.fsPath, repository.root)) {
1801 1802 1803
				repository = this.model.getRepositoryForSubmodule(resource) || repository;
			}

J
Joao Moreno 已提交
1804
			const tuple = result.filter(p => p.repository === repository)[0];
J
Joao Moreno 已提交
1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820

			if (tuple) {
				tuple.resources.push(resource);
			} else {
				result.push({ repository, resources: [resource] });
			}

			return result;
		}, [] as { repository: Repository, resources: Uri[] }[]);

		const promises = groups
			.map(({ repository, resources }) => fn(repository as Repository, isSingleResource ? resources[0] : resources));

		return Promise.all(promises);
	}

J
Joao Moreno 已提交
1821 1822 1823
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
João Moreno 已提交
1824
}