commands.ts 23.1 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 } from 'vscode';
J
Joao Moreno 已提交
9
import { Ref, RefType, Git } from './git';
J
Joao Moreno 已提交
10
import { Model, Resource, Status, CommitOptions } from './model';
J
Joao Moreno 已提交
11
import * as staging from './staging';
J
Joao Moreno 已提交
12
import * as path from 'path';
J
Joao Moreno 已提交
13
import * as os from 'os';
14
import { uniqueFilter } from './util';
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;
	}
}

63 64 65 66 67 68 69 70 71
interface Command {
	commandId: string;
	key: string;
	method: Function;
	skipModelCheck: boolean;
	requiresDiffInformation: boolean;
}

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

73
function command(commandId: string, skipModelCheck = false, requiresDiffInformation = false): Function {
J
Joao Moreno 已提交
74
	return (target: any, key: string, descriptor: any) => {
J
Joao Moreno 已提交
75 76 77 78
		if (!(typeof descriptor.value === 'function')) {
			throw new Error('not supported');
		}

79
		Commands.push({ commandId, key, method: descriptor.value, skipModelCheck, requiresDiffInformation });
J
Joao Moreno 已提交
80 81
	};
}
J
Joao Moreno 已提交
82

J
Joao Moreno 已提交
83
export class CommandCenter {
J
Joao Moreno 已提交
84

J
Joao Moreno 已提交
85
	private model: Model;
J
Joao Moreno 已提交
86
	private disposables: Disposable[];
J
Joao Moreno 已提交
87

J
Joao Moreno 已提交
88
	constructor(
J
Joao Moreno 已提交
89
		private git: Git,
J
Joao Moreno 已提交
90
		model: Model | undefined,
J
Joao Moreno 已提交
91 92
		private outputChannel: OutputChannel,
		private telemetryReporter: TelemetryReporter
J
Joao Moreno 已提交
93
	) {
J
Joao Moreno 已提交
94 95 96 97
		if (model) {
			this.model = model;
		}

J
Joao Moreno 已提交
98
		this.disposables = Commands
99 100 101 102 103 104 105 106 107
			.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 已提交
108 109
	}

J
Joao Moreno 已提交
110
	@command('git.refresh')
J
Joao Moreno 已提交
111
	async refresh(): Promise<void> {
J
Joao Moreno 已提交
112
		await this.model.status();
J
Joao Moreno 已提交
113
	}
J
Joao Moreno 已提交
114

J
Joao Moreno 已提交
115 116 117 118 119
	async open(resource: Resource): Promise<void> {
		const left = this.getLeftResource(resource);
		const right = this.getRightResource(resource);
		const title = this.getTitle(resource);

J
Joao Moreno 已提交
120 121 122 123 124
		if (!right) {
			// TODO
			console.error('oh no');
			return;
		}
J
Joao Moreno 已提交
125

J
Joao Moreno 已提交
126
		if (!left) {
J
Joao Moreno 已提交
127
			return await commands.executeCommand<void>('vscode.open', right);
J
Joao Moreno 已提交
128 129
		}

J
Joao Moreno 已提交
130
		return await commands.executeCommand<void>('vscode.diff', left, right, title);
J
Joao Moreno 已提交
131 132 133 134 135 136
	}

	private getLeftResource(resource: Resource): Uri | undefined {
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
J
Joao Moreno 已提交
137
				return resource.original.with({ scheme: 'git', query: 'HEAD' });
J
Joao Moreno 已提交
138 139

			case Status.MODIFIED:
140
				return resource.sourceUri.with({ scheme: 'git', query: '~' });
J
Joao Moreno 已提交
141
		}
J
Joao Moreno 已提交
142
	}
J
Joao Moreno 已提交
143

J
Joao Moreno 已提交
144 145 146 147 148
	private getRightResource(resource: Resource): Uri | undefined {
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_ADDED:
			case Status.INDEX_COPIED:
149
				return resource.sourceUri.with({ scheme: 'git' });
J
Joao Moreno 已提交
150

J
Joao Moreno 已提交
151
			case Status.INDEX_RENAMED:
152
				return resource.sourceUri.with({ scheme: 'git' });
J
Joao Moreno 已提交
153 154 155

			case Status.INDEX_DELETED:
			case Status.DELETED:
156
				return resource.sourceUri.with({ scheme: 'git', query: 'HEAD' });
J
Joao Moreno 已提交
157 158 159 160

			case Status.MODIFIED:
			case Status.UNTRACKED:
			case Status.IGNORED:
161 162
				const uriString = resource.sourceUri.toString();
				const [indexStatus] = this.model.indexGroup.resources.filter(r => r.sourceUri.toString() === uriString);
J
Joao Moreno 已提交
163 164 165 166 167

				if (indexStatus && indexStatus.rename) {
					return indexStatus.rename;
				}

168
				return resource.sourceUri;
J
Joao Moreno 已提交
169

J
Joao Moreno 已提交
170
			case Status.BOTH_MODIFIED:
171
				return resource.sourceUri;
J
Joao Moreno 已提交
172 173 174 175
		}
	}

	private getTitle(resource: Resource): string {
176
		const basename = path.basename(resource.sourceUri.fsPath);
J
Joao Moreno 已提交
177 178 179 180 181 182 183 184 185 186 187 188 189

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

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

		return '';
	}

J
Joao Moreno 已提交
190
	private async _clone(): Promise<boolean> {
J
Joao Moreno 已提交
191
		const url = await window.showInputBox({
J
Joao Moreno 已提交
192 193
			prompt: localize('repourl', "Repository URL"),
			ignoreFocusOut: true
J
Joao Moreno 已提交
194 195 196
		});

		if (!url) {
J
Joao Moreno 已提交
197
			throw new Error('no_URL');
J
Joao Moreno 已提交
198 199 200 201
		}

		const parentPath = await window.showInputBox({
			prompt: localize('parent', "Parent Directory"),
J
Joao Moreno 已提交
202 203
			value: os.homedir(),
			ignoreFocusOut: true
J
Joao Moreno 已提交
204 205 206
		});

		if (!parentPath) {
J
Joao Moreno 已提交
207
			throw new Error('no_directory');
J
Joao Moreno 已提交
208 209
		}

J
Joao Moreno 已提交
210
		const clonePromise = this.git.clone(url, parentPath);
J
Joao Moreno 已提交
211
		window.setStatusBarMessage(localize('cloning', "Cloning git repository..."), clonePromise);
J
Joao Moreno 已提交
212
		let repositoryPath: string;
J
Joao Moreno 已提交
213

214
		try {
J
Joao Moreno 已提交
215
			repositoryPath = await clonePromise;
216 217
		} catch (err) {
			if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
J
Joao Moreno 已提交
218
				throw new Error('directory_not_empty');
219
			}
J
Joao Moreno 已提交
220

221
			throw err;
J
Joao Moreno 已提交
222
		}
J
Joao Moreno 已提交
223 224 225 226 227 228 229 230 231 232 233 234

		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;

		if (openFolder) {
			commands.executeCommand('vscode.openFolder', Uri.file(repositoryPath));
		}

		return openFolder;
	}

J
Joao Moreno 已提交
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
	/**
	 * Attempts to clone a git repository. Throws descriptive errors
	 * for usual error cases. Returns whether the user chose to open
	 * the resulting folder or otherwise.
	 *
	 * This only exists for the walkthrough contribution to have good
	 * telemetry.
	 *
	 * TODO@Christof: when all the telemetry questions are answered,
	 * please clean this up into a single clone method.
	 */
	@command('git.clone', true)
	async clone(): Promise<boolean> {
		return await this._clone();
	}

	@command('git.cloneSilent', true)
	async cloneSilent(): Promise<void> {
J
Joao Moreno 已提交
253 254 255 256 257
		try {
			await this._clone();
		} catch (err) {
			// noop
		}
J
Joao Moreno 已提交
258 259
	}

J
Joao Moreno 已提交
260 261 262 263 264
	@command('git.init')
	async init(): Promise<void> {
		await this.model.init();
	}

J
Joao Moreno 已提交
265
	@command('git.openFile')
J
Joao Moreno 已提交
266
	async openFile(uri?: Uri): Promise<void> {
267 268 269 270
		if (uri && uri.scheme === 'file') {
			return await commands.executeCommand<void>('vscode.open', uri);
		}

J
Joao Moreno 已提交
271
		const resource = this.resolveSCMResource(uri);
J
Joao Moreno 已提交
272

J
Joao Moreno 已提交
273 274
		if (!resource) {
			return;
J
Joao Moreno 已提交
275 276
		}

277
		return await commands.executeCommand<void>('vscode.open', resource.sourceUri);
J
Joao Moreno 已提交
278 279 280
	}

	@command('git.openChange')
J
Joao Moreno 已提交
281 282
	async openChange(uri?: Uri): Promise<void> {
		const resource = this.resolveSCMResource(uri);
J
Joao Moreno 已提交
283

J
Joao Moreno 已提交
284 285
		if (!resource) {
			return;
J
Joao Moreno 已提交
286 287
		}

J
Joao Moreno 已提交
288
		return await this.open(resource);
J
Joao Moreno 已提交
289 290
	}

J
Joao Moreno 已提交
291
	@command('git.stage')
292 293
	async stage(...uris: Uri[]): Promise<void> {
		const resources = this.toSCMResources(uris);
J
Joao Moreno 已提交
294

295
		if (!resources.length) {
J
Joao Moreno 已提交
296 297
			return;
		}
J
Joao Moreno 已提交
298

299
		return await this.model.add(...resources);
J
Joao Moreno 已提交
300 301
	}

J
Joao Moreno 已提交
302
	@command('git.stageAll')
J
Joao Moreno 已提交
303
	async stageAll(): Promise<void> {
J
Joao Moreno 已提交
304 305 306
		return await this.model.add();
	}

307 308
	@command('git.stageSelectedRanges', false, true)
	async stageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
309 310 311 312 313 314 315 316 317
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

J
Joao Moreno 已提交
318
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
319 320 321
			return;
		}

J
Joao Moreno 已提交
322
		const originalUri = modifiedUri.with({ scheme: 'git', query: '~' });
J
Joao Moreno 已提交
323 324 325 326
		const originalDocument = await workspace.openTextDocument(originalUri);
		const selections = textEditor.selections;
		const selectedDiffs = diffs.filter(diff => {
			const modifiedRange = diff.modifiedEndLineNumber === 0
327
				? new Range(modifiedDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end, modifiedDocument.lineAt(diff.modifiedStartLineNumber).range.start)
J
Joao Moreno 已提交
328 329 330 331 332 333 334 335 336 337 338
				: new Range(modifiedDocument.lineAt(diff.modifiedStartLineNumber - 1).range.start, modifiedDocument.lineAt(diff.modifiedEndLineNumber - 1).range.end);

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

		if (!selectedDiffs.length) {
			return;
		}

		const result = staging.applyChanges(originalDocument, modifiedDocument, selectedDiffs);
		await this.model.stage(modifiedUri, result);
J
Joao Moreno 已提交
339
	}
J
Joao Moreno 已提交
340

341 342
	@command('git.revertSelectedRanges', false, true)
	async revertSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

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

		const originalUri = modifiedUri.with({ scheme: 'git', query: '~' });
		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;
		}

		const result = staging.applyChanges(originalDocument, modifiedDocument, selectedDiffs);
		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 已提交
386
	@command('git.unstage')
387 388
	async unstage(...uris: Uri[]): Promise<void> {
		const resources = this.toSCMResources(uris);
J
Joao Moreno 已提交
389

390
		if (!resources.length) {
J
Joao Moreno 已提交
391 392 393
			return;
		}

394
		return await this.model.revertFiles(...resources);
J
Joao Moreno 已提交
395 396
	}

J
Joao Moreno 已提交
397
	@command('git.unstageAll')
J
Joao Moreno 已提交
398
	async unstageAll(): Promise<void> {
J
Joao Moreno 已提交
399
		return await this.model.revertFiles();
J
Joao Moreno 已提交
400
	}
J
Joao Moreno 已提交
401

402 403
	@command('git.unstageSelectedRanges', false, true)
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

		if (modifiedUri.scheme !== 'git' || modifiedUri.query !== '') {
			return;
		}

		const originalUri = modifiedUri.with({ scheme: 'git', query: 'HEAD' });
		const originalDocument = await workspace.openTextDocument(originalUri);
		const selections = textEditor.selections;
		const selectedDiffs = diffs.filter(diff => {
			const modifiedRange = diff.modifiedEndLineNumber === 0
				? new Range(diff.modifiedStartLineNumber - 1, 0, diff.modifiedStartLineNumber - 1, 0)
				: new Range(modifiedDocument.lineAt(diff.modifiedStartLineNumber - 1).range.start, modifiedDocument.lineAt(diff.modifiedEndLineNumber - 1).range.end);

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

		if (!selectedDiffs.length) {
			return;
		}

		const invertedDiffs = selectedDiffs.map(c => ({
			modifiedStartLineNumber: c.originalStartLineNumber,
			modifiedEndLineNumber: c.originalEndLineNumber,
			originalStartLineNumber: c.modifiedStartLineNumber,
			originalEndLineNumber: c.modifiedEndLineNumber
		}));

		const result = staging.applyChanges(modifiedDocument, originalDocument, invertedDiffs);
		await this.model.stage(modifiedUri, result);
	}

J
Joao Moreno 已提交
443
	@command('git.clean')
444 445
	async clean(...uris: Uri[]): Promise<void> {
		const resources = this.toSCMResources(uris);
J
Joao Moreno 已提交
446

447
		if (!resources.length) {
J
Joao Moreno 已提交
448 449
			return;
		}
J
Joao Moreno 已提交
450

451
		const message = resources.length === 1
452
			? localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(resources[0].sourceUri.fsPath))
453 454
			: localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", resources.length);

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

J
Joao Moreno 已提交
458 459 460 461
		if (pick !== yes) {
			return;
		}

462
		await this.model.clean(...resources);
J
Joao Moreno 已提交
463
	}
J
Joao Moreno 已提交
464

J
Joao Moreno 已提交
465
	@command('git.cleanAll')
J
Joao Moreno 已提交
466
	async cleanAll(): Promise<void> {
467 468
		const message = localize('confirm discard all', "Are you sure you want to discard ALL changes?");
		const yes = localize('discard', "Discard Changes");
J
Joao Moreno 已提交
469
		const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
470 471 472 473 474

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

J
Joao Moreno 已提交
475
		await this.model.clean(...this.model.workingTreeGroup.resources);
J
Joao Moreno 已提交
476 477
	}

J
Joao Moreno 已提交
478 479 480 481 482 483 484 485 486 487 488 489 490 491
	private async smartCommit(
		getCommitMessage: () => Promise<string>,
		opts?: CommitOptions
	): Promise<boolean> {
		if (!opts) {
			opts = { all: this.model.indexGroup.resources.length === 0 };
		}

		if (
			// no changes
			(this.model.indexGroup.resources.length === 0 && this.model.workingTreeGroup.resources.length === 0)
			// or no staged changes and not `all`
			|| (!opts.all && this.model.indexGroup.resources.length === 0)
		) {
J
Joao Moreno 已提交
492 493 494 495
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
496
		const message = await getCommitMessage();
J
Joao Moreno 已提交
497 498 499 500 501 502

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

J
Joao Moreno 已提交
503
		await this.model.commit(message, opts);
J
Joao Moreno 已提交
504 505 506 507

		return true;
	}

J
Joao Moreno 已提交
508
	private async commitWithAnyInput(opts?: CommitOptions): Promise<void> {
509
		const message = scm.inputBox.value;
J
Joao Moreno 已提交
510
		const getCommitMessage = async () => {
J
Joao Moreno 已提交
511 512 513 514 515 516
			if (message) {
				return message;
			}

			return await window.showInputBox({
				placeHolder: localize('commit message', "Commit message"),
J
Joao Moreno 已提交
517 518
				prompt: localize('provide commit message', "Please provide a commit message"),
				ignoreFocusOut: true
J
Joao Moreno 已提交
519
			});
J
Joao Moreno 已提交
520 521 522
		};

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

		if (message && didCommit) {
J
Joao Moreno 已提交
525
			scm.inputBox.value = await this.model.getCommitTemplate();
J
Joao Moreno 已提交
526
		}
J
Joao Moreno 已提交
527 528
	}

J
Joao Moreno 已提交
529
	@command('git.commit')
J
Joao Moreno 已提交
530 531 532 533
	async commit(): Promise<void> {
		await this.commitWithAnyInput();
	}

J
Joao Moreno 已提交
534
	@command('git.commitWithInput')
J
Joao Moreno 已提交
535
	async commitWithInput(): Promise<void> {
J
Joao Moreno 已提交
536
		const didCommit = await this.smartCommit(async () => scm.inputBox.value);
J
Joao Moreno 已提交
537 538

		if (didCommit) {
J
Joao Moreno 已提交
539
			scm.inputBox.value = await this.model.getCommitTemplate();
J
Joao Moreno 已提交
540
		}
J
Joao Moreno 已提交
541 542
	}

J
Joao Moreno 已提交
543
	@command('git.commitStaged')
J
Joao Moreno 已提交
544
	async commitStaged(): Promise<void> {
J
Joao Moreno 已提交
545
		await this.commitWithAnyInput({ all: false });
J
Joao Moreno 已提交
546 547
	}

J
Joao Moreno 已提交
548
	@command('git.commitStagedSigned')
J
Joao Moreno 已提交
549
	async commitStagedSigned(): Promise<void> {
J
Joao Moreno 已提交
550
		await this.commitWithAnyInput({ all: false, signoff: true });
J
Joao Moreno 已提交
551 552
	}

J
Joao Moreno 已提交
553
	@command('git.commitAll')
J
Joao Moreno 已提交
554
	async commitAll(): Promise<void> {
J
Joao Moreno 已提交
555
		await this.commitWithAnyInput({ all: true });
J
Joao Moreno 已提交
556 557
	}

J
Joao Moreno 已提交
558
	@command('git.commitAllSigned')
J
Joao Moreno 已提交
559
	async commitAllSigned(): Promise<void> {
J
Joao Moreno 已提交
560
		await this.commitWithAnyInput({ all: true, signoff: true });
J
Joao Moreno 已提交
561 562
	}

J
Joao Moreno 已提交
563
	@command('git.undoCommit')
J
Joao Moreno 已提交
564
	async undoCommit(): Promise<void> {
J
Joao Moreno 已提交
565 566 567 568 569 570 571 572 573
		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 已提交
574 575
	}

J
Joao Moreno 已提交
576
	@command('git.checkout')
J
Joao Moreno 已提交
577 578
	async checkout(): Promise<void> {
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
579
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
580 581 582 583 584 585 586 587 588 589 590 591
		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 已提交
592 593 594
		const picks = [...heads, ...tags, ...remoteHeads];
		const placeHolder = 'Select a ref to checkout';
		const choice = await window.showQuickPick<CheckoutItem>(picks, { placeHolder });
J
Joao Moreno 已提交
595 596 597 598 599 600

		if (!choice) {
			return;
		}

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

J
Joao Moreno 已提交
603
	@command('git.branch')
J
Joao Moreno 已提交
604 605
	async branch(): Promise<void> {
		const result = await window.showInputBox({
J
Joao Moreno 已提交
606
			placeHolder: localize('branch name', "Branch name"),
J
Joao Moreno 已提交
607 608
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true
J
Joao Moreno 已提交
609
		});
J
Joao Moreno 已提交
610

J
Joao Moreno 已提交
611 612 613
		if (!result) {
			return;
		}
J
Joao Moreno 已提交
614

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

J
Joao Moreno 已提交
619
	@command('git.pull')
J
Joao Moreno 已提交
620
	async pull(): Promise<void> {
J
Joao Moreno 已提交
621 622 623 624 625 626 627 628
		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 已提交
629 630
	}

J
Joao Moreno 已提交
631
	@command('git.pullRebase')
J
Joao Moreno 已提交
632
	async pullRebase(): Promise<void> {
J
Joao Moreno 已提交
633 634 635 636 637 638 639 640
		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(true);
J
Joao Moreno 已提交
641 642
	}

J
Joao Moreno 已提交
643
	@command('git.push')
J
Joao Moreno 已提交
644
	async push(): Promise<void> {
J
Joao Moreno 已提交
645 646 647 648 649 650 651 652
		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 已提交
653 654
	}

J
Joao Moreno 已提交
655
	@command('git.pushTo')
J
Joao Moreno 已提交
656
	async pushTo(): Promise<void> {
J
Joao Moreno 已提交
657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678
		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;
		}

		this.model.push(pick.label, branchName);
J
Joao Moreno 已提交
679 680
	}

J
Joao Moreno 已提交
681
	@command('git.sync')
J
Joao Moreno 已提交
682
	async sync(): Promise<void> {
J
Joao Moreno 已提交
683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704
		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 已提交
705 706 707
		await this.model.sync();
	}

J
Joao Moreno 已提交
708
	@command('git.publish')
J
Joao Moreno 已提交
709
	async publish(): Promise<void> {
J
Joao Moreno 已提交
710 711 712 713 714 715 716
		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 已提交
717 718
		const branchName = this.model.HEAD && this.model.HEAD.name || '';
		const picks = this.model.remotes.map(r => r.name);
J
Joao Moreno 已提交
719
		const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
J
Joao Moreno 已提交
720 721 722 723 724 725 726 727 728
		const choice = await window.showQuickPick(picks, { placeHolder });

		if (!choice) {
			return;
		}

		await this.model.push(choice, branchName, { setUpstream: true });
	}

J
Joao Moreno 已提交
729
	@command('git.showOutput')
J
Joao Moreno 已提交
730 731 732 733
	showOutput(): void {
		this.outputChannel.show();
	}

J
Joao Moreno 已提交
734
	private createCommand(id: string, key: string, method: Function, skipModelCheck: boolean): (...args: any[]) => any {
735
		const result = (...args) => {
J
Joao Moreno 已提交
736
			if (!skipModelCheck && !this.model) {
J
Joao Moreno 已提交
737 738 739 740
				window.showInformationMessage(localize('disabled', "Git is either disabled or not supported in this workspace"));
				return;
			}

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

J
Joao Moreno 已提交
743 744 745 746 747 748 749 750 751 752
			const result = Promise.resolve(method.apply(this, args));

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

				switch (err.gitErrorCode) {
					case 'DirtyWorkTree':
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
					default:
753 754 755
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
J
Joao Moreno 已提交
756
							.split(/[\r\n]/)
757 758 759 760 761 762
							.filter(line => !!line)
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780

						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();
				}
			});
		};
781 782 783 784 785

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

		return result;
J
Joao Moreno 已提交
786 787
	}

J
Joao Moreno 已提交
788 789 790 791
	private resolveSCMResource(uri?: Uri): Resource | undefined {
		uri = uri || window.activeTextEditor && window.activeTextEditor.document.uri;

		if (!uri) {
792
			return undefined;
J
Joao Moreno 已提交
793 794
		}

795 796
		if (uri.scheme === 'git-resource') {
			const {resourceGroupId} = JSON.parse(uri.query) as { resourceGroupId: string, sourceUri: string };
J
Joao Moreno 已提交
797
			const [resourceGroup] = this.model.resources.filter(g => g.contextKey === resourceGroupId);
798 799 800 801 802 803 804 805 806

			if (!resourceGroup) {
				return;
			}

			const uriStr = uri.toString();
			const [resource] = resourceGroup.resources.filter(r => r.uri.toString() === uriStr);

			return resource;
J
Joao Moreno 已提交
807 808 809 810 811 812 813 814 815
		}

		if (uri.scheme === 'git') {
			uri = uri.with({ scheme: 'file' });
		}

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

816 817
			return this.model.workingTreeGroup.resources.filter(r => r.sourceUri.toString() === uriString)[0]
				|| this.model.indexGroup.resources.filter(r => r.sourceUri.toString() === uriString)[0];
J
Joao Moreno 已提交
818 819 820
		}
	}

821 822 823 824 825 826
	private toSCMResources(uris: Uri[]): Resource[] {
		return uris.filter(uniqueFilter(uri => uri.toString()))
			.map(uri => this.resolveSCMResource(uri))
			.filter(r => !!r) as Resource[];
	}

J
Joao Moreno 已提交
827 828 829
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
Joao Moreno 已提交
830
}