commands.ts 14.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, SCMResourceGroup, SCMResource, window, workspace, QuickPickItem, OutputChannel } from 'vscode';
J
Joao Moreno 已提交
9
import { Ref, RefType } from './git';
J
Joao Moreno 已提交
10
import { Model, Resource, Status, CommitOptions } from './model';
J
Joao Moreno 已提交
11
import * as path from 'path';
J
Joao Moreno 已提交
12 13 14
import * as nls from 'vscode-nls';

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

J
Joao Moreno 已提交
16 17 18 19
function resolveGitURI(uri: Uri): SCMResource | SCMResourceGroup | undefined {
	if (uri.authority !== 'git') {
		return;
	}
J
Joao Moreno 已提交
20

J
Joao Moreno 已提交
21
	return scm.getResourceFromURI(uri);
J
Joao Moreno 已提交
22 23
}

J
Joao Moreno 已提交
24 25
function resolveGitResource(uri: Uri): Resource | undefined {
	const resource = resolveGitURI(uri);
J
Joao Moreno 已提交
26

J
Joao Moreno 已提交
27 28 29
	if (!(resource instanceof Resource)) {
		return;
	}
J
Joao Moreno 已提交
30

J
Joao Moreno 已提交
31
	return resource;
J
Joao Moreno 已提交
32 33
}

J
Joao Moreno 已提交
34 35 36 37 38 39 40
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 已提交
41
	constructor(protected ref: Ref) { }
J
Joao Moreno 已提交
42 43 44 45 46 47 48 49 50 51 52 53 54 55

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

		if (!ref) {
			return;
		}

		await model.checkout(ref);
	}
}

class CheckoutTagItem extends CheckoutItem {

J
Joao Moreno 已提交
56 57 58
	get description(): string {
		return localize('tag at', "Tag at {0}", this.shortCommit);
	}
J
Joao Moreno 已提交
59 60 61 62
}

class CheckoutRemoteHeadItem extends CheckoutItem {

J
Joao Moreno 已提交
63 64 65
	get description(): string {
		return localize('remote branch at', "Remote branch at {0}", this.shortCommit);
	}
J
Joao Moreno 已提交
66 67 68 69 70 71 72 73 74 75 76

	protected get treeish(): string | undefined {
		if (!this.ref.name) {
			return;
		}

		const match = /^[^/]+\/(.*)$/.exec(this.ref.name);
		return match ? match[1] : this.ref.name;
	}
}

J
Joao Moreno 已提交
77
const Commands: { commandId: string; method: Function; }[] = [];
J
Joao Moreno 已提交
78

J
Joao Moreno 已提交
79 80
function command(commandId: string): Function {
	return (target: any, key: string, descriptor: any) => {
J
Joao Moreno 已提交
81 82 83 84
		if (!(typeof descriptor.value === 'function')) {
			throw new Error('not supported');
		}

J
Joao Moreno 已提交
85 86 87
		Commands.push({ commandId, method: descriptor.value });
	};
}
J
Joao Moreno 已提交
88

J
Joao Moreno 已提交
89
export class CommandCenter {
J
Joao Moreno 已提交
90

J
Joao Moreno 已提交
91
	private model: Model;
J
Joao Moreno 已提交
92
	private disposables: Disposable[];
J
Joao Moreno 已提交
93

J
Joao Moreno 已提交
94
	constructor(
J
Joao Moreno 已提交
95
		model: Model | undefined,
J
Joao Moreno 已提交
96 97
		private outputChannel: OutputChannel
	) {
J
Joao Moreno 已提交
98 99 100 101
		if (model) {
			this.model = model;
		}

J
Joao Moreno 已提交
102 103
		this.disposables = Commands
			.map(({ commandId, method }) => commands.registerCommand(commandId, this.createCommand(method)));
J
Joao Moreno 已提交
104 105
	}

J
Joao Moreno 已提交
106
	@command('git.refresh')
J
Joao Moreno 已提交
107
	async refresh(): Promise<void> {
J
Joao Moreno 已提交
108
		await this.model.status();
J
Joao Moreno 已提交
109
	}
J
Joao Moreno 已提交
110

J
Joao Moreno 已提交
111
	@command('git.openChange')
J
Joao Moreno 已提交
112
	async openChange(uri: Uri): Promise<void> {
J
Joao Moreno 已提交
113
		const resource = resolveGitResource(uri);
J
Joao Moreno 已提交
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148

		if (!resource) {
			return;
		}

		return this.open(resource);
	}

	async open(resource: Resource): Promise<void> {
		const left = this.getLeftResource(resource);
		const right = this.getRightResource(resource);
		const title = this.getTitle(resource);

		if (!left) {
			if (!right) {
				// TODO
				console.error('oh no');
				return;
			}

			return commands.executeCommand<void>('vscode.open', right);
		}

		return commands.executeCommand<void>('vscode.diff', left, right, title);
	}

	private getLeftResource(resource: Resource): Uri | undefined {
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
				return resource.uri.with({ scheme: 'git', query: 'HEAD' });

			case Status.MODIFIED:
				const uriString = resource.uri.toString();
				const [indexStatus] = this.model.indexGroup.resources.filter(r => r.uri.toString() === uriString);
J
Joao Moreno 已提交
149 150 151 152 153 154

				if (indexStatus) {
					return resource.uri.with({ scheme: 'git' });
				}

				return resource.uri.with({ scheme: 'git', query: 'HEAD' });
J
Joao Moreno 已提交
155
		}
J
Joao Moreno 已提交
156
	}
J
Joao Moreno 已提交
157

J
Joao Moreno 已提交
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
	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:
				return resource.uri.with({ scheme: 'git' });

			case Status.INDEX_DELETED:
			case Status.DELETED:
				return resource.uri.with({ scheme: 'git', query: 'HEAD' });

			case Status.MODIFIED:
			case Status.UNTRACKED:
			case Status.IGNORED:
			case Status.BOTH_MODIFIED:
				return resource.uri;
		}
	}

	private getTitle(resource: Resource): string {
		const basename = path.basename(resource.uri.fsPath);

		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 已提交
193
	@command('git.openFile')
J
Joao Moreno 已提交
194
	async openFile(uri: Uri): Promise<void> {
J
Joao Moreno 已提交
195
		const resource = resolveGitResource(uri);
J
Joao Moreno 已提交
196 197 198 199 200 201

		if (!resource) {
			return;
		}

		return commands.executeCommand<void>('vscode.open', resource.uri);
J
Joao Moreno 已提交
202 203
	}

J
Joao Moreno 已提交
204
	@command('git.stage')
J
Joao Moreno 已提交
205 206
	async stage(uri: Uri): Promise<void> {
		const resource = resolveGitResource(uri);
J
Joao Moreno 已提交
207

J
Joao Moreno 已提交
208 209 210
		if (!resource) {
			return;
		}
J
Joao Moreno 已提交
211

J
Joao Moreno 已提交
212
		return await this.model.stage(resource);
J
Joao Moreno 已提交
213 214
	}

J
Joao Moreno 已提交
215
	@command('git.stageAll')
J
Joao Moreno 已提交
216 217 218
	async stageAll(): Promise<void> {
		return await this.model.stage();
	}
J
Joao Moreno 已提交
219

J
Joao Moreno 已提交
220
	@command('git.unstage')
J
Joao Moreno 已提交
221 222
	async unstage(uri: Uri): Promise<void> {
		const resource = resolveGitResource(uri);
J
Joao Moreno 已提交
223

J
Joao Moreno 已提交
224
		if (!resource) {
J
Joao Moreno 已提交
225 226 227
			return;
		}

J
Joao Moreno 已提交
228 229 230
		return await this.model.unstage(resource);
	}

J
Joao Moreno 已提交
231
	@command('git.unstageAll')
J
Joao Moreno 已提交
232 233 234
	async unstageAll(): Promise<void> {
		return await this.model.unstage();
	}
J
Joao Moreno 已提交
235

J
Joao Moreno 已提交
236
	@command('git.clean')
J
Joao Moreno 已提交
237 238 239 240
	async clean(uri: Uri): Promise<void> {
		const resource = resolveGitResource(uri);

		if (!resource) {
J
Joao Moreno 已提交
241 242
			return;
		}
J
Joao Moreno 已提交
243

J
Joao Moreno 已提交
244
		const basename = path.basename(resource.uri.fsPath);
J
Joao Moreno 已提交
245
		const message = localize('confirm clean', "Are you sure you want to clean changes in {0}?", basename);
J
Joao Moreno 已提交
246 247
		const yes = localize('clean', "Clean Changes");
		const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
248

J
Joao Moreno 已提交
249 250 251 252
		if (pick !== yes) {
			return;
		}

J
Joao Moreno 已提交
253
		await this.model.clean(resource);
J
Joao Moreno 已提交
254
	}
J
Joao Moreno 已提交
255

J
Joao Moreno 已提交
256
	@command('git.cleanAll')
J
Joao Moreno 已提交
257
	async cleanAll(): Promise<void> {
J
Joao Moreno 已提交
258
		const message = localize('confirm clean all', "Are you sure you want to clean all changes?");
J
Joao Moreno 已提交
259 260
		const yes = localize('clean', "Clean Changes");
		const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
261 262 263 264 265

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

J
Joao Moreno 已提交
266
		await this.model.clean(...this.model.workingTreeGroup.resources);
J
Joao Moreno 已提交
267 268
	}

J
Joao Moreno 已提交
269 270 271 272 273 274 275 276 277 278 279 280 281 282
	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 已提交
283 284 285 286
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
287
		const message = await getCommitMessage();
J
Joao Moreno 已提交
288 289 290 291 292 293

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

J
Joao Moreno 已提交
294
		await this.model.commit(message, opts);
J
Joao Moreno 已提交
295 296 297 298

		return true;
	}

J
Joao Moreno 已提交
299
	private async commitWithAnyInput(opts?: CommitOptions): Promise<void> {
300
		const message = scm.inputBox.value;
J
Joao Moreno 已提交
301
		const getCommitMessage = async () => {
J
Joao Moreno 已提交
302 303 304 305 306 307 308 309
			if (message) {
				return message;
			}

			return await window.showInputBox({
				placeHolder: localize('commit message', "Commit message"),
				prompt: localize('provide commit message', "Please provide a commit message")
			});
J
Joao Moreno 已提交
310 311 312
		};

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

		if (message && didCommit) {
315
			scm.inputBox.value = '';
J
Joao Moreno 已提交
316
		}
J
Joao Moreno 已提交
317 318
	}

J
Joao Moreno 已提交
319
	@command('git.commit')
J
Joao Moreno 已提交
320 321 322 323
	async commit(): Promise<void> {
		await this.commitWithAnyInput();
	}

J
Joao Moreno 已提交
324
	@command('git.commitWithInput')
J
Joao Moreno 已提交
325
	async commitWithInput(): Promise<void> {
J
Joao Moreno 已提交
326
		const didCommit = await this.smartCommit(async () => scm.inputBox.value);
J
Joao Moreno 已提交
327 328

		if (didCommit) {
329
			scm.inputBox.value = '';
J
Joao Moreno 已提交
330
		}
J
Joao Moreno 已提交
331 332
	}

J
Joao Moreno 已提交
333
	@command('git.commitStaged')
J
Joao Moreno 已提交
334
	async commitStaged(): Promise<void> {
J
Joao Moreno 已提交
335
		await this.commitWithAnyInput({ all: false });
J
Joao Moreno 已提交
336 337
	}

J
Joao Moreno 已提交
338
	@command('git.commitStagedSigned')
J
Joao Moreno 已提交
339
	async commitStagedSigned(): Promise<void> {
J
Joao Moreno 已提交
340
		await this.commitWithAnyInput({ all: false, signoff: true });
J
Joao Moreno 已提交
341 342
	}

J
Joao Moreno 已提交
343
	@command('git.commitAll')
J
Joao Moreno 已提交
344
	async commitAll(): Promise<void> {
J
Joao Moreno 已提交
345
		await this.commitWithAnyInput({ all: true });
J
Joao Moreno 已提交
346 347
	}

J
Joao Moreno 已提交
348
	@command('git.commitAllSigned')
J
Joao Moreno 已提交
349
	async commitAllSigned(): Promise<void> {
J
Joao Moreno 已提交
350
		await this.commitWithAnyInput({ all: true, signoff: true });
J
Joao Moreno 已提交
351 352
	}

J
Joao Moreno 已提交
353
	@command('git.undoCommit')
J
Joao Moreno 已提交
354
	async undoCommit(): Promise<void> {
J
Joao Moreno 已提交
355 356 357 358 359 360 361 362 363
		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 已提交
364 365
	}

J
Joao Moreno 已提交
366
	@command('git.checkout')
J
Joao Moreno 已提交
367 368
	async checkout(): Promise<void> {
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
369
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
370 371 372 373 374 375 376 377 378 379 380 381
		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 已提交
382 383 384
		const picks = [...heads, ...tags, ...remoteHeads];
		const placeHolder = 'Select a ref to checkout';
		const choice = await window.showQuickPick<CheckoutItem>(picks, { placeHolder });
J
Joao Moreno 已提交
385 386 387 388 389 390

		if (!choice) {
			return;
		}

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

J
Joao Moreno 已提交
393
	@command('git.branch')
J
Joao Moreno 已提交
394 395
	async branch(): Promise<void> {
		const result = await window.showInputBox({
J
Joao Moreno 已提交
396 397
			placeHolder: localize('branch name', "Branch name"),
			prompt: localize('provide branch name', "Please provide a branch name")
J
Joao Moreno 已提交
398
		});
J
Joao Moreno 已提交
399

J
Joao Moreno 已提交
400 401 402
		if (!result) {
			return;
		}
J
Joao Moreno 已提交
403

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

J
Joao Moreno 已提交
408
	@command('git.pull')
J
Joao Moreno 已提交
409
	async pull(): Promise<void> {
J
Joao Moreno 已提交
410 411 412 413 414 415 416 417
		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 已提交
418 419
	}

J
Joao Moreno 已提交
420
	@command('git.pullRebase')
J
Joao Moreno 已提交
421
	async pullRebase(): Promise<void> {
J
Joao Moreno 已提交
422 423 424 425 426 427 428 429
		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 已提交
430 431
	}

J
Joao Moreno 已提交
432
	@command('git.push')
J
Joao Moreno 已提交
433
	async push(): Promise<void> {
J
Joao Moreno 已提交
434 435 436 437 438 439 440 441
		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 已提交
442 443
	}

J
Joao Moreno 已提交
444
	@command('git.pushTo')
J
Joao Moreno 已提交
445
	async pushTo(): Promise<void> {
J
Joao Moreno 已提交
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467
		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 已提交
468 469
	}

J
Joao Moreno 已提交
470
	@command('git.sync')
J
Joao Moreno 已提交
471
	async sync(): Promise<void> {
J
Joao Moreno 已提交
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
		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 已提交
494 495 496
		await this.model.sync();
	}

J
Joao Moreno 已提交
497
	@command('git.publish')
J
Joao Moreno 已提交
498
	async publish(): Promise<void> {
J
Joao Moreno 已提交
499 500 501 502 503 504 505
		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 已提交
506 507
		const branchName = this.model.HEAD && this.model.HEAD.name || '';
		const picks = this.model.remotes.map(r => r.name);
J
Joao Moreno 已提交
508
		const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
J
Joao Moreno 已提交
509 510 511 512 513 514 515 516 517
		const choice = await window.showQuickPick(picks, { placeHolder });

		if (!choice) {
			return;
		}

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

J
Joao Moreno 已提交
518
	@command('git.showOutput')
J
Joao Moreno 已提交
519 520 521 522
	showOutput(): void {
		this.outputChannel.show();
	}

J
Joao Moreno 已提交
523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564
	private createCommand(method: Function): (...args: any[]) => any {
		return (...args) => {
			if (!this.model) {
				window.showInformationMessage(localize('disabled', "Git is either disabled or not supported in this workspace"));
				return;
			}

			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:
						const lines = (err.stderr || err.message || String(err))
							.replace(/^error: /, '')
							.split(/[\r\n]/)
							.filter(line => !!line);

						message = lines[0] || 'Git error';
						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();
				}
			});
		};
	}

J
Joao Moreno 已提交
565 566 567
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
Joao Moreno 已提交
568
}