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

'use strict';

J
Joao Moreno 已提交
8
import { Uri, commands, 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
	}

D
Duroktar 已提交
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
	@command('git.openOldFile')
	async openOldFile(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 {
			resource = this.getSCMResource();
		}

		if (!resource) {
			return;
		}
		return await this._openOldResource(resource);
	}

	private async _openOldResource(resource: Resource): Promise<void> {
		const old = this.getLeftResource(resource);
		const current = this.getRightResource(resource);

		if (!old) {
			return await commands.executeCommand<void>('vscode.open', current);
		}
		return await commands.executeCommand<void>('vscode.open', old);
	}

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

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

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

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

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

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

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

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

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

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

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

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

		if (!textEditor) {
			return;
		}

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

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

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

		if (!selectedDiffs.length) {
			return;
		}

439 440
		const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);

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

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

		if (!textEditor) {
			return;
		}

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

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

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

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

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

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

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

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

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

		if (!textEditor) {
			return;
		}

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

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

		const { ref } = fromGitUri(modifiedUri);

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

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

		if (!selectedDiffs.length) {
			return;
		}

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

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

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

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		return true;
	}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		if (!choice) {
			return;
		}

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

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

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

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

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

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

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

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

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

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

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

		if (!choice) {
			return;
		}

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

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

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

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

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

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

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

		if (!choice) {
			return;
		}

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

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

N
NKumar2 已提交
986 987 988 989 990 991 992 993 994 995 996 997
	@command('git.ignore')
	async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
		const resources = resourceStates
			.filter(s => s instanceof Resource) as Resource[];

		if (!resources.length) {
			return;
		}

		await this.model.ignore(resources);
	}

J
Joao Moreno 已提交
998
	private createCommand(id: string, key: string, method: Function, skipModelCheck: boolean): (...args: any[]) => any {
999
		const result = (...args) => {
J
Joao Moreno 已提交
1000
			if (!skipModelCheck && !this.model) {
J
Joao Moreno 已提交
1001 1002 1003 1004
				window.showInformationMessage(localize('disabled', "Git is either disabled or not supported in this workspace"));
				return;
			}

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

J
Joao Moreno 已提交
1007 1008 1009 1010 1011 1012
			const result = Promise.resolve(method.apply(this, args));

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

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

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047

						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();
				}
			});
		};
1048 1049 1050 1051 1052

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

		return result;
J
Joao Moreno 已提交
1053 1054
	}

1055 1056
	private getSCMResource(uri?: Uri): Resource | undefined {
		uri = uri ? uri : window.activeTextEditor && window.activeTextEditor.document.uri;
J
Joao Moreno 已提交
1057 1058

		if (!uri) {
1059
			return undefined;
J
Joao Moreno 已提交
1060 1061 1062
		}

		if (uri.scheme === 'git') {
J
Joao Moreno 已提交
1063 1064
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
1065 1066 1067 1068 1069
		}

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

J
Joao Moreno 已提交
1070 1071
			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 已提交
1072 1073 1074
		}
	}

J
Joao Moreno 已提交
1075 1076 1077
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
Joao Moreno 已提交
1078
}