commands.ts 28.6 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';
9
import { Ref, RefType, Git, GitErrorCodes } 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 88 89

	async run(model: Model): Promise<void> {
		await model.merge(this.ref.name);
	}
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 316
	}

	@command('git.openChange')
317 318 319 320 321 322 323 324
	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 已提交
325 326 327
			resource = this.getSCMResource();
		}

J
Joao Moreno 已提交
328 329
		if (!resource) {
			return;
J
Joao Moreno 已提交
330 331
		}

J
Joao Moreno 已提交
332
		return await this._openResource(resource);
J
Joao Moreno 已提交
333 334
	}

335 336 337
	@command('git.openFileFromUri')
	async openFileFromUri(uri?: Uri): Promise<void> {
		const resource = this.getSCMResource(uri);
J
Joao Moreno 已提交
338 339 340 341 342 343 344 345 346 347
		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;
		}
348

J
Joao Moreno 已提交
349
		if (!uriToOpen) {
350 351 352
			return;
		}

J
Joao Moreno 已提交
353 354 355
		const viewColumn = window.activeTextEditor && window.activeTextEditor.viewColumn || ViewColumn.One;

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

J
Joao Moreno 已提交
358
	@command('git.stage')
359
	async stage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
360
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
361
			const resource = this.getSCMResource();
362 363 364 365 366 367 368 369

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

370 371 372
		const resources = resourceStates
			.filter(s => s instanceof Resource && (s.resourceGroup instanceof WorkingTreeGroup || s.resourceGroup instanceof MergeGroup)) as Resource[];

373
		if (!resources.length) {
J
Joao Moreno 已提交
374 375
			return;
		}
J
Joao Moreno 已提交
376

377
		return await this.model.add(...resources);
J
Joao Moreno 已提交
378 379
	}

J
Joao Moreno 已提交
380
	@command('git.stageAll')
J
Joao Moreno 已提交
381
	async stageAll(): Promise<void> {
J
Joao Moreno 已提交
382 383 384
		return await this.model.add();
	}

385 386
	@command('git.stageSelectedRanges', false, true)
	async stageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
387 388 389 390 391 392 393 394 395
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

J
Joao Moreno 已提交
396
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
397 398 399
			return;
		}

J
Joao Moreno 已提交
400
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
401
		const originalDocument = await workspace.openTextDocument(originalUri);
402 403 404 405
		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 已提交
406 407 408 409 410

		if (!selectedDiffs.length) {
			return;
		}

411 412
		const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);

J
Joao Moreno 已提交
413
		await this.model.stage(modifiedUri, result);
J
Joao Moreno 已提交
414
	}
J
Joao Moreno 已提交
415

416 417
	@command('git.revertSelectedRanges', false, true)
	async revertSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
418 419 420 421 422 423 424 425 426 427 428 429 430
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

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

J
Joao Moreno 已提交
431
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
		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;
		}

455
		const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);
J
Joao Moreno 已提交
456 457 458 459 460
		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 已提交
461
	@command('git.unstage')
462
	async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
463
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
464
			const resource = this.getSCMResource();
465 466 467 468 469 470 471 472

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

473 474 475
		const resources = resourceStates
			.filter(s => s instanceof Resource && s.resourceGroup instanceof IndexGroup) as Resource[];

476
		if (!resources.length) {
J
Joao Moreno 已提交
477 478 479
			return;
		}

480
		return await this.model.revertFiles(...resources);
J
Joao Moreno 已提交
481 482
	}

J
Joao Moreno 已提交
483
	@command('git.unstageAll')
J
Joao Moreno 已提交
484
	async unstageAll(): Promise<void> {
J
Joao Moreno 已提交
485
		return await this.model.revertFiles();
J
Joao Moreno 已提交
486
	}
J
Joao Moreno 已提交
487

488 489
	@command('git.unstageSelectedRanges', false, true)
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
490 491 492 493 494 495 496 497 498
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

499 500 501 502 503 504 505
		if (modifiedUri.scheme !== 'git') {
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
J
Joao Moreno 已提交
506 507 508
			return;
		}

J
Joao Moreno 已提交
509
		const originalUri = toGitUri(modifiedUri, 'HEAD');
J
Joao Moreno 已提交
510
		const originalDocument = await workspace.openTextDocument(originalUri);
511 512 513 514
		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 已提交
515 516 517 518 519

		if (!selectedDiffs.length) {
			return;
		}

520 521
		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
J
Joao Moreno 已提交
522 523 524 525

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

J
Joao Moreno 已提交
526
	@command('git.clean')
527
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
528
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
529
			const resource = this.getSCMResource();
530 531 532 533 534 535 536 537

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

538 539 540
		const resources = resourceStates
			.filter(s => s instanceof Resource && s.resourceGroup instanceof WorkingTreeGroup) as Resource[];

541
		if (!resources.length) {
J
Joao Moreno 已提交
542 543
			return;
		}
J
Joao Moreno 已提交
544

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

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

J
Joao Moreno 已提交
552 553 554 555
		if (pick !== yes) {
			return;
		}

556
		await this.model.clean(...resources);
J
Joao Moreno 已提交
557
	}
J
Joao Moreno 已提交
558

J
Joao Moreno 已提交
559
	@command('git.cleanAll')
J
Joao Moreno 已提交
560
	async cleanAll(): Promise<void> {
561 562
		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 已提交
563
		const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
564 565 566 567 568

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

J
Joao Moreno 已提交
569
		await this.model.clean(...this.model.workingTreeGroup.resources);
J
Joao Moreno 已提交
570 571
	}

J
Joao Moreno 已提交
572
	private async smartCommit(
573
		getCommitMessage: () => Promise<string | undefined>,
J
Joao Moreno 已提交
574 575
		opts?: CommitOptions
	): Promise<boolean> {
576 577 578
		const config = workspace.getConfiguration('git');
		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
		const noStagedChanges = this.model.indexGroup.resources.length === 0;
579
		const noUnstagedChanges = this.model.workingTreeGroup.resources.length === 0;
580 581

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

J
Joao Moreno 已提交
584 585
			// 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?");
586 587 588 589 590
			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 已提交
591 592 593
				config.update('enableSmartCommit', true, true);
			} else if (pick !== yes) {
				return false; // do not commit on cancel
594 595 596
			}
		}

J
Joao Moreno 已提交
597
		if (!opts) {
598
			opts = { all: noStagedChanges };
J
Joao Moreno 已提交
599 600 601 602
		}

		if (
			// no changes
603
			(noStagedChanges && noUnstagedChanges)
J
Joao Moreno 已提交
604
			// or no staged changes and not `all`
605
			|| (!opts.all && noStagedChanges)
J
Joao Moreno 已提交
606
		) {
J
Joao Moreno 已提交
607 608 609 610
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
611
		const message = await getCommitMessage();
J
Joao Moreno 已提交
612 613 614 615 616 617

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

J
Joao Moreno 已提交
618
		await this.model.commit(message, opts);
J
Joao Moreno 已提交
619 620 621 622

		return true;
	}

J
Joao Moreno 已提交
623
	private async commitWithAnyInput(opts?: CommitOptions): Promise<void> {
624
		const message = scm.inputBox.value;
J
Joao Moreno 已提交
625
		const getCommitMessage = async () => {
J
Joao Moreno 已提交
626 627 628 629 630 631
			if (message) {
				return message;
			}

			return await window.showInputBox({
				placeHolder: localize('commit message', "Commit message"),
J
Joao Moreno 已提交
632 633
				prompt: localize('provide commit message', "Please provide a commit message"),
				ignoreFocusOut: true
J
Joao Moreno 已提交
634
			});
J
Joao Moreno 已提交
635 636 637
		};

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

		if (message && didCommit) {
J
Joao Moreno 已提交
640
			scm.inputBox.value = await this.model.getCommitTemplate();
J
Joao Moreno 已提交
641
		}
J
Joao Moreno 已提交
642 643
	}

J
Joao Moreno 已提交
644
	@command('git.commit')
J
Joao Moreno 已提交
645 646 647 648
	async commit(): Promise<void> {
		await this.commitWithAnyInput();
	}

J
Joao Moreno 已提交
649
	@command('git.commitWithInput')
J
Joao Moreno 已提交
650
	async commitWithInput(): Promise<void> {
J
Joao Moreno 已提交
651 652 653 654
		if (!scm.inputBox.value) {
			return;
		}

J
Joao Moreno 已提交
655
		const didCommit = await this.smartCommit(async () => scm.inputBox.value);
J
Joao Moreno 已提交
656 657

		if (didCommit) {
J
Joao Moreno 已提交
658
			scm.inputBox.value = await this.model.getCommitTemplate();
J
Joao Moreno 已提交
659
		}
J
Joao Moreno 已提交
660 661
	}

J
Joao Moreno 已提交
662
	@command('git.commitStaged')
J
Joao Moreno 已提交
663
	async commitStaged(): Promise<void> {
J
Joao Moreno 已提交
664
		await this.commitWithAnyInput({ all: false });
J
Joao Moreno 已提交
665 666
	}

J
Joao Moreno 已提交
667
	@command('git.commitStagedSigned')
J
Joao Moreno 已提交
668
	async commitStagedSigned(): Promise<void> {
J
Joao Moreno 已提交
669
		await this.commitWithAnyInput({ all: false, signoff: true });
J
Joao Moreno 已提交
670 671
	}

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

J
Joao Moreno 已提交
677
	@command('git.commitAllSigned')
J
Joao Moreno 已提交
678
	async commitAllSigned(): Promise<void> {
J
Joao Moreno 已提交
679
		await this.commitWithAnyInput({ all: true, signoff: true });
J
Joao Moreno 已提交
680 681
	}

J
Joao Moreno 已提交
682
	@command('git.undoCommit')
J
Joao Moreno 已提交
683
	async undoCommit(): Promise<void> {
J
Joao Moreno 已提交
684 685 686 687 688 689 690 691 692
		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 已提交
693 694
	}

J
Joao Moreno 已提交
695
	@command('git.checkout')
J
Joao Moreno 已提交
696 697 698 699 700
	async checkout(treeish: string): Promise<void> {
		if (typeof treeish === 'string') {
			return await this.model.checkout(treeish);
		}

J
Joao Moreno 已提交
701
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
702
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
703 704 705 706 707 708 709 710 711 712 713 714
		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 已提交
715
		const picks = [...heads, ...tags, ...remoteHeads];
716
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
J
Joao Moreno 已提交
717
		const choice = await window.showQuickPick<CheckoutItem>(picks, { placeHolder });
J
Joao Moreno 已提交
718 719 720 721 722 723

		if (!choice) {
			return;
		}

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

J
Joao Moreno 已提交
726
	@command('git.branch')
J
Joao Moreno 已提交
727 728
	async branch(): Promise<void> {
		const result = await window.showInputBox({
J
Joao Moreno 已提交
729
			placeHolder: localize('branch name', "Branch name"),
J
Joao Moreno 已提交
730 731
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true
J
Joao Moreno 已提交
732
		});
J
Joao Moreno 已提交
733

J
Joao Moreno 已提交
734 735 736
		if (!result) {
			return;
		}
J
Joao Moreno 已提交
737

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

M
Maik Riechert 已提交
742
	@command('git.deleteBranch')
743 744 745 746 747 748 749 750
	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 已提交
751

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

M
Maik Riechert 已提交
755
			if (!choice || !choice.branchName) {
756 757
				return;
			}
M
Maik Riechert 已提交
758
			name = choice.branchName;
759
			run = force => choice.run(this.model, force);
M
Maik Riechert 已提交
760 761
		}

762 763 764 765 766 767 768 769 770 771 772 773 774 775 776
		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 已提交
777 778
	}

779 780 781 782 783 784 785
	@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)
786
			.map(ref => new MergeItem(ref));
787 788

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

		const picks = [...heads, ...remoteHeads];
792 793
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
794 795 796 797 798

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
799
		await choice.run(this.model);
800 801
	}

J
Joao Moreno 已提交
802
	@command('git.pull')
J
Joao Moreno 已提交
803
	async pull(): Promise<void> {
J
Joao Moreno 已提交
804 805 806 807 808 809 810 811
		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 已提交
812 813
	}

J
Joao Moreno 已提交
814
	@command('git.pullRebase')
J
Joao Moreno 已提交
815
	async pullRebase(): Promise<void> {
J
Joao Moreno 已提交
816 817 818 819 820 821 822
		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 已提交
823
		await this.model.pullWithRebase();
J
Joao Moreno 已提交
824 825
	}

J
Joao Moreno 已提交
826
	@command('git.push')
J
Joao Moreno 已提交
827
	async push(): Promise<void> {
J
Joao Moreno 已提交
828 829 830 831 832 833 834 835
		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 已提交
836 837
	}

J
Joao Moreno 已提交
838
	@command('git.pushTo')
J
Joao Moreno 已提交
839
	async pushTo(): Promise<void> {
J
Joao Moreno 已提交
840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860
		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 已提交
861
		this.model.pushTo(pick.label, branchName);
J
Joao Moreno 已提交
862 863
	}

J
Joao Moreno 已提交
864
	@command('git.sync')
J
Joao Moreno 已提交
865
	async sync(): Promise<void> {
J
Joao Moreno 已提交
866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887
		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 已提交
888 889 890
		await this.model.sync();
	}

J
Joao Moreno 已提交
891
	@command('git.publish')
J
Joao Moreno 已提交
892
	async publish(): Promise<void> {
J
Joao Moreno 已提交
893 894 895 896 897 898 899
		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 已提交
900 901
		const branchName = this.model.HEAD && this.model.HEAD.name || '';
		const picks = this.model.remotes.map(r => r.name);
J
Joao Moreno 已提交
902
		const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
J
Joao Moreno 已提交
903 904 905 906 907 908
		const choice = await window.showQuickPick(picks, { placeHolder });

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
909
		await this.model.pushTo(choice, branchName, true);
J
Joao Moreno 已提交
910 911
	}

J
Joao Moreno 已提交
912
	@command('git.showOutput')
J
Joao Moreno 已提交
913 914 915 916
	showOutput(): void {
		this.outputChannel.show();
	}

N
NKumar2 已提交
917 918 919 920 921 922 923 924 925 926 927 928
	@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 已提交
929
	private createCommand(id: string, key: string, method: Function, skipModelCheck: boolean): (...args: any[]) => any {
930
		const result = (...args) => {
J
Joao Moreno 已提交
931
			if (!skipModelCheck && !this.model) {
J
Joao Moreno 已提交
932 933 934 935
				window.showInformationMessage(localize('disabled', "Git is either disabled or not supported in this workspace"));
				return;
			}

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

J
Joao Moreno 已提交
938 939 940 941 942 943
			const result = Promise.resolve(method.apply(this, args));

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

				switch (err.gitErrorCode) {
944
					case GitErrorCodes.DirtyWorkTree:
J
Joao Moreno 已提交
945 946
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
947 948 949
					case GitErrorCodes.PushRejected:
						message = localize('cant push', "Can't push refs to remote. Run 'Pull' first to integrate your changes.");
						break;
J
Joao Moreno 已提交
950
					default:
951 952 953
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
J
Joao Moreno 已提交
954
							.split(/[\r\n]/)
955 956 957 958 959 960
							.filter(line => !!line)
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978

						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();
				}
			});
		};
979 980 981 982 983

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

		return result;
J
Joao Moreno 已提交
984 985
	}

986 987
	private getSCMResource(uri?: Uri): Resource | undefined {
		uri = uri ? uri : window.activeTextEditor && window.activeTextEditor.document.uri;
J
Joao Moreno 已提交
988 989

		if (!uri) {
990
			return undefined;
J
Joao Moreno 已提交
991 992 993
		}

		if (uri.scheme === 'git') {
J
Joao Moreno 已提交
994 995
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
996 997 998 999 1000
		}

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

J
Joao Moreno 已提交
1001 1002
			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 已提交
1003 1004 1005
		}
	}

J
Joao Moreno 已提交
1006 1007 1008
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
Joao Moreno 已提交
1009
}