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

'use strict';

8
import { Uri, commands, scm, 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';
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);
256 257
		window.withProgress({location: ProgressLocation.SourceControl, title: localize('cloning', "Cloning git repository...")}, () => clonePromise);
		window.withProgress({location: ProgressLocation.Window, title: localize('cloning', "Cloning git repository...")}, () => clonePromise);
J
Joao Moreno 已提交
258

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

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

J
Joao Moreno 已提交
285
	@command('git.openFile')
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
	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 已提交
306 307
		}

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

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

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

J
Joao Moreno 已提交
317 318
	@command('git.openHEADFile')
	async openHEADFile(arg?: Resource | Uri): Promise<void> {
D
Duroktar 已提交
319 320 321 322 323 324 325 326 327 328 329 330 331 332
		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 已提交
333
		const HEAD = this.getLeftResource(resource);
D
Duroktar 已提交
334

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

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

J
Joao Moreno 已提交
343
	@command('git.openChange')
344 345 346 347 348 349 350 351
	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 已提交
352 353 354
			resource = this.getSCMResource();
		}

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

361 362 363
	@command('git.openFileFromUri')
	async openFileFromUri(uri?: Uri): Promise<void> {
		const resource = this.getSCMResource(uri);
J
Joao Moreno 已提交
364 365 366 367 368 369 370 371 372 373
		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;
		}
374

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

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

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

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

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

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

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

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

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

		if (!textEditor) {
			return;
		}

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

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

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

		if (!selectedDiffs.length) {
			return;
		}

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

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

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

		if (!textEditor) {
			return;
		}

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

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

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

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

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

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

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

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

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

		if (!textEditor) {
			return;
		}

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

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

		const { ref } = fromGitUri(modifiedUri);

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

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

		if (!selectedDiffs.length) {
			return;
		}

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

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

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

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

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

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

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

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

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

J
Joao Moreno 已提交
585
	@command('git.cleanAll')
J
Joao Moreno 已提交
586
	async cleanAll(): Promise<void> {
587 588
		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 已提交
589
		const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
590 591 592 593 594

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

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

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

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

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

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

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

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

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

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

		return true;
	}

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

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

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

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

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

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

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

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

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

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

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

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

J
Joao Moreno 已提交
708
	@command('git.undoCommit')
J
Joao Moreno 已提交
709
	async undoCommit(): Promise<void> {
J
Joao Moreno 已提交
710 711 712 713 714 715 716 717 718
		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 已提交
719 720
	}

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

J
Joao Moreno 已提交
727
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
728
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
729 730 731 732 733 734 735 736 737 738 739 740
		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 已提交
741
		const picks = [...heads, ...tags, ...remoteHeads];
742
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
J
Joao Moreno 已提交
743
		const choice = await window.showQuickPick<CheckoutItem>(picks, { placeHolder });
J
Joao Moreno 已提交
744 745 746 747 748 749

		if (!choice) {
			return;
		}

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

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

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

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

M
Maik Riechert 已提交
768
	@command('git.deleteBranch')
769 770 771 772 773 774 775 776
	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 已提交
777

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

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

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

805 806 807 808 809 810 811
	@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 已提交
812 813
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
814 815

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

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

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
827 828 829 830 831 832 833 834 835 836
		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);
		}
837 838
	}

J
Joao Moreno 已提交
839
	@command('git.pullFrom')
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 868
	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 已提交
869
	@command('git.pull')
J
Joao Moreno 已提交
870
	async pull(): Promise<void> {
J
Joao Moreno 已提交
871 872 873 874 875 876 877 878
		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 已提交
879 880
	}

J
Joao Moreno 已提交
881
	@command('git.pullRebase')
J
Joao Moreno 已提交
882
	async pullRebase(): Promise<void> {
J
Joao Moreno 已提交
883 884 885 886 887 888 889
		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 已提交
890
		await this.model.pullWithRebase();
J
Joao Moreno 已提交
891 892
	}

J
Joao Moreno 已提交
893
	@command('git.push')
J
Joao Moreno 已提交
894
	async push(): Promise<void> {
J
Joao Moreno 已提交
895 896 897 898 899 900 901 902
		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 已提交
903 904
	}

J
Joao Moreno 已提交
905
	@command('git.pushTo')
J
Joao Moreno 已提交
906
	async pushTo(): Promise<void> {
J
Joao Moreno 已提交
907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927
		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 已提交
928
		this.model.pushTo(pick.label, branchName);
J
Joao Moreno 已提交
929 930
	}

J
Joao Moreno 已提交
931
	@command('git.sync')
J
Joao Moreno 已提交
932
	async sync(): Promise<void> {
J
Joao Moreno 已提交
933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954
		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 已提交
955 956 957
		await this.model.sync();
	}

J
Joao Moreno 已提交
958
	@command('git.publish')
J
Joao Moreno 已提交
959
	async publish(): Promise<void> {
J
Joao Moreno 已提交
960 961 962 963 964 965 966
		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 已提交
967 968
		const branchName = this.model.HEAD && this.model.HEAD.name || '';
		const picks = this.model.remotes.map(r => r.name);
J
Joao Moreno 已提交
969
		const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
J
Joao Moreno 已提交
970 971 972 973 974 975
		const choice = await window.showQuickPick(picks, { placeHolder });

		if (!choice) {
			return;
		}

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

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

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

J
Joao Moreno 已提交
989 990 991 992 993 994 995 996 997 998 999 1000
			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 已提交
1001 1002 1003
			return;
		}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

J
Joao Moreno 已提交
1079 1080
			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 已提交
1081 1082 1083
		}
	}

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