commands.ts 47.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';

J
Joao Moreno 已提交
8
import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor } 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';
J
Joao Moreno 已提交
13
import { grep } from './util';
14
import { applyLineChanges, intersectDiffWithRange, toLineRanges, invertLineChange } from './staging';
J
Joao Moreno 已提交
15
import * as path from 'path';
J
Joao Moreno 已提交
16
import * as os from 'os';
J
Joao Moreno 已提交
17
import TelemetryReporter from 'vscode-extension-telemetry';
J
Joao Moreno 已提交
18 19 20
import * as nls from 'vscode-nls';

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

J
Joao Moreno 已提交
22 23 24 25 26 27 28
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 已提交
29
	constructor(protected ref: Ref) { }
J
Joao Moreno 已提交
30

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

		if (!ref) {
			return;
		}

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

class CheckoutTagItem extends CheckoutItem {

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

class CheckoutRemoteHeadItem extends CheckoutItem {

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

	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 已提交
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 ''; }

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

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

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

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

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

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

J
Joao Moreno 已提交
130
export class CommandCenter {
J
Joao Moreno 已提交
131 132

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

J
Joao Moreno 已提交
134
	constructor(
J
Joao Moreno 已提交
135
		private git: Git,
J
Joao Moreno 已提交
136
		private model: Model,
J
Joao Moreno 已提交
137 138
		private outputChannel: OutputChannel,
		private telemetryReporter: TelemetryReporter
J
Joao Moreno 已提交
139
	) {
J
Joao Moreno 已提交
140 141
		this.disposables = Commands.map(({ commandId, key, method, options }) => {
			const command = this.createCommand(commandId, key, method, options);
142

J
Joao Moreno 已提交
143
			if (options.diff) {
J
Joao Moreno 已提交
144 145 146 147 148
				return commands.registerDiffInformationCommand(commandId, command);
			} else {
				return commands.registerCommand(commandId, command);
			}
		});
J
Joao Moreno 已提交
149 150
	}

J
Joao Moreno 已提交
151 152
	@command('git.refresh', { repository: true })
	async refresh(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
153
		await repository.status();
J
Joao Moreno 已提交
154
	}
J
Joao Moreno 已提交
155

J
Joao Moreno 已提交
156 157
	@command('git.openResource')
	async openResource(resource: Resource): Promise<void> {
J
Joao 已提交
158
		await this._openResource(resource, undefined, true, false);
J
Joao Moreno 已提交
159 160
	}

J
Joao 已提交
161
	private async _openResource(resource: Resource, preview?: boolean, preserveFocus?: boolean, preserveSelection?: boolean): Promise<void> {
J
Joao Moreno 已提交
162
		const left = this.getLeftResource(resource);
J
Joao Moreno 已提交
163
		const right = this.getRightResource(resource);
J
Joao Moreno 已提交
164 165
		const title = this.getTitle(resource);

J
Joao Moreno 已提交
166 167 168 169 170
		if (!right) {
			// TODO
			console.error('oh no');
			return;
		}
J
Joao Moreno 已提交
171

J
Joao Moreno 已提交
172
		const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
173 174
			preserveFocus,
			preview,
175
			viewColumn: ViewColumn.Active
J
Joao Moreno 已提交
176 177
		};

J
Joao Moreno 已提交
178 179
		const activeTextEditor = window.activeTextEditor;

180
		if (preserveSelection && activeTextEditor && activeTextEditor.document.uri.toString() === right.toString()) {
J
Joao Moreno 已提交
181 182 183
			opts.selection = activeTextEditor.selection;
		}

184 185 186 187 188 189
		if (!left) {
			const document = await workspace.openTextDocument(right);
			await window.showTextDocument(document, opts);
			return;
		}

J
Joao Moreno 已提交
190
		return await commands.executeCommand<void>('vscode.diff', left, right, title, opts);
J
Joao Moreno 已提交
191 192 193 194 195 196
	}

	private getLeftResource(resource: Resource): Uri | undefined {
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
J
Joao Moreno 已提交
197
				return toGitUri(resource.original, 'HEAD');
J
Joao Moreno 已提交
198 199

			case Status.MODIFIED:
J
Joao Moreno 已提交
200
				return toGitUri(resource.resourceUri, '~');
201 202 203

			case Status.DELETED_BY_THEM:
				return toGitUri(resource.resourceUri, '');
J
Joao Moreno 已提交
204
		}
J
Joao Moreno 已提交
205
	}
J
Joao Moreno 已提交
206

J
Joao Moreno 已提交
207
	private getRightResource(resource: Resource): Uri | undefined {
J
Joao Moreno 已提交
208 209 210 211 212
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_ADDED:
			case Status.INDEX_COPIED:
			case Status.INDEX_RENAMED:
J
Joao Moreno 已提交
213
				return toGitUri(resource.resourceUri, '');
J
Joao Moreno 已提交
214 215

			case Status.INDEX_DELETED:
216
			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
217
			case Status.DELETED:
J
Joao Moreno 已提交
218
				return toGitUri(resource.resourceUri, 'HEAD');
J
Joao Moreno 已提交
219 220 221 222

			case Status.MODIFIED:
			case Status.UNTRACKED:
			case Status.IGNORED:
J
Joao Moreno 已提交
223 224 225 226 227 228
				const repository = this.model.getRepository(resource.resourceUri);

				if (!repository) {
					return;
				}

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

J
Joao Moreno 已提交
232 233
				if (indexStatus && indexStatus.renameResourceUri) {
					return indexStatus.renameResourceUri;
J
Joao Moreno 已提交
234 235
				}

J
Joao Moreno 已提交
236
				return resource.resourceUri;
J
Joao Moreno 已提交
237

238
			case Status.BOTH_ADDED:
J
Joao Moreno 已提交
239
			case Status.BOTH_MODIFIED:
J
Joao Moreno 已提交
240
				return resource.resourceUri;
J
Joao Moreno 已提交
241 242 243 244
		}
	}

	private getTitle(resource: Resource): string {
J
Joao Moreno 已提交
245
		const basename = path.basename(resource.resourceUri.fsPath);
J
Joao Moreno 已提交
246 247 248 249

		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
M
Marc Kassay 已提交
250
			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
251 252 253
				return `${basename} (Index)`;

			case Status.MODIFIED:
M
Marc Kassay 已提交
254 255
			case Status.BOTH_ADDED:
			case Status.BOTH_MODIFIED:
J
Joao Moreno 已提交
256 257 258 259 260 261
				return `${basename} (Working Tree)`;
		}

		return '';
	}

J
Joao Moreno 已提交
262
	@command('git.clone')
263 264 265 266 267 268 269
	async clone(url?: string): Promise<void> {
		if (!url) {
			url = await window.showInputBox({
				prompt: localize('repourl', "Repository URL"),
				ignoreFocusOut: true
			});
		}
J
Joao Moreno 已提交
270 271

		if (!url) {
K
kieferrm 已提交
272
			/* __GDPR__
K
kieferrm 已提交
273 274 275 276
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
277 278
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
			return;
J
Joao Moreno 已提交
279 280
		}

281
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
282
		const value = config.get<string>('defaultCloneDirectory') || os.homedir();
283

J
Joao Moreno 已提交
284 285
		const parentPath = await window.showInputBox({
			prompt: localize('parent', "Parent Directory"),
J
Joao Moreno 已提交
286
			value,
J
Joao Moreno 已提交
287
			ignoreFocusOut: true
J
Joao Moreno 已提交
288 289 290
		});

		if (!parentPath) {
K
kieferrm 已提交
291
			/* __GDPR__
K
kieferrm 已提交
292 293 294 295
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
296 297
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
			return;
J
Joao Moreno 已提交
298 299
		}

J
Joao Moreno 已提交
300
		const clonePromise = this.git.clone(url, parentPath);
M
Maryam Archie 已提交
301

J
Joao Moreno 已提交
302

303
		try {
M
Maryam Archie 已提交
304 305 306
			window.withProgress({ location: ProgressLocation.SourceControl, title: localize('cloning', "Cloning git repository...") }, () => clonePromise);
			window.withProgress({ location: ProgressLocation.Window, title: localize('cloning', "Cloning git repository...") }, () => clonePromise);

307 308 309 310 311 312
			const repositoryPath = await clonePromise;

			const open = localize('openrepo', "Open Repository");
			const result = await window.showInformationMessage(localize('proposeopen', "Would you like to open the cloned repository?"), open);

			const openFolder = result === open;
K
kieferrm 已提交
313
			/* __GDPR__
K
kieferrm 已提交
314 315 316 317 318
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
					"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
				}
			*/
319 320 321 322
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: openFolder ? 1 : 0 });
			if (openFolder) {
				commands.executeCommand('vscode.openFolder', Uri.file(repositoryPath));
			}
323 324
		} catch (err) {
			if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
K
kieferrm 已提交
325
				/* __GDPR__
K
kieferrm 已提交
326 327 328 329
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
330 331
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
			} else {
K
kieferrm 已提交
332
				/* __GDPR__
K
kieferrm 已提交
333 334 335 336
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
337
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
338 339
			}
			throw err;
J
Joao Moreno 已提交
340 341 342
		}
	}

J
Joao Moreno 已提交
343
	@command('git.init')
J
Joao Moreno 已提交
344
	async init(): Promise<void> {
J
Joao Moreno 已提交
345 346 347 348 349 350 351 352 353 354 355
		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 已提交
356 357
		});

J
Joao Moreno 已提交
358
		if (!result || result.length === 0) {
J
Joao Moreno 已提交
359 360 361
			return;
		}

J
Joao Moreno 已提交
362 363 364 365 366 367 368 369 370 371 372 373
		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;
			}
		}

		const path = uri.fsPath;
J
Joao Moreno 已提交
374 375
		await this.git.init(path);
		await this.model.tryOpenRepository(path);
J
Joao Moreno 已提交
376 377
	}

J
Joao Moreno 已提交
378 379 380 381 382
	@command('git.close', { repository: true })
	async close(repository: Repository): Promise<void> {
		this.model.close(repository);
	}

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

387
		let uris: Uri[] | undefined;
388 389 390

		if (arg instanceof Uri) {
			if (arg.scheme === 'git') {
391
				uris = [Uri.file(fromGitUri(arg).path)];
392
			} else if (arg.scheme === 'file') {
393
				uris = [arg];
394 395 396 397 398 399
			}
		} else {
			let resource = arg;

			if (!(resource instanceof Resource)) {
				// can happen when called from a keybinding
400
				resource = this.getSCMResource();
401 402 403
			}

			if (resource) {
404
				uris = [...resourceStates.map(r => r.resourceUri), resource.resourceUri];
405
			}
J
Joao Moreno 已提交
406 407
		}

408
		if (!uris) {
J
Joao Moreno 已提交
409
			return;
J
Joao Moreno 已提交
410 411
		}

412 413 414 415
		const preview = uris.length === 1 ? true : false;
		const activeTextEditor = window.activeTextEditor;
		for (const uri of uris) {
			const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
416
				preserveFocus,
417
				preview: preview,
418
				viewColumn: ViewColumn.Active
419 420
			};

421
			if (activeTextEditor && activeTextEditor.document.uri.toString() === uri.toString()) {
B
Benjamin Pasero 已提交
422 423 424
				opts.selection = activeTextEditor.selection;
			}

425 426
			const document = await workspace.openTextDocument(uri);
			await window.showTextDocument(document, opts);
J
Joao Moreno 已提交
427
		}
J
Joao Moreno 已提交
428 429
	}

J
Joao Moreno 已提交
430 431
	@command('git.openHEADFile')
	async openHEADFile(arg?: Resource | Uri): Promise<void> {
D
Duroktar 已提交
432 433 434 435 436
		let resource: Resource | undefined = undefined;

		if (arg instanceof Resource) {
			resource = arg;
		} else if (arg instanceof Uri) {
437
			resource = this.getSCMResource(arg);
D
Duroktar 已提交
438
		} else {
439
			resource = this.getSCMResource();
D
Duroktar 已提交
440 441 442 443 444 445
		}

		if (!resource) {
			return;
		}

J
Joao Moreno 已提交
446
		const HEAD = this.getLeftResource(resource);
D
Duroktar 已提交
447

J
Joao Moreno 已提交
448 449 450
		if (!HEAD) {
			window.showWarningMessage(localize('HEAD not available', "HEAD version of '{0}' is not available.", path.basename(resource.resourceUri.fsPath)));
			return;
D
Duroktar 已提交
451
		}
J
Joao Moreno 已提交
452 453

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

J
Joao Moreno 已提交
456 457
	@command('git.openChange')
	async openChange(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
458
		const preserveFocus = arg instanceof Resource;
J
Joao 已提交
459
		const preserveSelection = arg instanceof Uri || !arg;
460
		let resources: Resource[] | undefined = undefined;
461

462
		if (arg instanceof Uri) {
463
			const resource = this.getSCMResource(arg);
464 465 466
			if (resource !== undefined) {
				resources = [resource];
			}
467
		} else {
468
			let resource: Resource | undefined = undefined;
J
Joao Moreno 已提交
469

470 471 472
			if (arg instanceof Resource) {
				resource = arg;
			} else {
473
				resource = this.getSCMResource();
474 475 476 477 478
			}

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

481
		if (!resources) {
J
Joao Moreno 已提交
482
			return;
J
Joao Moreno 已提交
483
		}
J
Joao Moreno 已提交
484

485 486
		const preview = resources.length === 1 ? undefined : false;
		for (const resource of resources) {
J
Joao 已提交
487
			await this._openResource(resource, preview, preserveFocus, preserveSelection);
488
		}
J
Joao Moreno 已提交
489 490
	}

491 492
	@command('git.stage')
	async stage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
493
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
494
			const resource = this.getSCMResource();
495 496 497 498 499 500 501 502

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

503
		const selection = resourceStates.filter(s => s instanceof Resource) as Resource[];
J
Joao Moreno 已提交
504 505 506 507 508 509 510 511 512 513 514 515 516 517
		const merge = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge);
		const bothModified = merge.filter(s => s.type === Status.BOTH_MODIFIED);
		const promises = bothModified.map(s => grep(s.resourceUri.fsPath, /^<{7}|^={7}|^>{7}/));
		const unresolvedBothModified = await Promise.all<boolean>(promises);
		const resolvedConflicts = bothModified.filter((s, i) => !unresolvedBothModified[i]);
		const unresolvedConflicts = [
			...merge.filter(s => s.type !== Status.BOTH_MODIFIED),
			...bothModified.filter((s, i) => unresolvedBothModified[i])
		];

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

519 520 521 522 523 524 525 526
			const yes = localize('yes', "Yes");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

J
Joao Moreno 已提交
527 528
		const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree);
		const scmResources = [...workingTree, ...resolvedConflicts, ...unresolvedConflicts];
529

530
		if (!scmResources.length) {
J
Joao Moreno 已提交
531 532
			return;
		}
J
Joao Moreno 已提交
533

534
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
535
		await this.runByRepository(resources, async (repository, resources) => repository.add(resources));
J
Joao Moreno 已提交
536 537
	}

J
Joao Moreno 已提交
538 539
	@command('git.stageAll', { repository: true })
	async stageAll(repository: Repository): Promise<void> {
540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
		const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[];
		const mergeConflicts = resources.filter(s => s.resourceGroupType === ResourceGroupType.Merge);

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

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

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

J
Joao Moreno 已提交
556
		await repository.add([]);
J
Joao Moreno 已提交
557 558
	}

J
Joao Moreno 已提交
559
	@command('git.stageChange')
J
Joao Moreno 已提交
560 561 562 563 564 565 566 567
	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 已提交
568 569
	}

J
Joao Moreno 已提交
570
	@command('git.stageSelectedRanges', { diff: true })
J
Joao Moreno 已提交
571
	async stageSelectedChanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
572 573 574 575 576 577 578
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
J
Joao Moreno 已提交
579 580 581 582
		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 已提交
583

J
Joao Moreno 已提交
584
		if (!selectedChanges.length) {
J
Joao Moreno 已提交
585 586 587
			return;
		}

J
Joao Moreno 已提交
588 589
		await this._stageChanges(textEditor, selectedChanges);
	}
J
Joao Moreno 已提交
590

J
Joao Moreno 已提交
591 592 593 594 595
	private async _stageChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
		const modifiedDocument = textEditor.document;
		const modifiedUri = modifiedDocument.uri;

		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
596 597 598
			return;
		}

J
Joao Moreno 已提交
599 600 601
		const originalUri = toGitUri(modifiedUri, '~');
		const originalDocument = await workspace.openTextDocument(originalUri);
		const result = applyLineChanges(originalDocument, modifiedDocument, changes);
602

J
Joao Moreno 已提交
603
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
604
	}
J
Joao Moreno 已提交
605

J
Joao Moreno 已提交
606
	@command('git.revertChange')
J
Joao Moreno 已提交
607 608 609 610 611 612 613 614
	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) {
			return;
		}

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

J
Joao Moreno 已提交
617
	@command('git.revertSelectedRanges', { diff: true })
J
Joao Moreno 已提交
618
	async revertSelectedRanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
619 620 621 622 623 624 625
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
J
Joao Moreno 已提交
626 627 628 629 630
		const selections = textEditor.selections;
		const selectedChanges = changes.filter(change => {
			const modifiedRange = change.modifiedEndLineNumber === 0
				? new Range(modifiedDocument.lineAt(change.modifiedStartLineNumber - 1).range.end, modifiedDocument.lineAt(change.modifiedStartLineNumber).range.start)
				: new Range(modifiedDocument.lineAt(change.modifiedStartLineNumber - 1).range.start, modifiedDocument.lineAt(change.modifiedEndLineNumber - 1).range.end);
J
Joao Moreno 已提交
631

J
Joao Moreno 已提交
632 633 634 635
			return selections.every(selection => !selection.intersection(modifiedRange));
		});

		if (selectedChanges.length === changes.length) {
J
Joao Moreno 已提交
636 637 638
			return;
		}

J
Joao Moreno 已提交
639 640
		await this._revertChanges(textEditor, selectedChanges);
	}
J
Joao Moreno 已提交
641

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

J
Joao Moreno 已提交
646
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
647 648 649
			return;
		}

J
Joao Moreno 已提交
650 651
		const originalUri = toGitUri(modifiedUri, '~');
		const originalDocument = await workspace.openTextDocument(originalUri);
J
Joao Moreno 已提交
652 653 654 655 656 657 658 659 660
		const basename = path.basename(modifiedUri.fsPath);
		const message = localize('confirm revert', "Are you sure you want to revert the selected changes in {0}?", basename);
		const yes = localize('revert', "Revert Changes");
		const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

J
Joao Moreno 已提交
661
		const result = applyLineChanges(originalDocument, modifiedDocument, changes);
J
Joao Moreno 已提交
662 663 664
		const edit = new WorkspaceEdit();
		edit.replace(modifiedUri, new Range(new Position(0, 0), modifiedDocument.lineAt(modifiedDocument.lineCount - 1).range.end), result);
		workspace.applyEdit(edit);
J
Joao Moreno 已提交
665
		await modifiedDocument.save();
J
Joao Moreno 已提交
666 667
	}

J
Joao Moreno 已提交
668 669
	@command('git.unstage')
	async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
670
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
671
			const resource = this.getSCMResource();
672 673 674 675 676 677 678 679

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

683
		if (!scmResources.length) {
J
Joao Moreno 已提交
684 685 686
			return;
		}

687
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
688
		await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
J
Joao Moreno 已提交
689 690
	}

J
Joao Moreno 已提交
691 692
	@command('git.unstageAll', { repository: true })
	async unstageAll(repository: Repository): Promise<void> {
693
		await repository.revert([]);
J
Joao Moreno 已提交
694
	}
J
Joao Moreno 已提交
695

J
Joao Moreno 已提交
696 697
	@command('git.unstageSelectedRanges', { diff: true })
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
698 699 700 701 702 703 704 705 706
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

707 708 709 710 711 712 713
		if (modifiedUri.scheme !== 'git') {
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
J
Joao Moreno 已提交
714 715 716
			return;
		}

J
Joao Moreno 已提交
717
		const originalUri = toGitUri(modifiedUri, 'HEAD');
J
Joao Moreno 已提交
718
		const originalDocument = await workspace.openTextDocument(originalUri);
719 720 721 722
		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 已提交
723 724 725 726 727

		if (!selectedDiffs.length) {
			return;
		}

728 729
		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
J
Joao Moreno 已提交
730

J
Joao Moreno 已提交
731
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
732 733
	}

J
Joao Moreno 已提交
734 735
	@command('git.clean')
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
736
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
737
			const resource = this.getSCMResource();
738 739 740 741 742 743 744 745

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

J
Joao Moreno 已提交
749
		if (!scmResources.length) {
J
Joao Moreno 已提交
750 751
			return;
		}
J
Joao Moreno 已提交
752

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

J
Joao Moreno 已提交
757
		if (scmResources.length === 1) {
J
Joao Moreno 已提交
758
			if (untrackedCount > 0) {
J
Joao Moreno 已提交
759
				message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
760 761
				yes = localize('delete file', "Delete file");
			} else {
J
Joao Moreno 已提交
762
				message = localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
763 764
			}
		} else {
J
Joao Moreno 已提交
765
			message = localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", scmResources.length);
J
Joao Moreno 已提交
766 767 768 769 770

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

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

J
Joao Moreno 已提交
774 775 776 777
		if (pick !== yes) {
			return;
		}

J
Joao Moreno 已提交
778
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
779
		await this.runByRepository(resources, async (repository, resources) => repository.clean(resources));
J
Joao Moreno 已提交
780
	}
J
Joao Moreno 已提交
781

J
Joao Moreno 已提交
782 783
	@command('git.cleanAll', { repository: true })
	async cleanAll(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
784
		let resources = repository.workingTreeGroup.resourceStates;
J
Joao Moreno 已提交
785 786 787 788 789

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

J
Joao Moreno 已提交
790 791
		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 已提交
792

J
Joao Moreno 已提交
793 794 795 796 797 798
		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 已提交
799
				: localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
800
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
801

J
Joao Moreno 已提交
802
			if (pick !== yes) {
J
Joao Moreno 已提交
803 804 805
				return;
			}

J
Joao 已提交
806
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
807 808 809 810 811 812 813 814
			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 已提交
815 816
			}

J
Joao 已提交
817
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
818 819 820 821
		} 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 已提交
822

J
Joao Moreno 已提交
823 824 825
			if (pick !== yes) {
				return;
			}
J
Joao Moreno 已提交
826

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

J
Joao Moreno 已提交
829 830 831 832
		} 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 已提交
833

J
Joao Moreno 已提交
834 835 836 837 838
			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 已提交
839

J
Joao Moreno 已提交
840
			const yesAll = localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
841 842 843 844 845 846 847 848
			const pick = await window.showWarningMessage(message, { modal: true }, yesTracked, yesAll);

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

J
Joao 已提交
849
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
850
		}
J
Joao Moreno 已提交
851 852
	}

J
Joao Moreno 已提交
853
	private async smartCommit(
J
Joao Moreno 已提交
854
		repository: Repository,
855
		getCommitMessage: () => Promise<string | undefined>,
J
Joao Moreno 已提交
856 857
		opts?: CommitOptions
	): Promise<boolean> {
858 859
		const config = workspace.getConfiguration('git');
		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
860
		const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
J
Joao Moreno 已提交
861 862
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
863 864

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

J
Joao Moreno 已提交
867 868
			// 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?");
869 870 871 872 873
			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 已提交
874 875 876
				config.update('enableSmartCommit', true, true);
			} else if (pick !== yes) {
				return false; // do not commit on cancel
877 878 879
			}
		}

J
Joao Moreno 已提交
880
		if (!opts) {
881
			opts = { all: noStagedChanges };
J
Joao Moreno 已提交
882 883
		}

884 885 886
		// enable signing of commits if configurated
		opts.signCommit = enableCommitSigning;

J
Joao Moreno 已提交
887 888
		if (
			// no changes
889
			(noStagedChanges && noUnstagedChanges)
J
Joao Moreno 已提交
890
			// or no staged changes and not `all`
891
			|| (!opts.all && noStagedChanges)
J
Joao Moreno 已提交
892
		) {
J
Joao Moreno 已提交
893 894 895 896
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
897
		const message = await getCommitMessage();
J
Joao Moreno 已提交
898 899 900 901 902

		if (!message) {
			return false;
		}

J
Joao Moreno 已提交
903
		await repository.commit(message, opts);
J
Joao Moreno 已提交
904 905 906 907

		return true;
	}

J
Joao Moreno 已提交
908
	private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
J
Joao Moreno 已提交
909
		const message = repository.inputBox.value;
J
Joao Moreno 已提交
910
		const getCommitMessage = async () => {
J
Joao Moreno 已提交
911 912 913 914 915 916
			if (message) {
				return message;
			}

			return await window.showInputBox({
				placeHolder: localize('commit message', "Commit message"),
J
Joao Moreno 已提交
917 918
				prompt: localize('provide commit message', "Please provide a commit message"),
				ignoreFocusOut: true
J
Joao Moreno 已提交
919
			});
J
Joao Moreno 已提交
920 921
		};

J
Joao Moreno 已提交
922
		const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
J
Joao Moreno 已提交
923 924

		if (message && didCommit) {
J
Joao Moreno 已提交
925
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
926
		}
J
Joao Moreno 已提交
927 928
	}

J
Joao Moreno 已提交
929
	@command('git.commit', { repository: true })
J
Joao Moreno 已提交
930 931
	async commit(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository);
J
Joao Moreno 已提交
932 933
	}

J
Joao Moreno 已提交
934
	@command('git.commitWithInput', { repository: true })
J
Joao Moreno 已提交
935
	async commitWithInput(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
936
		if (!repository.inputBox.value) {
J
Joao Moreno 已提交
937 938 939
			return;
		}

J
Joao Moreno 已提交
940
		const didCommit = await this.smartCommit(repository, async () => repository.inputBox.value);
J
Joao Moreno 已提交
941 942

		if (didCommit) {
J
Joao Moreno 已提交
943
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
944
		}
J
Joao Moreno 已提交
945 946
	}

J
Joao Moreno 已提交
947
	@command('git.commitStaged', { repository: true })
J
Joao Moreno 已提交
948 949
	async commitStaged(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false });
J
Joao Moreno 已提交
950 951
	}

J
Joao Moreno 已提交
952
	@command('git.commitStagedSigned', { repository: true })
J
Joao Moreno 已提交
953 954
	async commitStagedSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, signoff: true });
J
Joao Moreno 已提交
955 956
	}

J
Joao Moreno 已提交
957
	@command('git.commitStagedAmend', { repository: true })
J
Joao Moreno 已提交
958 959
	async commitStagedAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, amend: true });
K
Krzysztof Cieślak 已提交
960 961
	}

J
Joao Moreno 已提交
962
	@command('git.commitAll', { repository: true })
J
Joao Moreno 已提交
963 964
	async commitAll(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true });
J
Joao Moreno 已提交
965 966
	}

J
Joao Moreno 已提交
967
	@command('git.commitAllSigned', { repository: true })
J
Joao Moreno 已提交
968 969
	async commitAllSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, signoff: true });
J
Joao Moreno 已提交
970 971
	}

J
Joao Moreno 已提交
972
	@command('git.commitAllAmend', { repository: true })
J
Joao Moreno 已提交
973 974
	async commitAllAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, amend: true });
K
Krzysztof Cieślak 已提交
975 976
	}

J
Joao Moreno 已提交
977
	@command('git.undoCommit', { repository: true })
J
Joao Moreno 已提交
978 979
	async undoCommit(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
980 981 982 983 984

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

J
Joao Moreno 已提交
985 986
		const commit = await repository.getCommit('HEAD');
		await repository.reset('HEAD~');
J
Joao Moreno 已提交
987
		repository.inputBox.value = commit.message;
J
Joao Moreno 已提交
988 989
	}

J
Joao Moreno 已提交
990
	@command('git.checkout', { repository: true })
J
Joao Moreno 已提交
991
	async checkout(repository: Repository, treeish: string): Promise<void> {
J
Joao Moreno 已提交
992
		if (typeof treeish === 'string') {
J
Joao Moreno 已提交
993
			return await repository.checkout(treeish);
J
Joao Moreno 已提交
994 995
		}

J
Joao Moreno 已提交
996
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
997
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
998 999 1000
		const includeTags = checkoutType === 'all' || checkoutType === 'tags';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1001
		const createBranch = new CreateBranchItem(this);
J
Joao Moreno 已提交
1002

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

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

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

J
Joao Moreno 已提交
1012
		const picks = [createBranch, ...heads, ...tags, ...remoteHeads];
1013
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
J
Joao Moreno 已提交
1014
		const choice = await window.showQuickPick(picks, { placeHolder });
J
Joao Moreno 已提交
1015 1016 1017 1018 1019

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1020
		await choice.run(repository);
J
Joao Moreno 已提交
1021 1022
	}

J
Joao Moreno 已提交
1023
	@command('git.branch', { repository: true })
J
Joao Moreno 已提交
1024
	async branch(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1025
		const result = await window.showInputBox({
J
Joao Moreno 已提交
1026
			placeHolder: localize('branch name', "Branch name"),
J
Joao Moreno 已提交
1027 1028
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true
J
Joao Moreno 已提交
1029
		});
J
Joao Moreno 已提交
1030

J
Joao Moreno 已提交
1031 1032 1033
		if (!result) {
			return;
		}
J
Joao Moreno 已提交
1034

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

J
Joao Moreno 已提交
1039
	@command('git.deleteBranch', { repository: true })
J
Joao Moreno 已提交
1040
	async deleteBranch(repository: Repository, name: string, force?: boolean): Promise<void> {
1041 1042
		let run: (force?: boolean) => Promise<void>;
		if (typeof name === 'string') {
J
Joao Moreno 已提交
1043
			run = force => repository.deleteBranch(name, force);
1044
		} else {
J
Joao Moreno 已提交
1045 1046
			const currentHead = repository.HEAD && repository.HEAD.name;
			const heads = repository.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead)
1047
				.map(ref => new BranchDeleteItem(ref));
M
Maik Riechert 已提交
1048

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

M
Maik Riechert 已提交
1052
			if (!choice || !choice.branchName) {
1053 1054
				return;
			}
M
Maik Riechert 已提交
1055
			name = choice.branchName;
J
Joao Moreno 已提交
1056
			run = force => choice.run(repository, force);
M
Maik Riechert 已提交
1057 1058
		}

1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073
		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");
			const pick = await window.showWarningMessage(message, yes);

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

J
Joao Moreno 已提交
1076
	@command('git.merge', { repository: true })
J
Joao Moreno 已提交
1077
	async merge(repository: Repository): Promise<void> {
1078 1079 1080 1081
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string>('checkoutType') || 'all';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1082
		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
1083 1084
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1085

J
Joao Moreno 已提交
1086
		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
1087 1088
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1089 1090

		const picks = [...heads, ...remoteHeads];
1091 1092
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
1093 1094 1095 1096 1097

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1098
		try {
J
Joao Moreno 已提交
1099
			await choice.run(repository);
J
Joao Moreno 已提交
1100 1101 1102 1103 1104 1105 1106 1107
		} catch (err) {
			if (err.gitErrorCode !== GitErrorCodes.Conflict) {
				throw err;
			}

			const message = localize('merge conflicts', "There are merge conflicts. Resolve them before committing.");
			await window.showWarningMessage(message);
		}
1108 1109
	}

J
Joao Moreno 已提交
1110
	@command('git.createTag', { repository: true })
J
Joao Moreno 已提交
1111
	async createTag(repository: Repository): Promise<void> {
1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123
		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 已提交
1124
			prompt: localize('provide tag message', "Please provide a message to annotate the tag"),
1125 1126 1127 1128 1129
			ignoreFocusOut: true
		});

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

J
Joao Moreno 已提交
1133
	@command('git.pullFrom', { repository: true })
J
Joao Moreno 已提交
1134 1135
	async pullFrom(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159

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

		const picks = remotes.map(r => ({ label: r.name, description: r.url }));
		const placeHolder = localize('pick remote pull repo', "Pick a remote to pull the branch from");
		const pick = await window.showQuickPick(picks, { placeHolder });

		if (!pick) {
			return;
		}

		const branchName = await window.showInputBox({
			placeHolder: localize('branch name', "Branch name"),
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true
		});

		if (!branchName) {
			return;
		}

J
Joao Moreno 已提交
1160
		repository.pull(false, pick.label, branchName);
1161 1162
	}

J
Joao Moreno 已提交
1163
	@command('git.pull', { repository: true })
J
Joao Moreno 已提交
1164 1165
	async pull(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1166 1167 1168 1169 1170 1171

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

J
Joao Moreno 已提交
1172
		await repository.pull();
J
Joao Moreno 已提交
1173 1174
	}

J
Joao Moreno 已提交
1175
	@command('git.pullRebase', { repository: true })
J
Joao Moreno 已提交
1176 1177
	async pullRebase(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1178 1179 1180 1181 1182 1183

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

J
Joao Moreno 已提交
1184
		await repository.pullWithRebase();
J
Joao Moreno 已提交
1185 1186
	}

J
Joao Moreno 已提交
1187
	@command('git.push', { repository: true })
J
Joao Moreno 已提交
1188 1189
	async push(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1190 1191 1192 1193 1194 1195

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

J
Joao Moreno 已提交
1196
		await repository.push();
J
Joao Moreno 已提交
1197 1198
	}

J
Joao Moreno 已提交
1199
	@command('git.pushWithTags', { repository: true })
J
Joao Moreno 已提交
1200 1201
	async pushWithTags(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1202 1203 1204 1205 1206 1207

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

J
Joao Moreno 已提交
1208
		await repository.pushTags();
1209 1210 1211 1212

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

J
Joao Moreno 已提交
1213
	@command('git.pushTo', { repository: true })
J
Joao Moreno 已提交
1214 1215
	async pushTo(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1216 1217 1218 1219 1220 1221

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

J
Joao Moreno 已提交
1222
		if (!repository.HEAD || !repository.HEAD.name) {
J
Joao Moreno 已提交
1223 1224 1225 1226
			window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			return;
		}

J
Joao Moreno 已提交
1227
		const branchName = repository.HEAD.name;
J
Joao Moreno 已提交
1228 1229 1230 1231 1232 1233 1234 1235
		const picks = remotes.map(r => ({ label: r.name, description: r.url }));
		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;
		}

J
Joao Moreno 已提交
1236
		repository.pushTo(pick.label, branchName);
J
Joao Moreno 已提交
1237 1238
	}

J
Joao Moreno 已提交
1239
	@command('git.sync', { repository: true })
J
Joao Moreno 已提交
1240 1241
	async sync(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262

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

		const config = workspace.getConfiguration('git');
		const shouldPrompt = config.get<boolean>('confirmSync') === true;

		if (shouldPrompt) {
			const message = localize('sync is unpredictable', "This action will push and pull commits to and from '{0}'.", HEAD.upstream);
			const yes = localize('ok', "OK");
			const neverAgain = localize('never again', "OK, Never Show Again");
			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 已提交
1263
		await repository.sync();
J
Joao Moreno 已提交
1264 1265
	}

J
Joao 已提交
1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278
	@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;
			}

			await repository.sync();
		}));
	}

J
Joao Moreno 已提交
1279
	@command('git.publish', { repository: true })
J
Joao Moreno 已提交
1280 1281
	async publish(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1282 1283 1284 1285 1286 1287

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

J
Joao Moreno 已提交
1288
		const branchName = repository.HEAD && repository.HEAD.name || '';
M
Markus Wolf 已提交
1289 1290 1291 1292 1293 1294
		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 已提交
1295 1296 1297 1298 1299

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1300
		await repository.pushTo(choice, branchName, true);
J
Joao Moreno 已提交
1301 1302
	}

J
Joao Moreno 已提交
1303
	@command('git.showOutput')
J
Joao Moreno 已提交
1304 1305 1306 1307
	showOutput(): void {
		this.outputChannel.show();
	}

1308 1309
	@command('git.ignore')
	async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
1310
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
1311
			const resource = this.getSCMResource();
N
NKumar2 已提交
1312

1313
			if (!resource) {
J
Joao Moreno 已提交
1314 1315 1316
				return;
			}

1317
			resourceStates = [resource];
J
Joao Moreno 已提交
1318 1319
		}

1320
		const resources = resourceStates
J
Joao Moreno 已提交
1321 1322 1323
			.filter(s => s instanceof Resource)
			.map(r => r.resourceUri);

1324
		if (!resources.length) {
N
NKumar2 已提交
1325 1326 1327
			return;
		}

1328
		await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
N
NKumar2 已提交
1329 1330
	}

J
Joao Moreno 已提交
1331
	@command('git.stash', { repository: true })
J
Joao Moreno 已提交
1332 1333
	async stash(repository: Repository): Promise<void> {
		if (repository.workingTreeGroup.resourceStates.length === 0) {
K
Krzysztof Cieślak 已提交
1334 1335 1336
			window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
			return;
		}
J
Joao Moreno 已提交
1337 1338 1339 1340 1341 1342 1343 1344 1345 1346

		const message = await window.showInputBox({
			prompt: localize('provide stash message', "Optionally provide a stash message"),
			placeHolder: localize('stash message', "Stash message")
		});

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

J
Joao Moreno 已提交
1347
		await repository.createStash(message);
K
Krzysztof Cieślak 已提交
1348 1349
	}

J
Joao Moreno 已提交
1350
	@command('git.stashPop', { repository: true })
J
Joao Moreno 已提交
1351 1352
	async stashPop(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
1353 1354

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1355 1356 1357 1358
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

J
Joao Moreno 已提交
1359 1360
		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 已提交
1361 1362 1363 1364 1365
		const choice = await window.showQuickPick(picks, { placeHolder });

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

J
Joao Moreno 已提交
1367
		await repository.popStash(choice.id);
K
Krzysztof Cieślak 已提交
1368 1369
	}

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

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1375 1376 1377 1378
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

J
Joao Moreno 已提交
1379
		await repository.popStash();
J
Joao Moreno 已提交
1380
	}
K
Krzysztof Cieślak 已提交
1381

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

J
Joao Moreno 已提交
1386
			if (!options.repository) {
J
Joao Moreno 已提交
1387 1388
				result = Promise.resolve(method.apply(this, args));
			} else {
J
Joao Moreno 已提交
1389 1390
				// try to guess the repository based on the first argument
				const repository = this.model.getRepository(args[0]);
J
Joao Moreno 已提交
1391 1392 1393 1394 1395 1396 1397 1398 1399
				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();
				}
1400

J
Joao Moreno 已提交
1401
				result = repositoryPromise.then(repository => {
J
Joao Moreno 已提交
1402
					if (!repository) {
J
Joao 已提交
1403
						return Promise.resolve();
J
Joao Moreno 已提交
1404 1405
					}

J
Joao Moreno 已提交
1406
					return Promise.resolve(method.apply(this, [repository, ...args]));
J
Joao Moreno 已提交
1407
				});
J
Joao Moreno 已提交
1408 1409
			}

K
kieferrm 已提交
1410
			/* __GDPR__
K
kieferrm 已提交
1411 1412 1413 1414
				"git.command" : {
					"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
J
Joao Moreno 已提交
1415 1416
			this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

J
Joao Moreno 已提交
1417 1418 1419 1420
			return result.catch(async err => {
				let message: string;

				switch (err.gitErrorCode) {
1421
					case GitErrorCodes.DirtyWorkTree:
J
Joao Moreno 已提交
1422 1423
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
1424 1425 1426
					case GitErrorCodes.PushRejected:
						message = localize('cant push', "Can't push refs to remote. Run 'Pull' first to integrate your changes.");
						break;
J
Joao Moreno 已提交
1427
					default:
1428 1429 1430
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
J
Joao Moreno 已提交
1431
							.split(/[\r\n]/)
1432 1433 1434 1435 1436 1437
							.filter(line => !!line)
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455

						break;
				}

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

				const outputChannel = this.outputChannel as OutputChannel;
				const openOutputChannelChoice = localize('open git log', "Open Git Log");
				const choice = await window.showErrorMessage(message, openOutputChannelChoice);

				if (choice === openOutputChannelChoice) {
					outputChannel.show();
				}
			});
		};
1456 1457 1458 1459 1460

		// patch this object, so people can call methods directly
		this[key] = result;

		return result;
J
Joao Moreno 已提交
1461 1462
	}

1463
	private getSCMResource(uri?: Uri): Resource | undefined {
1464
		uri = uri ? uri : window.activeTextEditor && window.activeTextEditor.document.uri;
J
Joao Moreno 已提交
1465 1466

		if (!uri) {
1467
			return undefined;
J
Joao Moreno 已提交
1468 1469 1470
		}

		if (uri.scheme === 'git') {
J
Joao Moreno 已提交
1471 1472
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
1473 1474 1475 1476
		}

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

J
Joao Moreno 已提交
1479
			if (!repository) {
1480 1481
				return undefined;
			}
J
Joao Moreno 已提交
1482

J
Joao Moreno 已提交
1483 1484
			return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
				|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
J
Joao Moreno 已提交
1485 1486 1487
		}
	}

J
Joao Moreno 已提交
1488
	private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;
J
Joao Moreno 已提交
1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518
	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) => {
			const repository = this.model.getRepository(resource);

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

			const tuple = result.filter(p => p[0] === repository)[0];

			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 已提交
1519 1520 1521
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
Joao Moreno 已提交
1522
}