commands.ts 30.7 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, scm, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn } from 'vscode';
J
Joao Moreno 已提交
9
import { Ref, RefType, Git, GitErrorCodes, Branch } from './git';
10
import { Model, Resource, Status, CommitOptions, WorkingTreeGroup, IndexGroup, MergeGroup } from './model';
J
Joao Moreno 已提交
11
import { toGitUri, fromGitUri } from './uri';
12
import { applyLineChanges, intersectDiffWithRange, toLineRanges, invertLineChange } from './staging';
J
Joao Moreno 已提交
13
import * as path from 'path';
J
Joao Moreno 已提交
14
import * as os from 'os';
J
Joao Moreno 已提交
15
import TelemetryReporter from 'vscode-extension-telemetry';
J
Joao Moreno 已提交
16 17 18
import * as nls from 'vscode-nls';

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

J
Joao Moreno 已提交
20 21 22 23 24 25 26
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 已提交
27
	constructor(protected ref: Ref) { }
J
Joao Moreno 已提交
28 29 30 31 32 33 34 35 36 37 38 39 40 41

	async run(model: Model): Promise<void> {
		const ref = this.treeish;

		if (!ref) {
			return;
		}

		await model.checkout(ref);
	}
}

class CheckoutTagItem extends CheckoutItem {

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

class CheckoutRemoteHeadItem extends CheckoutItem {

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

	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 已提交
63 64
class BranchDeleteItem implements QuickPickItem {

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

70
	constructor(private ref: Ref) { }
M
Maik Riechert 已提交
71

72 73
	async run(model: Model, force?: boolean): Promise<void> {
		if (!this.branchName) {
M
Maik Riechert 已提交
74 75
			return;
		}
76
		await model.deleteBranch(this.branchName, force);
M
Maik Riechert 已提交
77 78 79
	}
}

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

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

92 93 94 95 96 97 98 99 100
interface Command {
	commandId: string;
	key: string;
	method: Function;
	skipModelCheck: boolean;
	requiresDiffInformation: boolean;
}

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

102
function command(commandId: string, skipModelCheck = false, requiresDiffInformation = false): Function {
J
Joao Moreno 已提交
103
	return (target: any, key: string, descriptor: any) => {
J
Joao Moreno 已提交
104 105 106 107
		if (!(typeof descriptor.value === 'function')) {
			throw new Error('not supported');
		}

108
		Commands.push({ commandId, key, method: descriptor.value, skipModelCheck, requiresDiffInformation });
J
Joao Moreno 已提交
109 110
	};
}
J
Joao Moreno 已提交
111

J
Joao Moreno 已提交
112
export class CommandCenter {
J
Joao Moreno 已提交
113

J
Joao Moreno 已提交
114
	private model: Model;
J
Joao Moreno 已提交
115
	private disposables: Disposable[];
J
Joao Moreno 已提交
116

J
Joao Moreno 已提交
117
	constructor(
J
Joao Moreno 已提交
118
		private git: Git,
J
Joao Moreno 已提交
119
		model: Model | undefined,
J
Joao Moreno 已提交
120 121
		private outputChannel: OutputChannel,
		private telemetryReporter: TelemetryReporter
J
Joao Moreno 已提交
122
	) {
J
Joao Moreno 已提交
123 124 125 126
		if (model) {
			this.model = model;
		}

J
Joao Moreno 已提交
127
		this.disposables = Commands
128 129 130 131 132 133 134 135 136
			.map(({ commandId, key, method, skipModelCheck, requiresDiffInformation }) => {
				const command = this.createCommand(commandId, key, method, skipModelCheck);

				if (requiresDiffInformation) {
					return commands.registerDiffInformationCommand(commandId, command);
				} else {
					return commands.registerCommand(commandId, command);
				}
			});
J
Joao Moreno 已提交
137 138
	}

J
Joao Moreno 已提交
139
	@command('git.refresh')
J
Joao Moreno 已提交
140
	async refresh(): Promise<void> {
J
Joao Moreno 已提交
141
		await this.model.status();
J
Joao Moreno 已提交
142
	}
J
Joao Moreno 已提交
143

J
Joao Moreno 已提交
144 145 146 147 148 149
	@command('git.openResource')
	async openResource(resource: Resource): Promise<void> {
		await this._openResource(resource);
	}

	private async _openResource(resource: Resource): Promise<void> {
J
Joao Moreno 已提交
150 151 152 153
		const left = this.getLeftResource(resource);
		const right = this.getRightResource(resource);
		const title = this.getTitle(resource);

J
Joao Moreno 已提交
154 155 156 157 158
		if (!right) {
			// TODO
			console.error('oh no');
			return;
		}
J
Joao Moreno 已提交
159

J
Joao Moreno 已提交
160 161
		const viewColumn = window.activeTextEditor && window.activeTextEditor.viewColumn || ViewColumn.One;

J
Joao Moreno 已提交
162
		if (!left) {
J
Joao Moreno 已提交
163
			return await commands.executeCommand<void>('vscode.open', right, viewColumn);
J
Joao Moreno 已提交
164 165
		}

J
Joao Moreno 已提交
166 167 168 169 170 171
		const opts: TextDocumentShowOptions = {
			preview: true,
			viewColumn
		};

		return await commands.executeCommand<void>('vscode.diff', left, right, title, opts);
J
Joao Moreno 已提交
172 173 174 175 176 177
	}

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

			case Status.MODIFIED:
J
Joao Moreno 已提交
181
				return toGitUri(resource.resourceUri, '~');
J
Joao Moreno 已提交
182
		}
J
Joao Moreno 已提交
183
	}
J
Joao Moreno 已提交
184

J
Joao Moreno 已提交
185 186 187 188 189 190
	private getRightResource(resource: Resource): Uri | undefined {
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_ADDED:
			case Status.INDEX_COPIED:
			case Status.INDEX_RENAMED:
J
Joao Moreno 已提交
191
				return toGitUri(resource.resourceUri, '');
J
Joao Moreno 已提交
192 193 194

			case Status.INDEX_DELETED:
			case Status.DELETED:
J
Joao Moreno 已提交
195
				return toGitUri(resource.resourceUri, 'HEAD');
J
Joao Moreno 已提交
196 197 198 199

			case Status.MODIFIED:
			case Status.UNTRACKED:
			case Status.IGNORED:
J
Joao Moreno 已提交
200 201
				const uriString = resource.resourceUri.toString();
				const [indexStatus] = this.model.indexGroup.resources.filter(r => r.resourceUri.toString() === uriString);
J
Joao Moreno 已提交
202

J
Joao Moreno 已提交
203 204
				if (indexStatus && indexStatus.renameResourceUri) {
					return indexStatus.renameResourceUri;
J
Joao Moreno 已提交
205 206
				}

J
Joao Moreno 已提交
207
				return resource.resourceUri;
J
Joao Moreno 已提交
208

J
Joao Moreno 已提交
209
			case Status.BOTH_MODIFIED:
J
Joao Moreno 已提交
210
				return resource.resourceUri;
J
Joao Moreno 已提交
211 212 213 214
		}
	}

	private getTitle(resource: Resource): string {
J
Joao Moreno 已提交
215
		const basename = path.basename(resource.resourceUri.fsPath);
J
Joao Moreno 已提交
216 217 218 219 220 221 222 223 224 225 226 227 228

		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
				return `${basename} (Index)`;

			case Status.MODIFIED:
				return `${basename} (Working Tree)`;
		}

		return '';
	}

229 230
	@command('git.clone', true)
	async clone(): Promise<void> {
J
Joao Moreno 已提交
231
		const url = await window.showInputBox({
J
Joao Moreno 已提交
232 233
			prompt: localize('repourl', "Repository URL"),
			ignoreFocusOut: true
J
Joao Moreno 已提交
234 235 236
		});

		if (!url) {
237 238
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
			return;
J
Joao Moreno 已提交
239 240
		}

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

J
Joao Moreno 已提交
244 245
		const parentPath = await window.showInputBox({
			prompt: localize('parent', "Parent Directory"),
J
Joao Moreno 已提交
246
			value,
J
Joao Moreno 已提交
247
			ignoreFocusOut: true
J
Joao Moreno 已提交
248 249 250
		});

		if (!parentPath) {
251 252
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
			return;
J
Joao Moreno 已提交
253 254
		}

J
Joao Moreno 已提交
255
		const clonePromise = this.git.clone(url, parentPath);
J
Joao Moreno 已提交
256 257
		window.setStatusBarMessage(localize('cloning', "Cloning git repository..."), clonePromise);

258
		try {
259 260 261 262 263 264 265 266 267 268
			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;
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: openFolder ? 1 : 0 });
			if (openFolder) {
				commands.executeCommand('vscode.openFolder', Uri.file(repositoryPath));
			}
269 270
		} catch (err) {
			if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
271 272 273
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
			} else {
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
274 275
			}
			throw err;
J
Joao Moreno 已提交
276 277 278
		}
	}

J
Joao Moreno 已提交
279 280 281 282 283
	@command('git.init')
	async init(): Promise<void> {
		await this.model.init();
	}

J
Joao Moreno 已提交
284
	@command('git.openFile')
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
	async openFile(arg?: Resource | Uri): Promise<void> {
		let uri: Uri | undefined;

		if (arg instanceof Uri) {
			if (arg.scheme === 'git') {
				uri = Uri.file(fromGitUri(arg).path);
			} else if (arg.scheme === 'file') {
				uri = arg;
			}
		} else {
			let resource = arg;

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

			if (resource) {
				uri = resource.resourceUri;
			}
J
Joao Moreno 已提交
305 306
		}

307
		if (!uri) {
J
Joao Moreno 已提交
308
			return;
J
Joao Moreno 已提交
309 310
		}

J
Joao Moreno 已提交
311 312 313
		const viewColumn = window.activeTextEditor && window.activeTextEditor.viewColumn || ViewColumn.One;

		return await commands.executeCommand<void>('vscode.open', uri, viewColumn);
J
Joao Moreno 已提交
314 315
	}

J
Joao Moreno 已提交
316 317
	@command('git.openHEADFile')
	async openHEADFile(arg?: Resource | Uri): Promise<void> {
D
Duroktar 已提交
318 319 320 321 322 323 324 325 326 327 328 329 330 331
		let resource: Resource | undefined = undefined;

		if (arg instanceof Resource) {
			resource = arg;
		} else if (arg instanceof Uri) {
			resource = this.getSCMResource(arg);
		} else {
			resource = this.getSCMResource();
		}

		if (!resource) {
			return;
		}

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

J
Joao Moreno 已提交
334 335 336
		if (!HEAD) {
			window.showWarningMessage(localize('HEAD not available', "HEAD version of '{0}' is not available.", path.basename(resource.resourceUri.fsPath)));
			return;
D
Duroktar 已提交
337
		}
J
Joao Moreno 已提交
338 339

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

J
Joao Moreno 已提交
342
	@command('git.openChange')
343 344 345 346 347 348 349 350
	async openChange(arg?: Resource | Uri): Promise<void> {
		let resource: Resource | undefined = undefined;

		if (arg instanceof Resource) {
			resource = arg;
		} else if (arg instanceof Uri) {
			resource = this.getSCMResource(arg);
		} else {
J
Joao Moreno 已提交
351 352 353
			resource = this.getSCMResource();
		}

J
Joao Moreno 已提交
354 355
		if (!resource) {
			return;
J
Joao Moreno 已提交
356
		}
J
Joao Moreno 已提交
357
		return await this._openResource(resource);
J
Joao Moreno 已提交
358 359
	}

360 361 362
	@command('git.openFileFromUri')
	async openFileFromUri(uri?: Uri): Promise<void> {
		const resource = this.getSCMResource(uri);
J
Joao Moreno 已提交
363 364 365 366 367 368 369 370 371 372
		let uriToOpen: Uri | undefined;

		if (resource) {
			uriToOpen = resource.resourceUri;
		} else if (uri && uri.scheme === 'git') {
			const { path } = fromGitUri(uri);
			uriToOpen = Uri.file(path);
		} else if (uri && uri.scheme === 'file') {
			uriToOpen = uri;
		}
373

J
Joao Moreno 已提交
374
		if (!uriToOpen) {
375 376 377
			return;
		}

J
Joao Moreno 已提交
378 379 380
		const viewColumn = window.activeTextEditor && window.activeTextEditor.viewColumn || ViewColumn.One;

		return await commands.executeCommand<void>('vscode.open', uriToOpen, viewColumn);
381 382
	}

J
Joao Moreno 已提交
383
	@command('git.stage')
384
	async stage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
385
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
386
			const resource = this.getSCMResource();
387 388 389 390 391 392 393 394

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

395 396 397
		const resources = resourceStates
			.filter(s => s instanceof Resource && (s.resourceGroup instanceof WorkingTreeGroup || s.resourceGroup instanceof MergeGroup)) as Resource[];

398
		if (!resources.length) {
J
Joao Moreno 已提交
399 400
			return;
		}
J
Joao Moreno 已提交
401

402
		return await this.model.add(...resources);
J
Joao Moreno 已提交
403 404
	}

J
Joao Moreno 已提交
405
	@command('git.stageAll')
J
Joao Moreno 已提交
406
	async stageAll(): Promise<void> {
J
Joao Moreno 已提交
407 408 409
		return await this.model.add();
	}

410 411
	@command('git.stageSelectedRanges', false, true)
	async stageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
412 413 414 415 416 417 418 419 420
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

J
Joao Moreno 已提交
421
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
422 423 424
			return;
		}

J
Joao Moreno 已提交
425
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
426
		const originalDocument = await workspace.openTextDocument(originalUri);
427 428 429 430
		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 已提交
431 432 433 434 435

		if (!selectedDiffs.length) {
			return;
		}

436 437
		const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);

J
Joao Moreno 已提交
438
		await this.model.stage(modifiedUri, result);
J
Joao Moreno 已提交
439
	}
J
Joao Moreno 已提交
440

441 442
	@command('git.revertSelectedRanges', false, true)
	async revertSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
443 444 445 446 447 448 449 450 451 452 453 454 455
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

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

J
Joao Moreno 已提交
456
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
		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;
		}

480
		const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);
J
Joao Moreno 已提交
481 482 483 484 485
		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 已提交
486
	@command('git.unstage')
487
	async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
488
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
489
			const resource = this.getSCMResource();
490 491 492 493 494 495 496 497

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

498 499 500
		const resources = resourceStates
			.filter(s => s instanceof Resource && s.resourceGroup instanceof IndexGroup) as Resource[];

501
		if (!resources.length) {
J
Joao Moreno 已提交
502 503 504
			return;
		}

505
		return await this.model.revertFiles(...resources);
J
Joao Moreno 已提交
506 507
	}

J
Joao Moreno 已提交
508
	@command('git.unstageAll')
J
Joao Moreno 已提交
509
	async unstageAll(): Promise<void> {
J
Joao Moreno 已提交
510
		return await this.model.revertFiles();
J
Joao Moreno 已提交
511
	}
J
Joao Moreno 已提交
512

513 514
	@command('git.unstageSelectedRanges', false, true)
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
515 516 517 518 519 520 521 522 523
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

524 525 526 527 528 529 530
		if (modifiedUri.scheme !== 'git') {
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
J
Joao Moreno 已提交
531 532 533
			return;
		}

J
Joao Moreno 已提交
534
		const originalUri = toGitUri(modifiedUri, 'HEAD');
J
Joao Moreno 已提交
535
		const originalDocument = await workspace.openTextDocument(originalUri);
536 537 538 539
		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 已提交
540 541 542 543 544

		if (!selectedDiffs.length) {
			return;
		}

545 546
		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
J
Joao Moreno 已提交
547 548 549 550

		await this.model.stage(modifiedUri, result);
	}

J
Joao Moreno 已提交
551
	@command('git.clean')
552
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
553
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
554
			const resource = this.getSCMResource();
555 556 557 558 559 560 561 562

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

563 564 565
		const resources = resourceStates
			.filter(s => s instanceof Resource && s.resourceGroup instanceof WorkingTreeGroup) as Resource[];

566
		if (!resources.length) {
J
Joao Moreno 已提交
567 568
			return;
		}
J
Joao Moreno 已提交
569

570
		const message = resources.length === 1
J
Joao Moreno 已提交
571
			? localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(resources[0].resourceUri.fsPath))
572 573
			: localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", resources.length);

574
		const yes = localize('discard', "Discard Changes");
J
Joao Moreno 已提交
575
		const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
576

J
Joao Moreno 已提交
577 578 579 580
		if (pick !== yes) {
			return;
		}

581
		await this.model.clean(...resources);
J
Joao Moreno 已提交
582
	}
J
Joao Moreno 已提交
583

J
Joao Moreno 已提交
584
	@command('git.cleanAll')
J
Joao Moreno 已提交
585
	async cleanAll(): Promise<void> {
586 587
		const message = localize('confirm discard all', "Are you sure you want to discard ALL changes? This is IRREVERSIBLE!");
		const yes = localize('discardAll', "Discard ALL Changes");
J
Joao Moreno 已提交
588
		const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
589 590 591 592 593

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

J
Joao Moreno 已提交
594
		await this.model.clean(...this.model.workingTreeGroup.resources);
J
Joao Moreno 已提交
595 596
	}

J
Joao Moreno 已提交
597
	private async smartCommit(
598
		getCommitMessage: () => Promise<string | undefined>,
J
Joao Moreno 已提交
599 600
		opts?: CommitOptions
	): Promise<boolean> {
601 602 603
		const config = workspace.getConfiguration('git');
		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
		const noStagedChanges = this.model.indexGroup.resources.length === 0;
604
		const noUnstagedChanges = this.model.workingTreeGroup.resources.length === 0;
605 606

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

J
Joao Moreno 已提交
609 610
			// 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?");
611 612 613 614 615
			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 已提交
616 617 618
				config.update('enableSmartCommit', true, true);
			} else if (pick !== yes) {
				return false; // do not commit on cancel
619 620 621
			}
		}

J
Joao Moreno 已提交
622
		if (!opts) {
623
			opts = { all: noStagedChanges };
J
Joao Moreno 已提交
624 625 626 627
		}

		if (
			// no changes
628
			(noStagedChanges && noUnstagedChanges)
J
Joao Moreno 已提交
629
			// or no staged changes and not `all`
630
			|| (!opts.all && noStagedChanges)
J
Joao Moreno 已提交
631
		) {
J
Joao Moreno 已提交
632 633 634 635
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
636
		const message = await getCommitMessage();
J
Joao Moreno 已提交
637 638 639 640 641 642

		if (!message) {
			// TODO@joao: show modal dialog to confirm empty message commit
			return false;
		}

J
Joao Moreno 已提交
643
		await this.model.commit(message, opts);
J
Joao Moreno 已提交
644 645 646 647

		return true;
	}

J
Joao Moreno 已提交
648
	private async commitWithAnyInput(opts?: CommitOptions): Promise<void> {
649
		const message = scm.inputBox.value;
J
Joao Moreno 已提交
650
		const getCommitMessage = async () => {
J
Joao Moreno 已提交
651 652 653 654 655 656
			if (message) {
				return message;
			}

			return await window.showInputBox({
				placeHolder: localize('commit message', "Commit message"),
J
Joao Moreno 已提交
657 658
				prompt: localize('provide commit message', "Please provide a commit message"),
				ignoreFocusOut: true
J
Joao Moreno 已提交
659
			});
J
Joao Moreno 已提交
660 661 662
		};

		const didCommit = await this.smartCommit(getCommitMessage, opts);
J
Joao Moreno 已提交
663 664

		if (message && didCommit) {
J
Joao Moreno 已提交
665
			scm.inputBox.value = await this.model.getCommitTemplate();
J
Joao Moreno 已提交
666
		}
J
Joao Moreno 已提交
667 668
	}

J
Joao Moreno 已提交
669
	@command('git.commit')
J
Joao Moreno 已提交
670 671 672 673
	async commit(): Promise<void> {
		await this.commitWithAnyInput();
	}

J
Joao Moreno 已提交
674
	@command('git.commitWithInput')
J
Joao Moreno 已提交
675
	async commitWithInput(): Promise<void> {
J
Joao Moreno 已提交
676 677 678 679
		if (!scm.inputBox.value) {
			return;
		}

J
Joao Moreno 已提交
680
		const didCommit = await this.smartCommit(async () => scm.inputBox.value);
J
Joao Moreno 已提交
681 682

		if (didCommit) {
J
Joao Moreno 已提交
683
			scm.inputBox.value = await this.model.getCommitTemplate();
J
Joao Moreno 已提交
684
		}
J
Joao Moreno 已提交
685 686
	}

J
Joao Moreno 已提交
687
	@command('git.commitStaged')
J
Joao Moreno 已提交
688
	async commitStaged(): Promise<void> {
J
Joao Moreno 已提交
689
		await this.commitWithAnyInput({ all: false });
J
Joao Moreno 已提交
690 691
	}

J
Joao Moreno 已提交
692
	@command('git.commitStagedSigned')
J
Joao Moreno 已提交
693
	async commitStagedSigned(): Promise<void> {
J
Joao Moreno 已提交
694
		await this.commitWithAnyInput({ all: false, signoff: true });
J
Joao Moreno 已提交
695 696
	}

J
Joao Moreno 已提交
697
	@command('git.commitAll')
J
Joao Moreno 已提交
698
	async commitAll(): Promise<void> {
J
Joao Moreno 已提交
699
		await this.commitWithAnyInput({ all: true });
J
Joao Moreno 已提交
700 701
	}

J
Joao Moreno 已提交
702
	@command('git.commitAllSigned')
J
Joao Moreno 已提交
703
	async commitAllSigned(): Promise<void> {
J
Joao Moreno 已提交
704
		await this.commitWithAnyInput({ all: true, signoff: true });
J
Joao Moreno 已提交
705 706
	}

J
Joao Moreno 已提交
707
	@command('git.undoCommit')
J
Joao Moreno 已提交
708
	async undoCommit(): Promise<void> {
J
Joao Moreno 已提交
709 710 711 712 713 714 715 716 717
		const HEAD = this.model.HEAD;

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

		const commit = await this.model.getCommit('HEAD');
		await this.model.reset('HEAD~');
		scm.inputBox.value = commit.message;
J
Joao Moreno 已提交
718 719
	}

J
Joao Moreno 已提交
720
	@command('git.checkout')
J
Joao Moreno 已提交
721 722 723 724 725
	async checkout(treeish: string): Promise<void> {
		if (typeof treeish === 'string') {
			return await this.model.checkout(treeish);
		}

J
Joao Moreno 已提交
726
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
727
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
728 729 730 731 732 733 734 735 736 737 738 739
		const includeTags = checkoutType === 'all' || checkoutType === 'tags';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

		const heads = this.model.refs.filter(ref => ref.type === RefType.Head)
			.map(ref => new CheckoutItem(ref));

		const tags = (includeTags ? this.model.refs.filter(ref => ref.type === RefType.Tag) : [])
			.map(ref => new CheckoutTagItem(ref));

		const remoteHeads = (includeRemotes ? this.model.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
			.map(ref => new CheckoutRemoteHeadItem(ref));

J
Joao Moreno 已提交
740
		const picks = [...heads, ...tags, ...remoteHeads];
741
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
J
Joao Moreno 已提交
742
		const choice = await window.showQuickPick<CheckoutItem>(picks, { placeHolder });
J
Joao Moreno 已提交
743 744 745 746 747 748

		if (!choice) {
			return;
		}

		await choice.run(this.model);
J
Joao Moreno 已提交
749 750
	}

J
Joao Moreno 已提交
751
	@command('git.branch')
J
Joao Moreno 已提交
752 753
	async branch(): Promise<void> {
		const result = await window.showInputBox({
J
Joao Moreno 已提交
754
			placeHolder: localize('branch name', "Branch name"),
J
Joao Moreno 已提交
755 756
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true
J
Joao Moreno 已提交
757
		});
J
Joao Moreno 已提交
758

J
Joao Moreno 已提交
759 760 761
		if (!result) {
			return;
		}
J
Joao Moreno 已提交
762

J
Joao Moreno 已提交
763 764
		const name = result.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-');
		await this.model.branch(name);
J
Joao Moreno 已提交
765 766
	}

M
Maik Riechert 已提交
767
	@command('git.deleteBranch')
768 769 770 771 772 773 774 775
	async deleteBranch(name: string, force?: boolean): Promise<void> {
		let run: (force?: boolean) => Promise<void>;
		if (typeof name === 'string') {
			run = force => this.model.deleteBranch(name, force);
		} else {
			const currentHead = this.model.HEAD && this.model.HEAD.name;
			const heads = this.model.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead)
				.map(ref => new BranchDeleteItem(ref));
M
Maik Riechert 已提交
776

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

M
Maik Riechert 已提交
780
			if (!choice || !choice.branchName) {
781 782
				return;
			}
M
Maik Riechert 已提交
783
			name = choice.branchName;
784
			run = force => choice.run(this.model, force);
M
Maik Riechert 已提交
785 786
		}

787 788 789 790 791 792 793 794 795 796 797 798 799 800 801
		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 已提交
802 803
	}

804 805 806 807 808 809 810
	@command('git.merge')
	async merge(): Promise<void> {
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string>('checkoutType') || 'all';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

		const heads = this.model.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
811 812
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
813 814

		const remoteHeads = (includeRemotes ? this.model.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
815 816
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
817 818

		const picks = [...heads, ...remoteHeads];
819 820
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
821 822 823 824 825

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
826 827 828 829 830 831 832 833 834 835
		try {
			await choice.run(this.model);
		} 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);
		}
836 837
	}

J
Joao Moreno 已提交
838
	@command('git.pullFrom')
839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867
	async pullFrom(): Promise<void> {
		const remotes = this.model.remotes;

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

		this.model.pull(false, pick.label, branchName);
	}

J
Joao Moreno 已提交
868
	@command('git.pull')
J
Joao Moreno 已提交
869
	async pull(): Promise<void> {
J
Joao Moreno 已提交
870 871 872 873 874 875 876 877
		const remotes = this.model.remotes;

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

		await this.model.pull();
J
Joao Moreno 已提交
878 879
	}

J
Joao Moreno 已提交
880
	@command('git.pullRebase')
J
Joao Moreno 已提交
881
	async pullRebase(): Promise<void> {
J
Joao Moreno 已提交
882 883 884 885 886 887 888
		const remotes = this.model.remotes;

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

J
Joao Moreno 已提交
889
		await this.model.pullWithRebase();
J
Joao Moreno 已提交
890 891
	}

J
Joao Moreno 已提交
892
	@command('git.push')
J
Joao Moreno 已提交
893
	async push(): Promise<void> {
J
Joao Moreno 已提交
894 895 896 897 898 899 900 901
		const remotes = this.model.remotes;

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

		await this.model.push();
J
Joao Moreno 已提交
902 903
	}

J
Joao Moreno 已提交
904
	@command('git.pushTo')
J
Joao Moreno 已提交
905
	async pushTo(): Promise<void> {
J
Joao Moreno 已提交
906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926
		const remotes = this.model.remotes;

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

		if (!this.model.HEAD || !this.model.HEAD.name) {
			window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			return;
		}

		const branchName = this.model.HEAD.name;
		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 已提交
927
		this.model.pushTo(pick.label, branchName);
J
Joao Moreno 已提交
928 929
	}

J
Joao Moreno 已提交
930
	@command('git.sync')
J
Joao Moreno 已提交
931
	async sync(): Promise<void> {
J
Joao Moreno 已提交
932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953
		const HEAD = this.model.HEAD;

		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 已提交
954 955 956
		await this.model.sync();
	}

J
Joao Moreno 已提交
957
	@command('git.publish')
J
Joao Moreno 已提交
958
	async publish(): Promise<void> {
J
Joao Moreno 已提交
959 960 961 962 963 964 965
		const remotes = this.model.remotes;

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

J
Joao Moreno 已提交
966 967
		const branchName = this.model.HEAD && this.model.HEAD.name || '';
		const picks = this.model.remotes.map(r => r.name);
J
Joao Moreno 已提交
968
		const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
J
Joao Moreno 已提交
969 970 971 972 973 974
		const choice = await window.showQuickPick(picks, { placeHolder });

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
975
		await this.model.pushTo(choice, branchName, true);
J
Joao Moreno 已提交
976 977
	}

J
Joao Moreno 已提交
978
	@command('git.showOutput')
J
Joao Moreno 已提交
979 980 981 982
	showOutput(): void {
		this.outputChannel.show();
	}

N
NKumar2 已提交
983 984
	@command('git.ignore')
	async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
985 986
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
			const uri = window.activeTextEditor && window.activeTextEditor.document.uri;
N
NKumar2 已提交
987

J
Joao Moreno 已提交
988 989 990 991 992 993 994 995 996 997 998 999
			if (!uri) {
				return;
			}

			return await this.model.ignore([uri]);
		}

		const uris = resourceStates
			.filter(s => s instanceof Resource)
			.map(r => r.resourceUri);

		if (!uris.length) {
N
NKumar2 已提交
1000 1001 1002
			return;
		}

J
Joao Moreno 已提交
1003
		await this.model.ignore(uris);
N
NKumar2 已提交
1004 1005
	}

J
Joao Moreno 已提交
1006
	private createCommand(id: string, key: string, method: Function, skipModelCheck: boolean): (...args: any[]) => any {
1007
		const result = (...args) => {
J
Joao Moreno 已提交
1008
			if (!skipModelCheck && !this.model) {
J
Joao Moreno 已提交
1009 1010 1011 1012
				window.showInformationMessage(localize('disabled', "Git is either disabled or not supported in this workspace"));
				return;
			}

J
Joao Moreno 已提交
1013 1014
			this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

J
Joao Moreno 已提交
1015 1016 1017 1018 1019 1020
			const result = Promise.resolve(method.apply(this, args));

			return result.catch(async err => {
				let message: string;

				switch (err.gitErrorCode) {
1021
					case GitErrorCodes.DirtyWorkTree:
J
Joao Moreno 已提交
1022 1023
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
1024 1025 1026
					case GitErrorCodes.PushRejected:
						message = localize('cant push', "Can't push refs to remote. Run 'Pull' first to integrate your changes.");
						break;
J
Joao Moreno 已提交
1027
					default:
1028 1029 1030
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
J
Joao Moreno 已提交
1031
							.split(/[\r\n]/)
1032 1033 1034 1035 1036 1037
							.filter(line => !!line)
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055

						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();
				}
			});
		};
1056 1057 1058 1059 1060

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

		return result;
J
Joao Moreno 已提交
1061 1062
	}

1063 1064
	private getSCMResource(uri?: Uri): Resource | undefined {
		uri = uri ? uri : window.activeTextEditor && window.activeTextEditor.document.uri;
J
Joao Moreno 已提交
1065 1066

		if (!uri) {
1067
			return undefined;
J
Joao Moreno 已提交
1068 1069 1070
		}

		if (uri.scheme === 'git') {
J
Joao Moreno 已提交
1071 1072
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
1073 1074 1075 1076 1077
		}

		if (uri.scheme === 'file') {
			const uriString = uri.toString();

J
Joao Moreno 已提交
1078 1079
			return this.model.workingTreeGroup.resources.filter(r => r.resourceUri.toString() === uriString)[0]
				|| this.model.indexGroup.resources.filter(r => r.resourceUri.toString() === uriString)[0];
J
Joao Moreno 已提交
1080 1081 1082
		}
	}

J
Joao Moreno 已提交
1083 1084 1085
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
Joao Moreno 已提交
1086
}