commands.ts 46.3 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 } 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 560
	@command('git.stageSelectedRanges', { diff: true })
	async stageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
561 562 563 564 565 566 567 568 569
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

J
Joao Moreno 已提交
570
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
571 572 573
			return;
		}

J
Joao Moreno 已提交
574
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
575
		const originalDocument = await workspace.openTextDocument(originalUri);
576 577 578 579
		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 已提交
580 581 582 583 584

		if (!selectedDiffs.length) {
			return;
		}

585 586
		const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);

J
Joao Moreno 已提交
587
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
588
	}
J
Joao Moreno 已提交
589

J
Joao Moreno 已提交
590 591
	@command('git.revertSelectedRanges', { diff: true })
	async revertSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
592 593 594 595 596 597 598 599 600 601 602 603 604
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

		if (modifiedUri.scheme !== 'file') {
			return;
		}

J
Joao Moreno 已提交
605
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
		const originalDocument = await workspace.openTextDocument(originalUri);
		const selections = textEditor.selections;
		const selectedDiffs = diffs.filter(diff => {
			const modifiedRange = diff.modifiedEndLineNumber === 0
				? new Range(modifiedDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end, modifiedDocument.lineAt(diff.modifiedStartLineNumber).range.start)
				: new Range(modifiedDocument.lineAt(diff.modifiedStartLineNumber - 1).range.start, modifiedDocument.lineAt(diff.modifiedEndLineNumber - 1).range.end);

			return selections.every(selection => !selection.intersection(modifiedRange));
		});

		if (selectedDiffs.length === diffs.length) {
			return;
		}

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

629
		const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);
J
Joao Moreno 已提交
630 631 632 633 634
		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 已提交
635 636
	@command('git.unstage')
	async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
637
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
638
			const resource = this.getSCMResource();
639 640 641 642 643 644 645 646

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

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

654
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
655
		await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
J
Joao Moreno 已提交
656 657
	}

J
Joao Moreno 已提交
658 659
	@command('git.unstageAll', { repository: true })
	async unstageAll(repository: Repository): Promise<void> {
660
		await repository.revert([]);
J
Joao Moreno 已提交
661
	}
J
Joao Moreno 已提交
662

J
Joao Moreno 已提交
663 664
	@command('git.unstageSelectedRanges', { diff: true })
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
665 666 667 668 669 670 671 672 673
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

674 675 676 677 678 679 680
		if (modifiedUri.scheme !== 'git') {
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
J
Joao Moreno 已提交
681 682 683
			return;
		}

J
Joao Moreno 已提交
684
		const originalUri = toGitUri(modifiedUri, 'HEAD');
J
Joao Moreno 已提交
685
		const originalDocument = await workspace.openTextDocument(originalUri);
686 687 688 689
		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 已提交
690 691 692 693 694

		if (!selectedDiffs.length) {
			return;
		}

695 696
		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
J
Joao Moreno 已提交
697

J
Joao Moreno 已提交
698
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
699 700
	}

J
Joao Moreno 已提交
701 702
	@command('git.clean')
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
703
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
704
			const resource = this.getSCMResource();
705 706 707 708 709 710 711 712

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

J
Joao Moreno 已提交
716
		if (!scmResources.length) {
J
Joao Moreno 已提交
717 718
			return;
		}
J
Joao Moreno 已提交
719

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

J
Joao Moreno 已提交
724
		if (scmResources.length === 1) {
J
Joao Moreno 已提交
725
			if (untrackedCount > 0) {
J
Joao Moreno 已提交
726
				message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
727 728
				yes = localize('delete file', "Delete file");
			} else {
J
Joao Moreno 已提交
729
				message = localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
730 731
			}
		} else {
J
Joao Moreno 已提交
732
			message = localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", scmResources.length);
J
Joao Moreno 已提交
733 734 735 736 737

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

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

J
Joao Moreno 已提交
741 742 743 744
		if (pick !== yes) {
			return;
		}

J
Joao Moreno 已提交
745
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
746
		await this.runByRepository(resources, async (repository, resources) => repository.clean(resources));
J
Joao Moreno 已提交
747
	}
J
Joao Moreno 已提交
748

J
Joao Moreno 已提交
749 750
	@command('git.cleanAll', { repository: true })
	async cleanAll(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
751
		let resources = repository.workingTreeGroup.resourceStates;
J
Joao Moreno 已提交
752 753 754 755 756

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

J
Joao Moreno 已提交
757 758
		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 已提交
759

J
Joao Moreno 已提交
760 761 762 763 764 765
		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 已提交
766
				: localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
767
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
768

J
Joao Moreno 已提交
769
			if (pick !== yes) {
J
Joao Moreno 已提交
770 771 772
				return;
			}

J
Joao 已提交
773
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
774 775 776 777 778 779 780 781
			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 已提交
782 783
			}

J
Joao 已提交
784
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
785 786 787 788
		} 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 已提交
789

J
Joao Moreno 已提交
790 791 792
			if (pick !== yes) {
				return;
			}
J
Joao Moreno 已提交
793

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

J
Joao Moreno 已提交
796 797 798 799
		} 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 已提交
800

J
Joao Moreno 已提交
801 802 803 804 805
			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 已提交
806

J
Joao Moreno 已提交
807
			const yesAll = localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
808 809 810 811 812 813 814 815
			const pick = await window.showWarningMessage(message, { modal: true }, yesTracked, yesAll);

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

J
Joao 已提交
816
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
817
		}
J
Joao Moreno 已提交
818 819
	}

J
Joao Moreno 已提交
820
	private async smartCommit(
J
Joao Moreno 已提交
821
		repository: Repository,
822
		getCommitMessage: () => Promise<string | undefined>,
J
Joao Moreno 已提交
823 824
		opts?: CommitOptions
	): Promise<boolean> {
825 826
		const config = workspace.getConfiguration('git');
		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
827
		const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
J
Joao Moreno 已提交
828 829
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
830 831

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

J
Joao Moreno 已提交
834 835
			// 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?");
836 837 838 839 840
			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 已提交
841 842 843
				config.update('enableSmartCommit', true, true);
			} else if (pick !== yes) {
				return false; // do not commit on cancel
844 845 846
			}
		}

J
Joao Moreno 已提交
847
		if (!opts) {
848
			opts = { all: noStagedChanges };
J
Joao Moreno 已提交
849 850
		}

851 852 853
		// enable signing of commits if configurated
		opts.signCommit = enableCommitSigning;

J
Joao Moreno 已提交
854 855
		if (
			// no changes
856
			(noStagedChanges && noUnstagedChanges)
J
Joao Moreno 已提交
857
			// or no staged changes and not `all`
858
			|| (!opts.all && noStagedChanges)
J
Joao Moreno 已提交
859
		) {
J
Joao Moreno 已提交
860 861 862 863
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
864
		const message = await getCommitMessage();
J
Joao Moreno 已提交
865 866 867 868 869

		if (!message) {
			return false;
		}

J
Joao Moreno 已提交
870
		await repository.commit(message, opts);
J
Joao Moreno 已提交
871 872 873 874

		return true;
	}

J
Joao Moreno 已提交
875
	private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
J
Joao Moreno 已提交
876
		const message = repository.inputBox.value;
J
Joao Moreno 已提交
877
		const getCommitMessage = async () => {
J
Joao Moreno 已提交
878 879 880 881 882 883
			if (message) {
				return message;
			}

			return await window.showInputBox({
				placeHolder: localize('commit message', "Commit message"),
J
Joao Moreno 已提交
884 885
				prompt: localize('provide commit message', "Please provide a commit message"),
				ignoreFocusOut: true
J
Joao Moreno 已提交
886
			});
J
Joao Moreno 已提交
887 888
		};

J
Joao Moreno 已提交
889
		const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
J
Joao Moreno 已提交
890 891

		if (message && didCommit) {
J
Joao Moreno 已提交
892
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
893
		}
J
Joao Moreno 已提交
894 895
	}

J
Joao Moreno 已提交
896
	@command('git.commit', { repository: true })
J
Joao Moreno 已提交
897 898
	async commit(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository);
J
Joao Moreno 已提交
899 900
	}

J
Joao Moreno 已提交
901
	@command('git.commitWithInput', { repository: true })
J
Joao Moreno 已提交
902
	async commitWithInput(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
903
		if (!repository.inputBox.value) {
J
Joao Moreno 已提交
904 905 906
			return;
		}

J
Joao Moreno 已提交
907
		const didCommit = await this.smartCommit(repository, async () => repository.inputBox.value);
J
Joao Moreno 已提交
908 909

		if (didCommit) {
J
Joao Moreno 已提交
910
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
911
		}
J
Joao Moreno 已提交
912 913
	}

J
Joao Moreno 已提交
914
	@command('git.commitStaged', { repository: true })
J
Joao Moreno 已提交
915 916
	async commitStaged(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false });
J
Joao Moreno 已提交
917 918
	}

J
Joao Moreno 已提交
919
	@command('git.commitStagedSigned', { repository: true })
J
Joao Moreno 已提交
920 921
	async commitStagedSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, signoff: true });
J
Joao Moreno 已提交
922 923
	}

J
Joao Moreno 已提交
924
	@command('git.commitStagedAmend', { repository: true })
J
Joao Moreno 已提交
925 926
	async commitStagedAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, amend: true });
K
Krzysztof Cieślak 已提交
927 928
	}

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

J
Joao Moreno 已提交
934
	@command('git.commitAllSigned', { repository: true })
J
Joao Moreno 已提交
935 936
	async commitAllSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, signoff: true });
J
Joao Moreno 已提交
937 938
	}

J
Joao Moreno 已提交
939
	@command('git.commitAllAmend', { repository: true })
J
Joao Moreno 已提交
940 941
	async commitAllAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, amend: true });
K
Krzysztof Cieślak 已提交
942 943
	}

J
Joao Moreno 已提交
944
	@command('git.undoCommit', { repository: true })
J
Joao Moreno 已提交
945 946
	async undoCommit(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
947 948 949 950 951

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

J
Joao Moreno 已提交
952 953
		const commit = await repository.getCommit('HEAD');
		await repository.reset('HEAD~');
J
Joao Moreno 已提交
954
		repository.inputBox.value = commit.message;
J
Joao Moreno 已提交
955 956
	}

J
Joao Moreno 已提交
957
	@command('git.checkout', { repository: true })
J
Joao Moreno 已提交
958
	async checkout(repository: Repository, treeish: string): Promise<void> {
J
Joao Moreno 已提交
959
		if (typeof treeish === 'string') {
J
Joao Moreno 已提交
960
			return await repository.checkout(treeish);
J
Joao Moreno 已提交
961 962
		}

J
Joao Moreno 已提交
963
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
964
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
965 966 967
		const includeTags = checkoutType === 'all' || checkoutType === 'tags';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
968
		const createBranch = new CreateBranchItem(this);
J
Joao Moreno 已提交
969

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

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

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

J
Joao Moreno 已提交
979
		const picks = [createBranch, ...heads, ...tags, ...remoteHeads];
980
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
J
Joao Moreno 已提交
981
		const choice = await window.showQuickPick(picks, { placeHolder });
J
Joao Moreno 已提交
982 983 984 985 986

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
987
		await choice.run(repository);
J
Joao Moreno 已提交
988 989
	}

J
Joao Moreno 已提交
990
	@command('git.branch', { repository: true })
J
Joao Moreno 已提交
991
	async branch(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
992
		const result = await window.showInputBox({
J
Joao Moreno 已提交
993
			placeHolder: localize('branch name', "Branch name"),
J
Joao Moreno 已提交
994 995
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true
J
Joao Moreno 已提交
996
		});
J
Joao Moreno 已提交
997

J
Joao Moreno 已提交
998 999 1000
		if (!result) {
			return;
		}
J
Joao Moreno 已提交
1001

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

J
Joao Moreno 已提交
1006
	@command('git.deleteBranch', { repository: true })
J
Joao Moreno 已提交
1007
	async deleteBranch(repository: Repository, name: string, force?: boolean): Promise<void> {
1008 1009
		let run: (force?: boolean) => Promise<void>;
		if (typeof name === 'string') {
J
Joao Moreno 已提交
1010
			run = force => repository.deleteBranch(name, force);
1011
		} else {
J
Joao Moreno 已提交
1012 1013
			const currentHead = repository.HEAD && repository.HEAD.name;
			const heads = repository.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead)
1014
				.map(ref => new BranchDeleteItem(ref));
M
Maik Riechert 已提交
1015

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

M
Maik Riechert 已提交
1019
			if (!choice || !choice.branchName) {
1020 1021
				return;
			}
M
Maik Riechert 已提交
1022
			name = choice.branchName;
J
Joao Moreno 已提交
1023
			run = force => choice.run(repository, force);
M
Maik Riechert 已提交
1024 1025
		}

1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040
		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 已提交
1041 1042
	}

J
Joao Moreno 已提交
1043
	@command('git.merge', { repository: true })
J
Joao Moreno 已提交
1044
	async merge(repository: Repository): Promise<void> {
1045 1046 1047 1048
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string>('checkoutType') || 'all';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1049
		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
1050 1051
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1052

J
Joao Moreno 已提交
1053
		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
1054 1055
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1056 1057

		const picks = [...heads, ...remoteHeads];
1058 1059
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
1060 1061 1062 1063 1064

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1065
		try {
J
Joao Moreno 已提交
1066
			await choice.run(repository);
J
Joao Moreno 已提交
1067 1068 1069 1070 1071 1072 1073 1074
		} 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);
		}
1075 1076
	}

J
Joao Moreno 已提交
1077
	@command('git.createTag', { repository: true })
J
Joao Moreno 已提交
1078
	async createTag(repository: Repository): Promise<void> {
1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090
		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 已提交
1091
			prompt: localize('provide tag message', "Please provide a message to annotate the tag"),
1092 1093 1094 1095 1096
			ignoreFocusOut: true
		});

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

J
Joao Moreno 已提交
1100
	@command('git.pullFrom', { repository: true })
J
Joao Moreno 已提交
1101 1102
	async pullFrom(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126

		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 已提交
1127
		repository.pull(false, pick.label, branchName);
1128 1129
	}

J
Joao Moreno 已提交
1130
	@command('git.pull', { repository: true })
J
Joao Moreno 已提交
1131 1132
	async pull(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1133 1134 1135 1136 1137 1138

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

J
Joao Moreno 已提交
1139
		await repository.pull();
J
Joao Moreno 已提交
1140 1141
	}

J
Joao Moreno 已提交
1142
	@command('git.pullRebase', { repository: true })
J
Joao Moreno 已提交
1143 1144
	async pullRebase(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1145 1146 1147 1148 1149 1150

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

J
Joao Moreno 已提交
1151
		await repository.pullWithRebase();
J
Joao Moreno 已提交
1152 1153
	}

J
Joao Moreno 已提交
1154
	@command('git.push', { repository: true })
J
Joao Moreno 已提交
1155 1156
	async push(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1157 1158 1159 1160 1161 1162

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

J
Joao Moreno 已提交
1163
		await repository.push();
J
Joao Moreno 已提交
1164 1165
	}

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

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

J
Joao Moreno 已提交
1175
		await repository.pushTags();
1176 1177 1178 1179

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

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

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

J
Joao Moreno 已提交
1189
		if (!repository.HEAD || !repository.HEAD.name) {
J
Joao Moreno 已提交
1190 1191 1192 1193
			window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			return;
		}

J
Joao Moreno 已提交
1194
		const branchName = repository.HEAD.name;
J
Joao Moreno 已提交
1195 1196 1197 1198 1199 1200 1201 1202
		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 已提交
1203
		repository.pushTo(pick.label, branchName);
J
Joao Moreno 已提交
1204 1205
	}

J
Joao Moreno 已提交
1206
	@command('git.sync', { repository: true })
J
Joao Moreno 已提交
1207 1208
	async sync(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229

		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 已提交
1230
		await repository.sync();
J
Joao Moreno 已提交
1231 1232
	}

J
Joao 已提交
1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245
	@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 已提交
1246
	@command('git.publish', { repository: true })
J
Joao Moreno 已提交
1247 1248
	async publish(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1249 1250 1251 1252 1253 1254

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

J
Joao Moreno 已提交
1255
		const branchName = repository.HEAD && repository.HEAD.name || '';
M
Markus Wolf 已提交
1256 1257 1258 1259 1260 1261
		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 已提交
1262 1263 1264 1265 1266

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1267
		await repository.pushTo(choice, branchName, true);
J
Joao Moreno 已提交
1268 1269
	}

J
Joao Moreno 已提交
1270
	@command('git.showOutput')
J
Joao Moreno 已提交
1271 1272 1273 1274
	showOutput(): void {
		this.outputChannel.show();
	}

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

1280
			if (!resource) {
J
Joao Moreno 已提交
1281 1282 1283
				return;
			}

1284
			resourceStates = [resource];
J
Joao Moreno 已提交
1285 1286
		}

1287
		const resources = resourceStates
J
Joao Moreno 已提交
1288 1289 1290
			.filter(s => s instanceof Resource)
			.map(r => r.resourceUri);

1291
		if (!resources.length) {
N
NKumar2 已提交
1292 1293 1294
			return;
		}

1295
		await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
N
NKumar2 已提交
1296 1297
	}

J
Joao Moreno 已提交
1298
	@command('git.stash', { repository: true })
J
Joao Moreno 已提交
1299 1300
	async stash(repository: Repository): Promise<void> {
		if (repository.workingTreeGroup.resourceStates.length === 0) {
K
Krzysztof Cieślak 已提交
1301 1302 1303
			window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
			return;
		}
J
Joao Moreno 已提交
1304 1305 1306 1307 1308 1309 1310 1311 1312 1313

		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 已提交
1314
		await repository.createStash(message);
K
Krzysztof Cieślak 已提交
1315 1316
	}

J
Joao Moreno 已提交
1317
	@command('git.stashPop', { repository: true })
J
Joao Moreno 已提交
1318 1319
	async stashPop(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
1320 1321

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1322 1323 1324 1325
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

J
Joao Moreno 已提交
1326 1327
		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 已提交
1328 1329 1330 1331 1332
		const choice = await window.showQuickPick(picks, { placeHolder });

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

J
Joao Moreno 已提交
1334
		await repository.popStash(choice.id);
K
Krzysztof Cieślak 已提交
1335 1336
	}

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

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1342 1343 1344 1345
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

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

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

J
Joao Moreno 已提交
1353
			if (!options.repository) {
J
Joao Moreno 已提交
1354 1355
				result = Promise.resolve(method.apply(this, args));
			} else {
J
Joao Moreno 已提交
1356 1357
				// try to guess the repository based on the first argument
				const repository = this.model.getRepository(args[0]);
J
Joao Moreno 已提交
1358 1359 1360 1361 1362 1363 1364 1365 1366
				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();
				}
1367

J
Joao Moreno 已提交
1368
				result = repositoryPromise.then(repository => {
J
Joao Moreno 已提交
1369
					if (!repository) {
J
Joao 已提交
1370
						return Promise.resolve();
J
Joao Moreno 已提交
1371 1372
					}

J
Joao Moreno 已提交
1373
					return Promise.resolve(method.apply(this, [repository, ...args]));
J
Joao Moreno 已提交
1374
				});
J
Joao Moreno 已提交
1375 1376
			}

K
kieferrm 已提交
1377
			/* __GDPR__
K
kieferrm 已提交
1378 1379 1380 1381
				"git.command" : {
					"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
J
Joao Moreno 已提交
1382 1383
			this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

J
Joao Moreno 已提交
1384 1385 1386 1387
			return result.catch(async err => {
				let message: string;

				switch (err.gitErrorCode) {
1388
					case GitErrorCodes.DirtyWorkTree:
J
Joao Moreno 已提交
1389 1390
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
1391 1392 1393
					case GitErrorCodes.PushRejected:
						message = localize('cant push', "Can't push refs to remote. Run 'Pull' first to integrate your changes.");
						break;
J
Joao Moreno 已提交
1394
					default:
1395 1396 1397
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
J
Joao Moreno 已提交
1398
							.split(/[\r\n]/)
1399 1400 1401 1402 1403 1404
							.filter(line => !!line)
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422

						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();
				}
			});
		};
1423 1424 1425 1426 1427

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

		return result;
J
Joao Moreno 已提交
1428 1429
	}

1430
	private getSCMResource(uri?: Uri): Resource | undefined {
1431
		uri = uri ? uri : window.activeTextEditor && window.activeTextEditor.document.uri;
J
Joao Moreno 已提交
1432 1433

		if (!uri) {
1434
			return undefined;
J
Joao Moreno 已提交
1435 1436 1437
		}

		if (uri.scheme === 'git') {
J
Joao Moreno 已提交
1438 1439
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
1440 1441 1442 1443
		}

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

J
Joao Moreno 已提交
1446
			if (!repository) {
1447 1448
				return undefined;
			}
J
Joao Moreno 已提交
1449

J
Joao Moreno 已提交
1450 1451
			return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
				|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
J
Joao Moreno 已提交
1452 1453 1454
		}
	}

J
Joao Moreno 已提交
1455
	private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;
J
Joao Moreno 已提交
1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485
	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 已提交
1486 1487 1488
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
Joao Moreno 已提交
1489
}