commands.ts 64.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 { Git, CommitOptions, Stash, ForcePushMode } from './git';
J
Joao Moreno 已提交
10
import { Repository, Resource, Status, 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
import * as nls from 'vscode-nls';
J
Joao Moreno 已提交
20
import { Ref, RefType, Branch, GitErrorCodes } from './api/git';
J
Joao Moreno 已提交
21 22

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

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

	protected get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); }
	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> {
33
		const ref = this.ref.name;
J
Joao Moreno 已提交
34 35 36 37 38

		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
	async run(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
57 58 59 60
		if (!this.ref.name) {
			return;
		}

61
		await repository.checkoutTracking(this.ref.name);
J
Joao Moreno 已提交
62 63 64
	}
}

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

67 68 69
	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 已提交
70 71
	get description(): string { return this.shortCommit; }

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

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

82 83 84 85 86 87
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 已提交
88

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

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

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

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

101 102
	get shouldAlwaysShow(): boolean { return true; }

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

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

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

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

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

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

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

141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
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 };
}

157 158 159 160 161 162 163 164 165 166 167
enum PushType {
	Push,
	PushTo,
	PushTags,
}

interface PushOptions {
	pushType: PushType;
	forcePush?: boolean;
}

J
Joao Moreno 已提交
168
export class CommandCenter {
J
Joao Moreno 已提交
169 170

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

J
Joao Moreno 已提交
172
	constructor(
J
Joao Moreno 已提交
173
		private git: Git,
J
Joao Moreno 已提交
174
		private model: Model,
J
Joao Moreno 已提交
175 176
		private outputChannel: OutputChannel,
		private telemetryReporter: TelemetryReporter
J
Joao Moreno 已提交
177
	) {
J
Joao Moreno 已提交
178 179
		this.disposables = Commands.map(({ commandId, key, method, options }) => {
			const command = this.createCommand(commandId, key, method, options);
180

J
Joao Moreno 已提交
181
			if (options.diff) {
J
Joao Moreno 已提交
182 183 184 185 186
				return commands.registerDiffInformationCommand(commandId, command);
			} else {
				return commands.registerCommand(commandId, command);
			}
		});
J
Joao Moreno 已提交
187 188
	}

J
Joao Moreno 已提交
189 190
	@command('git.refresh', { repository: true })
	async refresh(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
191
		await repository.status();
J
Joao Moreno 已提交
192
	}
J
Joao Moreno 已提交
193

J
Joao Moreno 已提交
194 195
	@command('git.openResource')
	async openResource(resource: Resource): Promise<void> {
J
Joao 已提交
196
		await this._openResource(resource, undefined, true, false);
J
Joao Moreno 已提交
197 198
	}

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

202 203 204 205
		try {
			stat = await new Promise<Stats>((c, e) => lstat(resource.resourceUri.fsPath, (err, stat) => err ? e(err) : c(stat)));
		} catch (err) {
			// noop
206
		}
207

208 209 210 211
		let left: Uri | undefined;
		let right: Uri | undefined;

		if (stat && stat.isDirectory()) {
212 213 214 215
			const repository = this.model.getRepositoryForSubmodule(resource.resourceUri);

			if (repository) {
				right = toGitUri(resource.resourceUri, resource.resourceGroupType === ResourceGroupType.Index ? 'index' : 'wt', { submoduleOf: repository.root });
216 217 218 219 220
			}
		} else {
			left = await this.getLeftResource(resource);
			right = await this.getRightResource(resource);
		}
221

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

J
Joao Moreno 已提交
224 225 226 227 228
		if (!right) {
			// TODO
			console.error('oh no');
			return;
		}
J
Joao Moreno 已提交
229

J
Joao Moreno 已提交
230
		const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
231 232
			preserveFocus,
			preview,
233
			viewColumn: ViewColumn.Active
J
Joao Moreno 已提交
234 235
		};

J
Joao Moreno 已提交
236 237
		const activeTextEditor = window.activeTextEditor;

238 239 240
		// 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 已提交
241 242 243
			opts.selection = activeTextEditor.selection;
		}

244
		if (!left) {
J
Joao Moreno 已提交
245 246 247 248 249
			await commands.executeCommand<void>('vscode.open', right, opts);
		} else {
			await commands.executeCommand<void>('vscode.diff', left, right, title, opts);
		}
	}
J
Joao Moreno 已提交
250

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

J
Joao Moreno 已提交
254 255 256
		if (!repository) {
			return toGitUri(uri, ref);
		}
J
Joao Moreno 已提交
257

J
Joao Moreno 已提交
258
		try {
J
Joao Moreno 已提交
259 260 261
			let gitRef = ref;

			if (gitRef === '~') {
J
Joao Moreno 已提交
262
				const uriString = uri.toString();
J
Joao Moreno 已提交
263
				const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
J
Joao Moreno 已提交
264
				gitRef = indexStatus ? '' : 'HEAD';
J
Joao Moreno 已提交
265 266
			}

J
Joao Moreno 已提交
267
			const { size, object } = await repository.getObjectDetails(gitRef, uri.fsPath);
J
Joao Moreno 已提交
268
			const { mimetype } = await repository.detectObjectType(object);
J
Joao Moreno 已提交
269

J
Joao Moreno 已提交
270 271 272
			if (mimetype === 'text/plain') {
				return toGitUri(uri, ref);
			}
273

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

J
Joao Moreno 已提交
278
			if (ImageMimetypes.indexOf(mimetype) > -1) {
J
Joao Moreno 已提交
279 280
				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 已提交
281 282
			}

J
Joao Moreno 已提交
283
			return Uri.parse(`data:;label:${path.basename(uri.fsPath)};description:${gitRef},`);
J
Joao Moreno 已提交
284 285 286 287

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

J
Joao Moreno 已提交
290
	private async getLeftResource(resource: Resource): Promise<Uri | undefined> {
J
Joao Moreno 已提交
291 292 293
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
J
Joao Moreno 已提交
294
				return this.getURI(resource.original, 'HEAD');
J
Joao Moreno 已提交
295 296

			case Status.MODIFIED:
J
Joao Moreno 已提交
297
				return this.getURI(resource.resourceUri, '~');
298 299

			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
300
				return this.getURI(resource.resourceUri, '');
J
Joao Moreno 已提交
301
		}
J
Joao Moreno 已提交
302
	}
J
Joao Moreno 已提交
303

J
Joao Moreno 已提交
304
	private async getRightResource(resource: Resource): Promise<Uri | undefined> {
J
Joao Moreno 已提交
305 306 307 308 309
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_ADDED:
			case Status.INDEX_COPIED:
			case Status.INDEX_RENAMED:
J
Joao Moreno 已提交
310
				return this.getURI(resource.resourceUri, '');
J
Joao Moreno 已提交
311 312

			case Status.INDEX_DELETED:
313
			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
314
			case Status.DELETED:
J
Joao Moreno 已提交
315
				return this.getURI(resource.resourceUri, 'HEAD');
J
Joao Moreno 已提交
316 317 318 319

			case Status.MODIFIED:
			case Status.UNTRACKED:
			case Status.IGNORED:
J
Joao Moreno 已提交
320 321 322 323 324 325
				const repository = this.model.getRepository(resource.resourceUri);

				if (!repository) {
					return;
				}

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

J
Joao Moreno 已提交
329 330
				if (indexStatus && indexStatus.renameResourceUri) {
					return indexStatus.renameResourceUri;
J
Joao Moreno 已提交
331 332
				}

J
Joao Moreno 已提交
333
				return resource.resourceUri;
J
Joao Moreno 已提交
334

335
			case Status.BOTH_ADDED:
J
Joao Moreno 已提交
336
			case Status.BOTH_MODIFIED:
J
Joao Moreno 已提交
337
				return resource.resourceUri;
J
Joao Moreno 已提交
338 339 340 341
		}
	}

	private getTitle(resource: Resource): string {
J
Joao Moreno 已提交
342
		const basename = path.basename(resource.resourceUri.fsPath);
J
Joao Moreno 已提交
343 344 345 346

		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
M
Marc Kassay 已提交
347
			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
348 349 350
				return `${basename} (Index)`;

			case Status.MODIFIED:
M
Marc Kassay 已提交
351 352
			case Status.BOTH_ADDED:
			case Status.BOTH_MODIFIED:
J
Joao Moreno 已提交
353 354 355 356 357 358
				return `${basename} (Working Tree)`;
		}

		return '';
	}

J
Joao Moreno 已提交
359
	@command('git.clone')
360 361 362 363 364 365 366
	async clone(url?: string): Promise<void> {
		if (!url) {
			url = await window.showInputBox({
				prompt: localize('repourl', "Repository URL"),
				ignoreFocusOut: true
			});
		}
J
Joao Moreno 已提交
367 368

		if (!url) {
K
kieferrm 已提交
369
			/* __GDPR__
K
kieferrm 已提交
370 371 372 373
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
374 375
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
			return;
J
Joao Moreno 已提交
376 377
		}

378
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
379 380 381 382 383 384 385 386 387
		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 已提交
388 389
		});

J
Joao Moreno 已提交
390
		if (!uris || uris.length === 0) {
K
kieferrm 已提交
391
			/* __GDPR__
K
kieferrm 已提交
392 393 394 395
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
396 397
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
			return;
J
Joao Moreno 已提交
398 399
		}

J
Joao Moreno 已提交
400 401 402
		const uri = uris[0];
		const parentPath = uri.fsPath;

403
		try {
404 405 406 407 408
			const opts = {
				location: ProgressLocation.Notification,
				title: localize('cloning', "Cloning git repository '{0}'...", url),
				cancellable: true
			};
M
Maryam Archie 已提交
409

410 411
			const repositoryPath = await window.withProgress(
				opts,
J
Joao Moreno 已提交
412
				(_, token) => this.git.clone(url!, parentPath, token)
413
			);
414

415 416
			const choices = [];
			let message = localize('proposeopen', "Would you like to open the cloned repository?");
417
			const open = localize('openrepo', "Open Repository");
418 419 420 421 422 423 424 425 426
			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);
427 428

			const openFolder = result === open;
K
kieferrm 已提交
429
			/* __GDPR__
K
kieferrm 已提交
430 431 432 433 434
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
					"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
				}
			*/
435
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: openFolder ? 1 : 0 });
436 437 438

			const uri = Uri.file(repositoryPath);

439
			if (openFolder) {
440 441 442
				commands.executeCommand('vscode.openFolder', uri);
			} else if (result === addToWorkspace) {
				workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
443
			}
444 445
		} catch (err) {
			if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
K
kieferrm 已提交
446
				/* __GDPR__
K
kieferrm 已提交
447 448 449 450
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
451
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
J
Joao Moreno 已提交
452 453
			} else if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
				return;
454
			} else {
K
kieferrm 已提交
455
				/* __GDPR__
K
kieferrm 已提交
456 457 458 459
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
460
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
461
			}
J
Joao Moreno 已提交
462

463
			throw err;
J
Joao Moreno 已提交
464 465 466
		}
	}

J
Joao Moreno 已提交
467
	@command('git.init')
J
Joao Moreno 已提交
468
	async init(): Promise<void> {
J
Joao Moreno 已提交
469
		let repositoryPath: string | undefined;
J
Joao Moreno 已提交
470

J
Joao Moreno 已提交
471 472 473 474 475 476 477 478
		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 已提交
479

J
Joao Moreno 已提交
480
			repositoryPath = item.folder.uri.fsPath;
J
Joao Moreno 已提交
481
		}
J
Joao Moreno 已提交
482

J
Joao Moreno 已提交
483
		if (!repositoryPath) {
J
Joao Moreno 已提交
484 485 486 487 488 489 490 491 492 493 494 495
			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 已提交
496

J
Joao Moreno 已提交
497
			if (!result || result.length === 0) {
J
Joao Moreno 已提交
498 499
				return;
			}
J
Joao Moreno 已提交
500 501 502 503 504 505 506 507 508 509 510 511

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

J
Joao Moreno 已提交
512
			repositoryPath = uri.fsPath;
J
Joao Moreno 已提交
513 514
		}

J
Joao Moreno 已提交
515
		await this.git.init(repositoryPath);
I
Ivan Sučić 已提交
516 517

		const choices = [];
J
Joao Moreno 已提交
518 519
		let message = localize('proposeopen init', "Would you like to open the initialized repository?");
		const open = localize('openrepo', "Open Repository");
I
Ivan Sučić 已提交
520
		choices.push(open);
J
Joao Moreno 已提交
521 522 523 524 525 526

		const addToWorkspace = localize('add', "Add to Workspace");
		if (workspace.workspaceFolders) {
			message = localize('proposeopen2 init', "Would you like to open the initialized repository, or add it to the current workspace?");
			choices.push(addToWorkspace);
		}
I
Ivan Sučić 已提交
527 528

		const result = await window.showInformationMessage(message, ...choices);
J
Joao Moreno 已提交
529
		const uri = Uri.file(repositoryPath);
I
Ivan Sučić 已提交
530

J
Joao Moreno 已提交
531 532 533 534 535 536
		if (result === open) {
			commands.executeCommand('vscode.openFolder', uri);
		} else if (result === addToWorkspace) {
			workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
		} else {
			await this.model.openRepository(repositoryPath);
I
Ivan Sučić 已提交
537
		}
J
Joao Moreno 已提交
538 539
	}

J
Joao Moreno 已提交
540 541
	@command('git.openRepository', { repository: false })
	async openRepository(path?: string): Promise<void> {
542
		if (!path) {
J
Joao Moreno 已提交
543 544 545 546 547 548
			const result = await window.showOpenDialog({
				canSelectFiles: false,
				canSelectFolders: true,
				canSelectMany: false,
				defaultUri: Uri.file(os.homedir()),
				openLabel: localize('open repo', "Open Repository")
549 550
			});

J
Joao Moreno 已提交
551
			if (!result || result.length === 0) {
552 553 554
				return;
			}

J
Joao Moreno 已提交
555
			path = result[0].fsPath;
556
		}
J
Joao Moreno 已提交
557 558

		await this.model.openRepository(path);
559 560
	}

J
Joao Moreno 已提交
561 562 563 564 565
	@command('git.close', { repository: true })
	async close(repository: Repository): Promise<void> {
		this.model.close(repository);
	}

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

570
		let uris: Uri[] | undefined;
571 572 573

		if (arg instanceof Uri) {
			if (arg.scheme === 'git') {
574
				uris = [Uri.file(fromGitUri(arg).path)];
575
			} else if (arg.scheme === 'file') {
576
				uris = [arg];
577 578 579 580 581 582
			}
		} else {
			let resource = arg;

			if (!(resource instanceof Resource)) {
				// can happen when called from a keybinding
583
				resource = this.getSCMResource();
584 585 586
			}

			if (resource) {
J
Joao Moreno 已提交
587 588 589 590
				const resources = ([resource, ...resourceStates] as Resource[])
					.filter(r => r.type !== Status.DELETED && r.type !== Status.INDEX_DELETED);

				uris = resources.map(r => r.resourceUri);
591
			}
J
Joao Moreno 已提交
592 593
		}

594
		if (!uris) {
J
Joao Moreno 已提交
595
			return;
J
Joao Moreno 已提交
596 597
		}

598 599 600
		const activeTextEditor = window.activeTextEditor;
		for (const uri of uris) {
			const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
601
				preserveFocus,
J
Joao Moreno 已提交
602
				preview: false,
603
				viewColumn: ViewColumn.Active
604 605
			};

E
Eric Gang 已提交
606 607
			const document = await workspace.openTextDocument(uri);

608 609 610
			// 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) {
E
Eric Gang 已提交
611
				// preserve not only selection but also visible range
B
Benjamin Pasero 已提交
612
				opts.selection = activeTextEditor.selection;
E
Eric Gang 已提交
613 614 615 616 617
				const previousVisibleRanges = activeTextEditor.visibleRanges;
				const editor = await window.showTextDocument(document, opts);
				editor.revealRange(previousVisibleRanges[0]);
			} else {
				await window.showTextDocument(document, opts);
B
Benjamin Pasero 已提交
618
			}
J
Joao Moreno 已提交
619
		}
J
Joao Moreno 已提交
620 621
	}

J
Joao Moreno 已提交
622 623 624 625 626
	@command('git.openFile2')
	async openFile2(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
		this.openFile(arg, ...resourceStates);
	}

J
Joao Moreno 已提交
627 628
	@command('git.openHEADFile')
	async openHEADFile(arg?: Resource | Uri): Promise<void> {
D
Duroktar 已提交
629 630 631 632 633
		let resource: Resource | undefined = undefined;

		if (arg instanceof Resource) {
			resource = arg;
		} else if (arg instanceof Uri) {
634
			resource = this.getSCMResource(arg);
D
Duroktar 已提交
635
		} else {
636
			resource = this.getSCMResource();
D
Duroktar 已提交
637 638 639 640 641 642
		}

		if (!resource) {
			return;
		}

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

J
Joao Moreno 已提交
645 646 647
		if (!HEAD) {
			window.showWarningMessage(localize('HEAD not available', "HEAD version of '{0}' is not available.", path.basename(resource.resourceUri.fsPath)));
			return;
D
Duroktar 已提交
648
		}
J
Joao Moreno 已提交
649 650

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

J
Joao Moreno 已提交
653 654
	@command('git.openChange')
	async openChange(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
655
		const preserveFocus = arg instanceof Resource;
J
Joao 已提交
656
		const preserveSelection = arg instanceof Uri || !arg;
657
		let resources: Resource[] | undefined = undefined;
658

659
		if (arg instanceof Uri) {
660
			const resource = this.getSCMResource(arg);
661 662 663
			if (resource !== undefined) {
				resources = [resource];
			}
664
		} else {
665
			let resource: Resource | undefined = undefined;
J
Joao Moreno 已提交
666

667 668 669
			if (arg instanceof Resource) {
				resource = arg;
			} else {
670
				resource = this.getSCMResource();
671 672 673 674 675
			}

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

678
		if (!resources) {
J
Joao Moreno 已提交
679
			return;
J
Joao Moreno 已提交
680
		}
J
Joao Moreno 已提交
681

682 683
		const preview = resources.length === 1 ? undefined : false;
		for (const resource of resources) {
J
Joao 已提交
684
			await this._openResource(resource, preview, preserveFocus, preserveSelection);
685
		}
J
Joao Moreno 已提交
686 687
	}

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

692 693
		resourceStates = resourceStates.filter(s => !!s);

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

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

699 700 701 702 703 704 705
			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

706
		const selection = resourceStates.filter(s => s instanceof Resource) as Resource[];
707 708 709 710 711 712
		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));
713

714 715 716 717 718 719 720 721
			const yes = localize('yes', "Yes");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

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

J
Joao Moreno 已提交
725
		this.outputChannel.appendLine(`git.stage.scmResources ${scmResources.length}`);
726
		if (!scmResources.length) {
J
Joao Moreno 已提交
727 728
			return;
		}
J
Joao Moreno 已提交
729

730
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
731
		await this.runByRepository(resources, async (repository, resources) => repository.add(resources));
J
Joao Moreno 已提交
732 733
	}

J
Joao Moreno 已提交
734 735
	@command('git.stageAll', { repository: true })
	async stageAll(repository: Repository): Promise<void> {
736
		const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[];
737 738 739 740
		const { merge, unresolved } = await categorizeResourceByResolution(resources);

		if (unresolved.length > 0) {
			const message = unresolved.length > 1
J
Joao Moreno 已提交
741 742
				? 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));
743 744 745 746 747 748 749 750 751

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

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

J
Joao Moreno 已提交
752
		await repository.add([]);
J
Joao Moreno 已提交
753 754
	}

J
Joao Moreno 已提交
755
	@command('git.stageChange')
J
Joao Moreno 已提交
756 757 758 759 760 761 762 763
	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 已提交
764 765
	}

J
Joao Moreno 已提交
766
	@command('git.stageSelectedRanges', { diff: true })
J
Joao Moreno 已提交
767
	async stageSelectedChanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
768 769 770 771 772 773 774
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
J
Joao Moreno 已提交
775 776 777 778
		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 已提交
779

J
Joao Moreno 已提交
780
		if (!selectedChanges.length) {
J
Joao Moreno 已提交
781 782 783
			return;
		}

J
Joao Moreno 已提交
784 785
		await this._stageChanges(textEditor, selectedChanges);
	}
J
Joao Moreno 已提交
786

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

J
Joao Moreno 已提交
791
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
792 793 794
			return;
		}

J
Joao Moreno 已提交
795
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
796
		const originalDocument = await workspace.openTextDocument(originalUri);
J
Joao Moreno 已提交
797
		const result = applyLineChanges(originalDocument, modifiedDocument, changes);
J
Joao Moreno 已提交
798

J
Joao Moreno 已提交
799
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
800
	}
J
Joao Moreno 已提交
801

J
Joao Moreno 已提交
802
	@command('git.revertChange')
J
Joao Moreno 已提交
803 804 805 806
	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 已提交
807 808 809
			return;
		}

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

J
Joao Moreno 已提交
813
	@command('git.revertSelectedRanges', { diff: true })
J
Joao Moreno 已提交
814
	async revertSelectedRanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
815 816 817 818 819 820 821
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
J
Joao Moreno 已提交
822 823
		const selections = textEditor.selections;
		const selectedChanges = changes.filter(change => {
J
Joao Moreno 已提交
824
			const modifiedRange = getModifiedRange(modifiedDocument, change);
J
Joao Moreno 已提交
825 826 827 828
			return selections.every(selection => !selection.intersection(modifiedRange));
		});

		if (selectedChanges.length === changes.length) {
J
Joao Moreno 已提交
829 830 831
			return;
		}

J
Joao Moreno 已提交
832 833
		await this._revertChanges(textEditor, selectedChanges);
	}
J
Joao Moreno 已提交
834

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

J
Joao Moreno 已提交
839
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
840 841 842
			return;
		}

J
Joao Moreno 已提交
843 844
		const originalUri = toGitUri(modifiedUri, '~');
		const originalDocument = await workspace.openTextDocument(originalUri);
845 846
		const selectionsBeforeRevert = textEditor.selections;
		const visibleRangesBeforeRevert = textEditor.visibleRanges;
J
Joao Moreno 已提交
847
		const result = applyLineChanges(originalDocument, modifiedDocument, changes);
848

J
Joao Moreno 已提交
849 850 851
		const edit = new WorkspaceEdit();
		edit.replace(modifiedUri, new Range(new Position(0, 0), modifiedDocument.lineAt(modifiedDocument.lineCount - 1).range.end), result);
		workspace.applyEdit(edit);
852

J
Joao Moreno 已提交
853 854
		await modifiedDocument.save();

855 856
		textEditor.selections = selectionsBeforeRevert;
		textEditor.revealRange(visibleRangesBeforeRevert[0]);
J
Joao Moreno 已提交
857 858
	}

J
Joao Moreno 已提交
859 860
	@command('git.unstage')
	async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
861 862
		resourceStates = resourceStates.filter(s => !!s);

J
Joao Moreno 已提交
863
		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
864
			const resource = this.getSCMResource();
865 866 867 868 869 870 871 872

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

876
		if (!scmResources.length) {
J
Joao Moreno 已提交
877 878 879
			return;
		}

880
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
881
		await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
J
Joao Moreno 已提交
882 883
	}

J
Joao Moreno 已提交
884 885
	@command('git.unstageAll', { repository: true })
	async unstageAll(repository: Repository): Promise<void> {
886
		await repository.revert([]);
J
Joao Moreno 已提交
887
	}
J
Joao Moreno 已提交
888

J
Joao Moreno 已提交
889 890
	@command('git.unstageSelectedRanges', { diff: true })
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
891 892 893 894 895 896 897 898 899
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

900 901 902 903 904 905 906
		if (modifiedUri.scheme !== 'git') {
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
J
Joao Moreno 已提交
907 908 909
			return;
		}

J
Joao Moreno 已提交
910
		const originalUri = toGitUri(modifiedUri, 'HEAD');
J
Joao Moreno 已提交
911
		const originalDocument = await workspace.openTextDocument(originalUri);
912 913 914 915
		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 已提交
916 917 918 919 920

		if (!selectedDiffs.length) {
			return;
		}

921 922
		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
J
Joao Moreno 已提交
923

J
Joao Moreno 已提交
924
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
925 926
	}

J
Joao Moreno 已提交
927 928
	@command('git.clean')
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
929 930
		resourceStates = resourceStates.filter(s => !!s);

J
Joao Moreno 已提交
931
		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
932
			const resource = this.getSCMResource();
933 934 935 936 937 938 939 940

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

J
Joao Moreno 已提交
944
		if (!scmResources.length) {
J
Joao Moreno 已提交
945 946
			return;
		}
J
Joao Moreno 已提交
947

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

J
Joao Moreno 已提交
952
		if (scmResources.length === 1) {
J
Joao Moreno 已提交
953
			if (untrackedCount > 0) {
J
Joao Moreno 已提交
954
				message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
955 956
				yes = localize('delete file', "Delete file");
			} else {
957 958 959 960 961 962
				if (scmResources[0].type === Status.DELETED) {
					yes = localize('restore file', "Restore file");
					message = localize('confirm restore', "Are you sure you want to restore {0}?", path.basename(scmResources[0].resourceUri.fsPath));
				} else {
					message = localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(scmResources[0].resourceUri.fsPath));
				}
J
Joao Moreno 已提交
963 964
			}
		} else {
965 966 967 968 969 970
			if (scmResources.every(resource => resource.type === Status.DELETED)) {
				yes = localize('restore files', "Restore files");
				message = localize('confirm restore multiple', "Are you sure you want to restore {0} files?", scmResources.length);
			} else {
				message = localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", scmResources.length);
			}
J
Joao Moreno 已提交
971 972 973 974 975

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

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

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

J
Joao Moreno 已提交
983
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
984
		await this.runByRepository(resources, async (repository, resources) => repository.clean(resources));
J
Joao Moreno 已提交
985
	}
J
Joao Moreno 已提交
986

J
Joao Moreno 已提交
987 988
	@command('git.cleanAll', { repository: true })
	async cleanAll(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
989
		let resources = repository.workingTreeGroup.resourceStates;
J
Joao Moreno 已提交
990 991 992 993 994

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

J
Joao Moreno 已提交
995 996
		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 已提交
997

J
Joao Moreno 已提交
998 999 1000 1001 1002 1003
		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 已提交
1004
				: localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
1005
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
1006

J
Joao Moreno 已提交
1007
			if (pick !== yes) {
J
Joao Moreno 已提交
1008 1009 1010
				return;
			}

J
Joao 已提交
1011
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
1012 1013 1014 1015 1016 1017 1018 1019
			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 已提交
1020 1021
			}

J
Joao 已提交
1022
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
1023 1024 1025 1026
		} 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 已提交
1027

J
Joao Moreno 已提交
1028 1029 1030
			if (pick !== yes) {
				return;
			}
J
Joao Moreno 已提交
1031

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

J
Joao Moreno 已提交
1034 1035 1036 1037
		} 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 已提交
1038

J
Joao Moreno 已提交
1039 1040 1041 1042 1043
			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 已提交
1044

J
Joao Moreno 已提交
1045
			const yesAll = localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
1046 1047 1048 1049 1050 1051 1052 1053
			const pick = await window.showWarningMessage(message, { modal: true }, yesTracked, yesAll);

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

J
Joao 已提交
1054
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
1055
		}
J
Joao Moreno 已提交
1056 1057
	}

J
Joao Moreno 已提交
1058
	private async smartCommit(
J
Joao Moreno 已提交
1059
		repository: Repository,
1060
		getCommitMessage: () => Promise<string | undefined>,
J
Joao Moreno 已提交
1061 1062
		opts?: CommitOptions
	): Promise<boolean> {
1063
		const config = workspace.getConfiguration('git', Uri.file(repository.root));
J
Joao Moreno 已提交
1064 1065 1066 1067 1068 1069 1070 1071
		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
J
Joao Moreno 已提交
1072 1073
					? localize('unsaved files single', "The following file is unsaved: {0}.\n\nWould you like to save it before committing?", path.basename(unsavedTextDocuments[0].uri.fsPath))
					: localize('unsaved files', "There are {0} unsaved files.\n\nWould you like to save them before committing?", unsavedTextDocuments.length);
J
Joao Moreno 已提交
1074 1075 1076 1077 1078 1079 1080 1081 1082 1083
				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
				}
1084 1085 1086
			}
		}

1087
		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
1088
		const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
J
Joao Moreno 已提交
1089 1090
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
1091 1092

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

J
Joao Moreno 已提交
1095 1096
			// 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?");
1097 1098 1099 1100 1101
			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 已提交
1102 1103 1104
				config.update('enableSmartCommit', true, true);
			} else if (pick !== yes) {
				return false; // do not commit on cancel
1105 1106 1107
			}
		}

J
Joao Moreno 已提交
1108
		if (!opts) {
1109
			opts = { all: noStagedChanges };
J
Joao Moreno 已提交
1110 1111
		} else if (!opts.all && noStagedChanges) {
			opts = { ...opts, all: true };
J
Joao Moreno 已提交
1112 1113
		}

1114 1115 1116
		// enable signing of commits if configurated
		opts.signCommit = enableCommitSigning;

1117 1118 1119 1120
		if (config.get<boolean>('alwaysSignOff')) {
			opts.signoff = true;
		}

J
Joao Moreno 已提交
1121
		if (
1122
			(
J
Joao Moreno 已提交
1123 1124 1125 1126 1127
				// no changes
				(noStagedChanges && noUnstagedChanges)
				// or no staged changes and not `all`
				|| (!opts.all && noStagedChanges)
			)
1128
			&& !opts.empty
J
Joao Moreno 已提交
1129
		) {
J
Joao Moreno 已提交
1130 1131 1132 1133
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
1134
		const message = await getCommitMessage();
J
Joao Moreno 已提交
1135 1136 1137 1138 1139

		if (!message) {
			return false;
		}

J
Joao Moreno 已提交
1140
		await repository.commit(message, opts);
J
Joao Moreno 已提交
1141 1142 1143 1144

		return true;
	}

J
Joao Moreno 已提交
1145
	private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
J
Joao Moreno 已提交
1146
		const message = repository.inputBox.value;
J
Joao Moreno 已提交
1147
		const getCommitMessage = async () => {
J
Joao Moreno 已提交
1148 1149 1150 1151
			if (message) {
				return message;
			}

1152 1153 1154 1155 1156 1157
			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 已提交
1158
			return await window.showInputBox({
1159
				value,
J
Joao Moreno 已提交
1160
				placeHolder: localize('commit message', "Commit message"),
J
Joao Moreno 已提交
1161 1162
				prompt: localize('provide commit message', "Please provide a commit message"),
				ignoreFocusOut: true
J
Joao Moreno 已提交
1163
			});
J
Joao Moreno 已提交
1164 1165
		};

J
Joao Moreno 已提交
1166
		const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
J
Joao Moreno 已提交
1167 1168

		if (message && didCommit) {
J
Joao Moreno 已提交
1169
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
1170
		}
J
Joao Moreno 已提交
1171 1172
	}

J
Joao Moreno 已提交
1173
	@command('git.commit', { repository: true })
J
Joao Moreno 已提交
1174 1175
	async commit(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository);
J
Joao Moreno 已提交
1176 1177
	}

J
Joao Moreno 已提交
1178
	@command('git.commitWithInput', { repository: true })
J
Joao Moreno 已提交
1179
	async commitWithInput(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1180
		if (!repository.inputBox.value) {
J
Joao Moreno 已提交
1181 1182 1183
			return;
		}

J
Joao Moreno 已提交
1184
		const didCommit = await this.smartCommit(repository, async () => repository.inputBox.value);
J
Joao Moreno 已提交
1185 1186

		if (didCommit) {
J
Joao Moreno 已提交
1187
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
1188
		}
J
Joao Moreno 已提交
1189 1190
	}

J
Joao Moreno 已提交
1191
	@command('git.commitStaged', { repository: true })
J
Joao Moreno 已提交
1192 1193
	async commitStaged(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false });
J
Joao Moreno 已提交
1194 1195
	}

J
Joao Moreno 已提交
1196
	@command('git.commitStagedSigned', { repository: true })
J
Joao Moreno 已提交
1197 1198
	async commitStagedSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, signoff: true });
J
Joao Moreno 已提交
1199 1200
	}

J
Joao Moreno 已提交
1201
	@command('git.commitStagedAmend', { repository: true })
J
Joao Moreno 已提交
1202
	async commitStagedAmend(repository: Repository): Promise<void> {
1203
		await this.commitWithAnyInput(repository, { all: false, amend: true });
K
Krzysztof Cieślak 已提交
1204 1205
	}

J
Joao Moreno 已提交
1206
	@command('git.commitAll', { repository: true })
J
Joao Moreno 已提交
1207 1208
	async commitAll(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true });
J
Joao Moreno 已提交
1209 1210
	}

J
Joao Moreno 已提交
1211
	@command('git.commitAllSigned', { repository: true })
J
Joao Moreno 已提交
1212 1213
	async commitAllSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, signoff: true });
J
Joao Moreno 已提交
1214 1215
	}

J
Joao Moreno 已提交
1216
	@command('git.commitAllAmend', { repository: true })
J
Joao Moreno 已提交
1217 1218
	async commitAllAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, amend: true });
K
Krzysztof Cieślak 已提交
1219 1220
	}

J
Joao Moreno 已提交
1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242
	@command('git.commitEmpty', { repository: true })
	async commitEmpty(repository: Repository): Promise<void> {
		const root = Uri.file(repository.root);
		const config = workspace.getConfiguration('git', root);
		const shouldPrompt = config.get<boolean>('confirmEmptyCommits') === true;

		if (shouldPrompt) {
			const message = localize('confirm emtpy commit', "Are you sure you want to create an empty commit?");
			const yes = localize('yes', "Yes");
			const neverAgain = localize('yes never again', "Yes, Don't Show Again");
			const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);

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

		await this.commitWithAnyInput(repository, { empty: true });
	}

J
Joao Moreno 已提交
1243 1244 1245 1246 1247
	@command('git.restoreCommitTemplate', { repository: true })
	async restoreCommitTemplate(repository: Repository): Promise<void> {
		repository.inputBox.value = await repository.getCommitTemplate();
	}

J
Joao Moreno 已提交
1248
	@command('git.undoCommit', { repository: true })
J
Joao Moreno 已提交
1249 1250
	async undoCommit(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
1251 1252

		if (!HEAD || !HEAD.commit) {
J
Joao Moreno 已提交
1253
			window.showWarningMessage(localize('no more', "Can't undo because HEAD doesn't point to any commit."));
J
Joao Moreno 已提交
1254 1255 1256
			return;
		}

J
Joao Moreno 已提交
1257
		const commit = await repository.getCommit('HEAD');
J
Joao Moreno 已提交
1258 1259

		if (commit.parents.length > 0) {
1260 1261 1262 1263 1264
			await repository.reset('HEAD~');
		} else {
			await repository.deleteRef('HEAD');
			await this.unstageAll(repository);
		}
J
Joao Moreno 已提交
1265

J
Joao Moreno 已提交
1266
		repository.inputBox.value = commit.message;
J
Joao Moreno 已提交
1267 1268
	}

J
Joao Moreno 已提交
1269
	@command('git.checkout', { repository: true })
J
Joao Moreno 已提交
1270
	async checkout(repository: Repository, treeish: string): Promise<void> {
J
Joao Moreno 已提交
1271
		if (typeof treeish === 'string') {
J
Joao Moreno 已提交
1272
			return await repository.checkout(treeish);
J
Joao Moreno 已提交
1273 1274
		}

J
Joao Moreno 已提交
1275
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
1276
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
1277 1278 1279
		const includeTags = checkoutType === 'all' || checkoutType === 'tags';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1280
		const createBranch = new CreateBranchItem(this);
J
Joao Moreno 已提交
1281

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

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

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

J
Joao Moreno 已提交
1291
		const picks = [createBranch, ...heads, ...tags, ...remoteHeads];
1292
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
J
Joao Moreno 已提交
1293
		const choice = await window.showQuickPick(picks, { placeHolder });
J
Joao Moreno 已提交
1294 1295 1296 1297 1298

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1299
		await choice.run(repository);
J
Joao Moreno 已提交
1300 1301
	}

1302

J
Joao Moreno 已提交
1303
	@command('git.branch', { repository: true })
J
Joao Moreno 已提交
1304
	async branch(repository: Repository): Promise<void> {
1305
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
1306 1307 1308 1309 1310 1311 1312 1313 1314 1315
		const branchValidationRegex = config.get<string>('branchValidationRegex')!;
		const branchWhitespaceChar = config.get<string>('branchWhitespaceChar')!;
		const validateName = new RegExp(branchValidationRegex);
		const sanitize = (name: string) => {
			name = name.trim();

			if (!name) {
				return name;
			}

1316
			return name.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, branchWhitespaceChar);
J
Joao Moreno 已提交
1317 1318
		};

J
Joao Moreno 已提交
1319
		const result = await window.showInputBox({
J
Joao Moreno 已提交
1320
			placeHolder: localize('branch name', "Branch name"),
J
Joao Moreno 已提交
1321
			prompt: localize('provide branch name', "Please provide a branch name"),
1322 1323
			ignoreFocusOut: true,
			validateInput: (name: string) => {
J
Joao Moreno 已提交
1324
				if (validateName.test(sanitize(name))) {
1325 1326
					return null;
				}
J
Joao Moreno 已提交
1327 1328

				return localize('branch name format invalid', "Branch name needs to match regex: {0}", branchValidationRegex);
1329
			}
J
Joao Moreno 已提交
1330
		});
J
Joao Moreno 已提交
1331

J
Joao Moreno 已提交
1332 1333 1334
		const name = sanitize(result || '');

		if (!name) {
J
Joao Moreno 已提交
1335 1336
			return;
		}
J
Joao Moreno 已提交
1337

J
Joao Moreno 已提交
1338
		await repository.branch(name, true);
J
Joao Moreno 已提交
1339 1340
	}

J
Joao Moreno 已提交
1341
	@command('git.deleteBranch', { repository: true })
J
Joao Moreno 已提交
1342
	async deleteBranch(repository: Repository, name: string, force?: boolean): Promise<void> {
1343 1344
		let run: (force?: boolean) => Promise<void>;
		if (typeof name === 'string') {
J
Joao Moreno 已提交
1345
			run = force => repository.deleteBranch(name, force);
1346
		} else {
J
Joao Moreno 已提交
1347 1348
			const currentHead = repository.HEAD && repository.HEAD.name;
			const heads = repository.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead)
1349
				.map(ref => new BranchDeleteItem(ref));
M
Maik Riechert 已提交
1350

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

M
Maik Riechert 已提交
1354
			if (!choice || !choice.branchName) {
1355 1356
				return;
			}
M
Maik Riechert 已提交
1357
			name = choice.branchName;
J
Joao Moreno 已提交
1358
			run = force => choice.run(repository, force);
M
Maik Riechert 已提交
1359 1360
		}

1361 1362 1363 1364 1365 1366 1367 1368 1369
		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");
1370
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
1371 1372 1373 1374 1375

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

1378 1379
	@command('git.renameBranch', { repository: true })
	async renameBranch(repository: Repository): Promise<void> {
J
Justin Horner 已提交
1380 1381 1382 1383 1384 1385 1386 1387
		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 {
1388
			await repository.renameBranch(name);
J
Justin Horner 已提交
1389 1390 1391
		} catch (err) {
			switch (err.gitErrorCode) {
				case GitErrorCodes.InvalidBranchName:
1392 1393
					window.showErrorMessage(localize('invalid branch name', 'Invalid branch name'));
					return;
J
Justin Horner 已提交
1394
				case GitErrorCodes.BranchAlreadyExists:
J
Joao Moreno 已提交
1395
					window.showErrorMessage(localize('branch already exists', "A branch named '{0}' already exists", name));
1396 1397 1398
					return;
				default:
					throw err;
J
Justin Horner 已提交
1399 1400 1401 1402
			}
		}
	}

J
Joao Moreno 已提交
1403
	@command('git.merge', { repository: true })
J
Joao Moreno 已提交
1404
	async merge(repository: Repository): Promise<void> {
1405 1406 1407 1408
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string>('checkoutType') || 'all';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1409
		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
1410 1411
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1412

J
Joao Moreno 已提交
1413
		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
1414 1415
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1416 1417

		const picks = [...heads, ...remoteHeads];
1418 1419
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
1420 1421 1422 1423 1424

		if (!choice) {
			return;
		}

1425
		await choice.run(repository);
1426 1427
	}

J
Joao Moreno 已提交
1428
	@command('git.createTag', { repository: true })
J
Joao Moreno 已提交
1429
	async createTag(repository: Repository): Promise<void> {
1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441
		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 已提交
1442
			prompt: localize('provide tag message', "Please provide a message to annotate the tag"),
1443 1444 1445 1446 1447
			ignoreFocusOut: true
		});

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

J
Joao Moreno 已提交
1451 1452 1453 1454 1455 1456 1457
	@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;
		}

J
Joao Moreno 已提交
1458
		await repository.fetchDefault();
J
Joao Moreno 已提交
1459 1460
	}

J
Joao Moreno 已提交
1461 1462 1463 1464 1465 1466 1467 1468 1469 1470
	@command('git.fetchAll', { repository: true })
	async fetchAll(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.fetchAll();
	}

J
Joao Moreno 已提交
1471
	@command('git.pullFrom', { repository: true })
J
Joao Moreno 已提交
1472 1473
	async pullFrom(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1474 1475 1476 1477 1478 1479

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

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

D
Dozed12 已提交
1484
		if (!remotePick) {
1485 1486 1487
			return;
		}

D
Dozed12 已提交
1488 1489
		const remoteRefs = repository.refs;
		const remoteRefsFiltered = remoteRefs.filter(r => (r.remote === remotePick.label));
J
Joao Moreno 已提交
1490
		const branchPicks = remoteRefsFiltered.map(r => ({ label: r.name })) as { label: string; description: string }[];
J
Joao Moreno 已提交
1491 1492
		const branchPlaceHolder = localize('pick branch pull', "Pick a branch to pull from");
		const branchPick = await window.showQuickPick(branchPicks, { placeHolder: branchPlaceHolder });
1493

D
Dozed12 已提交
1494
		if (!branchPick) {
1495 1496 1497
			return;
		}

F
Francisco Moreira 已提交
1498 1499
		const remoteCharCnt = remotePick.label.length;

1500
		await repository.pullFrom(false, remotePick.label, branchPick.label.slice(remoteCharCnt + 1));
1501 1502
	}

J
Joao Moreno 已提交
1503
	@command('git.pull', { repository: true })
J
Joao Moreno 已提交
1504 1505
	async pull(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1506 1507 1508 1509 1510 1511

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

J
Joao Moreno 已提交
1512
		await repository.pull(repository.HEAD);
J
Joao Moreno 已提交
1513 1514
	}

J
Joao Moreno 已提交
1515
	@command('git.pullRebase', { repository: true })
J
Joao Moreno 已提交
1516 1517
	async pullRebase(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1518 1519 1520 1521 1522 1523

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

J
Joao Moreno 已提交
1524
		await repository.pullWithRebase(repository.HEAD);
J
Joao Moreno 已提交
1525 1526
	}

J
Joao Moreno 已提交
1527
	private async _push(repository: Repository, pushOptions: PushOptions) {
J
Joao Moreno 已提交
1528
		const remotes = repository.remotes;
1529

J
Joao Moreno 已提交
1530 1531 1532 1533
		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
			return;
		}
1534

J
Joao Moreno 已提交
1535 1536 1537 1538 1539 1540
		const config = workspace.getConfiguration('git', Uri.file(repository.root));
		let forcePushMode: ForcePushMode | undefined = undefined;

		if (pushOptions.forcePush) {
			if (!config.get<boolean>('allowForcePush')) {
				await window.showErrorMessage(localize('force push not allowed', "Force push is not allowed, please enable it with the 'git.allowForcePush' setting."));
1541 1542
				return;
			}
J
Joao Moreno 已提交
1543

J
Joao Moreno 已提交
1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557
			forcePushMode = config.get<boolean>('useForcePushWithLease') === true ? ForcePushMode.ForceWithLease : ForcePushMode.Force;

			if (config.get<boolean>('confirmForcePush')) {
				const message = localize('confirm force push', "You are about to force push your changes, this can be destructive and could inadvertedly overwrite changes made by others.\n\nAre you sure to continue?");
				const yes = localize('ok', "OK");
				const neverAgain = localize('never ask again', "OK, Don't Ask Again");
				const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);

				if (pick === neverAgain) {
					config.update('confirmForcePush', false, true);
				} else if (pick !== yes) {
					return;
				}
			}
J
Joao Moreno 已提交
1558 1559
		}

1560 1561 1562 1563 1564 1565 1566
		if (pushOptions.pushType === PushType.PushTags) {
			await repository.pushTags(undefined, forcePushMode);

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

1567 1568 1569 1570 1571
		if (!repository.HEAD || !repository.HEAD.name) {
			window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			return;
		}

1572 1573 1574 1575 1576 1577 1578
		if (pushOptions.pushType === PushType.Push) {
			try {
				await repository.push(repository.HEAD, forcePushMode);
			} catch (err) {
				if (err.gitErrorCode !== GitErrorCodes.NoUpstreamBranch) {
					throw err;
				}
1579

1580 1581 1582 1583 1584 1585 1586 1587 1588 1589
				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);
				}
			}
		} else {
1590
			const branchName = repository.HEAD.name;
1591 1592 1593
			const picks = remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl! }));
			const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
			const pick = await window.showQuickPick(picks, { placeHolder });
1594

1595 1596
			if (!pick) {
				return;
1597
			}
1598 1599

			await repository.pushTo(pick.label, branchName, undefined, forcePushMode);
1600
		}
J
Joao Moreno 已提交
1601 1602
	}

1603 1604
	@command('git.push', { repository: true })
	async push(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1605
		await this._push(repository, { pushType: PushType.Push });
1606
	}
1607

1608 1609
	@command('git.pushForce', { repository: true })
	async pushForce(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1610
		await this._push(repository, { pushType: PushType.Push, forcePush: true });
1611
	}
1612

1613 1614
	@command('git.pushWithTags', { repository: true })
	async pushWithTags(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1615
		await this._push(repository, { pushType: PushType.PushTags });
1616
	}
1617

1618 1619
	@command('git.pushWithTagsForce', { repository: true })
	async pushWithTagsForce(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1620
		await this._push(repository, { pushType: PushType.PushTags, forcePush: true });
1621 1622
	}

J
Joao Moreno 已提交
1623
	@command('git.pushTo', { repository: true })
J
Joao Moreno 已提交
1624
	async pushTo(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1625
		await this._push(repository, { pushType: PushType.PushTo });
1626
	}
J
Joao Moreno 已提交
1627

1628 1629
	@command('git.pushToForce', { repository: true })
	async pushToForce(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1630
		await this._push(repository, { pushType: PushType.PushTo, forcePush: true });
J
Joao Moreno 已提交
1631 1632
	}

J
Joao Moreno 已提交
1633
	private async _sync(repository: Repository, rebase: boolean): Promise<void> {
J
Joao Moreno 已提交
1634
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
1635 1636 1637 1638 1639

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

J
Joao Moreno 已提交
1640 1641 1642 1643
		const remoteName = HEAD.remote || HEAD.upstream.remote;
		const remote = repository.remotes.find(r => r.name === remoteName);
		const isReadonly = remote && remote.isReadOnly;

J
Joao Moreno 已提交
1644
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
1645
		const shouldPrompt = !isReadonly && config.get<boolean>('confirmSync') === true;
J
Joao Moreno 已提交
1646 1647

		if (shouldPrompt) {
J
Joao Moreno 已提交
1648
			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 已提交
1649
			const yes = localize('ok', "OK");
B
Benjamin Pasero 已提交
1650
			const neverAgain = localize('never again', "OK, Don't Show Again");
J
Joao Moreno 已提交
1651 1652 1653 1654 1655 1656 1657 1658 1659
			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 已提交
1660
		if (rebase) {
J
Joao Moreno 已提交
1661
			await repository.syncRebase(HEAD);
J
Joao Moreno 已提交
1662
		} else {
J
Joao Moreno 已提交
1663
			await repository.sync(HEAD);
J
Joao Moreno 已提交
1664 1665 1666 1667 1668 1669
		}
	}

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

J
Joao 已提交
1672 1673 1674 1675 1676 1677 1678 1679 1680
	@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 已提交
1681
			await repository.sync(HEAD);
J
Joao 已提交
1682 1683 1684
		}));
	}

1685
	@command('git.syncRebase', { repository: true })
J
Joao Moreno 已提交
1686 1687
	syncRebase(repository: Repository): Promise<void> {
		return this._sync(repository, true);
J
Joao Moreno 已提交
1688 1689
	}

J
Joao Moreno 已提交
1690
	@command('git.publish', { repository: true })
J
Joao Moreno 已提交
1691 1692
	async publish(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1693 1694 1695 1696 1697 1698

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

J
Joao Moreno 已提交
1699
		const branchName = repository.HEAD && repository.HEAD.name || '';
M
Markus Wolf 已提交
1700 1701 1702 1703 1704 1705
		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 已提交
1706 1707 1708 1709 1710

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1711
		await repository.pushTo(choice, branchName, true);
J
Joao Moreno 已提交
1712 1713
	}

1714 1715
	@command('git.ignore')
	async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
1716 1717
		resourceStates = resourceStates.filter(s => !!s);

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

1721
			if (!resource) {
J
Joao Moreno 已提交
1722 1723 1724
				return;
			}

1725
			resourceStates = [resource];
J
Joao Moreno 已提交
1726 1727
		}

1728
		const resources = resourceStates
J
Joao Moreno 已提交
1729 1730 1731
			.filter(s => s instanceof Resource)
			.map(r => r.resourceUri);

1732
		if (!resources.length) {
N
NKumar2 已提交
1733 1734 1735
			return;
		}

1736
		await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
N
NKumar2 已提交
1737 1738
	}

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

1743
		if (noUnstagedChanges && noStagedChanges) {
K
Krzysztof Cieślak 已提交
1744 1745 1746
			window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
			return;
		}
J
Joao Moreno 已提交
1747

1748
		const message = await this.getStashMessage();
J
Joao Moreno 已提交
1749 1750 1751 1752 1753

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

J
Joao Moreno 已提交
1754
		await repository.createStash(message, includeUntracked);
K
Krzysztof Cieślak 已提交
1755 1756
	}

1757 1758 1759 1760 1761 1762 1763
	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 已提交
1764 1765 1766 1767 1768 1769 1770 1771 1772 1773
	@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 已提交
1774
	@command('git.stashPop', { repository: true })
J
Joao Moreno 已提交
1775
	async stashPop(repository: Repository): Promise<void> {
1776
		const placeHolder = localize('pick stash to pop', "Pick a stash to pop");
J
Joao Moreno 已提交
1777
		const stash = await this.pickStash(repository, placeHolder);
1778

J
Joao Moreno 已提交
1779
		if (!stash) {
1780 1781 1782
			return;
		}

J
Joao Moreno 已提交
1783
		await repository.popStash(stash.index);
1784 1785 1786 1787
	}

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

J
Joao Moreno 已提交
1790 1791
		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
K
Krzysztof Cieślak 已提交
1792 1793 1794
			return;
		}

1795 1796 1797 1798 1799 1800
		await repository.popStash();
	}

	@command('git.stashApply', { repository: true })
	async stashApply(repository: Repository): Promise<void> {
		const placeHolder = localize('pick stash to apply', "Pick a stash to apply");
J
Joao Moreno 已提交
1801
		const stash = await this.pickStash(repository, placeHolder);
K
Krzysztof Cieślak 已提交
1802

J
Joao Moreno 已提交
1803
		if (!stash) {
K
Krzysztof Cieślak 已提交
1804 1805
			return;
		}
J
Joao Moreno 已提交
1806

J
Joao Moreno 已提交
1807
		await repository.applyStash(stash.index);
K
Krzysztof Cieślak 已提交
1808 1809
	}

1810 1811
	@command('git.stashApplyLatest', { repository: true })
	async stashApplyLatest(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1812
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
1813

J
Joao Moreno 已提交
1814 1815
		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
1816 1817 1818 1819 1820 1821
			return;
		}

		await repository.applyStash();
	}

J
Joao Moreno 已提交
1822
	private async pickStash(repository: Repository, placeHolder: string): Promise<Stash | undefined> {
1823 1824
		const stashes = await repository.getStashes();

J
Joao Moreno 已提交
1825 1826
		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
K
Krzysztof Cieślak 已提交
1827 1828 1829
			return;
		}

J
Joao Moreno 已提交
1830 1831 1832
		const picks = stashes.map(stash => ({ label: `#${stash.index}:  ${stash.description}`, description: '', details: '', stash }));
		const result = await window.showQuickPick(picks, { placeHolder });
		return result && result.stash;
J
Joao Moreno 已提交
1833
	}
K
Krzysztof Cieślak 已提交
1834

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

J
Joao Moreno 已提交
1839
			if (!options.repository) {
J
Joao Moreno 已提交
1840 1841
				result = Promise.resolve(method.apply(this, args));
			} else {
J
Joao Moreno 已提交
1842 1843
				// try to guess the repository based on the first argument
				const repository = this.model.getRepository(args[0]);
J
Joao Moreno 已提交
1844 1845 1846 1847 1848 1849 1850 1851 1852
				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();
				}
1853

J
Joao Moreno 已提交
1854
				result = repositoryPromise.then(repository => {
J
Joao Moreno 已提交
1855
					if (!repository) {
J
Joao 已提交
1856
						return Promise.resolve();
J
Joao Moreno 已提交
1857 1858
					}

J
Joao Moreno 已提交
1859
					return Promise.resolve(method.apply(this, [repository, ...args]));
J
Joao Moreno 已提交
1860
				});
J
Joao Moreno 已提交
1861 1862
			}

K
kieferrm 已提交
1863
			/* __GDPR__
K
kieferrm 已提交
1864 1865 1866 1867
				"git.command" : {
					"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
J
Joao Moreno 已提交
1868 1869
			this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

J
Joao Moreno 已提交
1870
			return result.catch(async err => {
J
Joao Moreno 已提交
1871
				const options: MessageOptions = {
1872
					modal: true
J
Joao Moreno 已提交
1873 1874
				};

J
Joao Moreno 已提交
1875
				let message: string;
1876
				let type: 'error' | 'warning' = 'error';
J
Joao Moreno 已提交
1877

J
Joao Moreno 已提交
1878 1879 1880 1881 1882
				const choices = new Map<string, () => void>();
				const openOutputChannelChoice = localize('open git log', "Open Git Log");
				const outputChannel = this.outputChannel as OutputChannel;
				choices.set(openOutputChannelChoice, () => outputChannel.show());

J
Joao Moreno 已提交
1883
				switch (err.gitErrorCode) {
1884
					case GitErrorCodes.DirtyWorkTree:
J
Joao Moreno 已提交
1885 1886
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
1887
					case GitErrorCodes.PushRejected:
J
Joao Moreno 已提交
1888
						message = localize('cant push', "Can't push refs to remote. Try running 'Pull' first to integrate your changes.");
1889
						break;
1890 1891 1892 1893 1894
					case GitErrorCodes.Conflict:
						message = localize('merge conflicts', "There are merge conflicts. Resolve them before committing.");
						type = 'warning';
						options.modal = false;
						break;
1895 1896
					case GitErrorCodes.NoUserNameConfigured:
					case GitErrorCodes.NoUserEmailConfigured:
J
Joao Moreno 已提交
1897 1898
						message = localize('missing user info', "Make sure you configure your 'user.name' and 'user.email' in git.");
						choices.set(localize('learn more', "Learn More"), () => commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup')));
1899
						break;
J
Joao Moreno 已提交
1900
					default:
1901 1902 1903
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
J
Joao Moreno 已提交
1904
							.split(/[\r\n]/)
J
João Moreno 已提交
1905
							.filter((line: string) => !!line)
1906 1907 1908 1909 1910
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
1911 1912 1913 1914 1915 1916 1917 1918 1919

						break;
				}

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

J
Joao Moreno 已提交
1920 1921 1922 1923 1924 1925 1926
				const allChoices = Array.from(choices.keys());
				const result = type === 'error'
					? await window.showErrorMessage(message, options, ...allChoices)
					: await window.showWarningMessage(message, options, ...allChoices);

				if (result) {
					const resultFn = choices.get(result);
J
Joao Moreno 已提交
1927

J
Joao Moreno 已提交
1928 1929 1930
					if (resultFn) {
						resultFn();
					}
J
Joao Moreno 已提交
1931 1932 1933
				}
			});
		};
1934 1935

		// patch this object, so people can call methods directly
1936
		(this as any)[key] = result;
1937 1938

		return result;
J
Joao Moreno 已提交
1939 1940
	}

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

J
Joao Moreno 已提交
1944
		this.outputChannel.appendLine(`git.getSCMResource.uri ${uri && uri.toString()}`);
J
Joao Moreno 已提交
1945 1946 1947
		for (const r of this.model.repositories.map(r => r.root)) {
			this.outputChannel.appendLine(`repo root ${r}`);
		}
J
Joao Moreno 已提交
1948

J
Joao Moreno 已提交
1949
		if (!uri) {
1950
			return undefined;
J
Joao Moreno 已提交
1951 1952 1953
		}

		if (uri.scheme === 'git') {
J
Joao Moreno 已提交
1954 1955
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
1956 1957 1958 1959
		}

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

J
Joao Moreno 已提交
1962
			if (!repository) {
1963 1964
				return undefined;
			}
J
Joao Moreno 已提交
1965

J
Joao Moreno 已提交
1966 1967
			return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
				|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
J
Joao Moreno 已提交
1968 1969 1970
		}
	}

J
Joao Moreno 已提交
1971
	private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;
J
Joao Moreno 已提交
1972 1973 1974 1975 1976 1977
	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) => {
1978
			let repository = this.model.getRepository(resource);
J
Joao Moreno 已提交
1979 1980 1981 1982 1983 1984

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

1985
			// Could it be a submodule?
1986
			if (pathEquals(resource.fsPath, repository.root)) {
1987 1988 1989
				repository = this.model.getRepositoryForSubmodule(resource) || repository;
			}

J
Joao Moreno 已提交
1990
			const tuple = result.filter(p => p.repository === repository)[0];
J
Joao Moreno 已提交
1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006

			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 已提交
2007 2008 2009
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
João Moreno 已提交
2010
}