commands.ts 46.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, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation } from 'vscode';
J
Joao Moreno 已提交
9
import { Ref, RefType, Git, GitErrorCodes, Branch } from './git';
J
Joao Moreno 已提交
10
import { Repository, Resource, Status, CommitOptions, ResourceGroupType } from './repository';
J
Joao Moreno 已提交
11
import { Model } from './model';
J
Joao Moreno 已提交
12
import { toGitUri, fromGitUri } from './uri';
J
Joao Moreno 已提交
13
import { grep } from './util';
14
import { applyLineChanges, intersectDiffWithRange, toLineRanges, invertLineChange } from './staging';
J
Joao Moreno 已提交
15
import * as path from 'path';
J
Joao Moreno 已提交
16
import * as os from 'os';
J
Joao Moreno 已提交
17
import TelemetryReporter from 'vscode-extension-telemetry';
J
Joao Moreno 已提交
18 19 20
import * as nls from 'vscode-nls';

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

J
Joao Moreno 已提交
22 23 24 25 26 27 28
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 已提交
29
	constructor(protected ref: Ref) { }
J
Joao Moreno 已提交
30

J
Joao Moreno 已提交
31
	async run(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
32 33 34 35 36 37
		const ref = this.treeish;

		if (!ref) {
			return;
		}

J
Joao Moreno 已提交
38
		await repository.checkout(ref);
J
Joao Moreno 已提交
39 40 41 42 43
	}
}

class CheckoutTagItem extends CheckoutItem {

J
Joao Moreno 已提交
44 45 46
	get description(): string {
		return localize('tag at', "Tag at {0}", this.shortCommit);
	}
J
Joao Moreno 已提交
47 48 49 50
}

class CheckoutRemoteHeadItem extends CheckoutItem {

J
Joao Moreno 已提交
51 52 53
	get description(): string {
		return localize('remote branch at', "Remote branch at {0}", this.shortCommit);
	}
J
Joao Moreno 已提交
54 55 56 57 58 59 60 61 62 63 64

	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 已提交
65 66
class BranchDeleteItem implements QuickPickItem {

67 68 69
	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 已提交
70 71
	get description(): string { return this.shortCommit; }

72
	constructor(private ref: Ref) { }
M
Maik Riechert 已提交
73

J
Joao Moreno 已提交
74
	async run(repository: Repository, force?: boolean): Promise<void> {
75
		if (!this.branchName) {
M
Maik Riechert 已提交
76 77
			return;
		}
J
Joao Moreno 已提交
78
		await repository.deleteBranch(this.branchName, force);
M
Maik Riechert 已提交
79 80 81
	}
}

82 83 84 85 86 87
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 已提交
88

J
Joao Moreno 已提交
89 90
	async run(repository: Repository): Promise<void> {
		await repository.merge(this.ref.name! || this.ref.commit!);
J
Joao Moreno 已提交
91
	}
92 93
}

J
Joao Moreno 已提交
94 95
class CreateBranchItem implements QuickPickItem {

J
Joao Moreno 已提交
96 97
	constructor(private cc: CommandCenter) { }

J
Joao Moreno 已提交
98 99 100
	get label(): string { return localize('create branch', '$(plus) Create new branch'); }
	get description(): string { return ''; }

J
Joao Moreno 已提交
101
	async run(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
102
		await this.cc.branch(repository);
J
Joao Moreno 已提交
103 104 105
	}
}

J
Joao Moreno 已提交
106
interface CommandOptions {
J
Joao Moreno 已提交
107
	repository?: boolean;
J
Joao Moreno 已提交
108 109 110
	diff?: boolean;
}

111 112 113 114
interface Command {
	commandId: string;
	key: string;
	method: Function;
J
Joao Moreno 已提交
115
	options: CommandOptions;
116 117 118
}

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

J
Joao Moreno 已提交
120
function command(commandId: string, options: CommandOptions = {}): Function {
J
Joao Moreno 已提交
121
	return (target: any, key: string, descriptor: any) => {
J
Joao Moreno 已提交
122 123 124 125
		if (!(typeof descriptor.value === 'function')) {
			throw new Error('not supported');
		}

J
Joao Moreno 已提交
126
		Commands.push({ commandId, key, method: descriptor.value, options });
J
Joao Moreno 已提交
127 128
	};
}
J
Joao Moreno 已提交
129

J
Joao Moreno 已提交
130
export class CommandCenter {
J
Joao Moreno 已提交
131 132

	private disposables: Disposable[];
J
Joao Moreno 已提交
133

J
Joao Moreno 已提交
134
	constructor(
J
Joao Moreno 已提交
135
		private git: Git,
J
Joao Moreno 已提交
136
		private model: Model,
J
Joao Moreno 已提交
137 138
		private outputChannel: OutputChannel,
		private telemetryReporter: TelemetryReporter
J
Joao Moreno 已提交
139
	) {
J
Joao Moreno 已提交
140 141
		this.disposables = Commands.map(({ commandId, key, method, options }) => {
			const command = this.createCommand(commandId, key, method, options);
142

J
Joao Moreno 已提交
143
			if (options.diff) {
J
Joao Moreno 已提交
144 145 146 147 148
				return commands.registerDiffInformationCommand(commandId, command);
			} else {
				return commands.registerCommand(commandId, command);
			}
		});
J
Joao Moreno 已提交
149 150
	}

J
Joao Moreno 已提交
151 152
	@command('git.refresh', { repository: true })
	async refresh(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
153
		await repository.status();
J
Joao Moreno 已提交
154
	}
J
Joao Moreno 已提交
155

J
Joao Moreno 已提交
156 157
	@command('git.openResource')
	async openResource(resource: Resource): Promise<void> {
J
Joao 已提交
158
		await this._openResource(resource, undefined, true, false);
J
Joao Moreno 已提交
159 160
	}

J
Joao 已提交
161
	private async _openResource(resource: Resource, preview?: boolean, preserveFocus?: boolean, preserveSelection?: boolean): Promise<void> {
J
Joao Moreno 已提交
162
		const left = this.getLeftResource(resource);
J
Joao Moreno 已提交
163
		const right = this.getRightResource(resource);
J
Joao Moreno 已提交
164 165
		const title = this.getTitle(resource);

J
Joao Moreno 已提交
166 167 168 169 170
		if (!right) {
			// TODO
			console.error('oh no');
			return;
		}
J
Joao Moreno 已提交
171

J
Joao Moreno 已提交
172
		const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
173 174
			preserveFocus,
			preview,
175
			viewColumn: ViewColumn.Active
J
Joao Moreno 已提交
176 177
		};

J
Joao Moreno 已提交
178 179
		const activeTextEditor = window.activeTextEditor;

180
		if (preserveSelection && activeTextEditor && activeTextEditor.document.uri.toString() === right.toString()) {
J
Joao Moreno 已提交
181 182 183
			opts.selection = activeTextEditor.selection;
		}

184 185 186 187 188 189
		if (!left) {
			const document = await workspace.openTextDocument(right);
			await window.showTextDocument(document, opts);
			return;
		}

J
Joao Moreno 已提交
190
		return await commands.executeCommand<void>('vscode.diff', left, right, title, opts);
J
Joao Moreno 已提交
191 192 193 194 195 196
	}

	private getLeftResource(resource: Resource): Uri | undefined {
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
J
Joao Moreno 已提交
197
				return toGitUri(resource.original, 'HEAD');
J
Joao Moreno 已提交
198 199

			case Status.MODIFIED:
J
Joao Moreno 已提交
200
				return toGitUri(resource.resourceUri, '~');
201 202 203

			case Status.DELETED_BY_THEM:
				return toGitUri(resource.resourceUri, '');
J
Joao Moreno 已提交
204
		}
J
Joao Moreno 已提交
205
	}
J
Joao Moreno 已提交
206

J
Joao Moreno 已提交
207
	private getRightResource(resource: Resource): Uri | undefined {
J
Joao Moreno 已提交
208 209 210 211 212
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_ADDED:
			case Status.INDEX_COPIED:
			case Status.INDEX_RENAMED:
J
Joao Moreno 已提交
213
				return toGitUri(resource.resourceUri, '');
J
Joao Moreno 已提交
214 215

			case Status.INDEX_DELETED:
216
			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
217
			case Status.DELETED:
J
Joao Moreno 已提交
218
				return toGitUri(resource.resourceUri, 'HEAD');
J
Joao Moreno 已提交
219 220 221 222

			case Status.MODIFIED:
			case Status.UNTRACKED:
			case Status.IGNORED:
J
Joao Moreno 已提交
223 224 225 226 227 228
				const repository = this.model.getRepository(resource.resourceUri);

				if (!repository) {
					return;
				}

J
Joao Moreno 已提交
229
				const uriString = resource.resourceUri.toString();
J
Joao Moreno 已提交
230
				const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
J
Joao Moreno 已提交
231

J
Joao Moreno 已提交
232 233
				if (indexStatus && indexStatus.renameResourceUri) {
					return indexStatus.renameResourceUri;
J
Joao Moreno 已提交
234 235
				}

J
Joao Moreno 已提交
236
				return resource.resourceUri;
J
Joao Moreno 已提交
237

238
			case Status.BOTH_ADDED:
J
Joao Moreno 已提交
239
			case Status.BOTH_MODIFIED:
J
Joao Moreno 已提交
240
				return resource.resourceUri;
J
Joao Moreno 已提交
241 242 243 244
		}
	}

	private getTitle(resource: Resource): string {
J
Joao Moreno 已提交
245
		const basename = path.basename(resource.resourceUri.fsPath);
J
Joao Moreno 已提交
246 247 248 249

		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
M
Marc Kassay 已提交
250
			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
251 252 253
				return `${basename} (Index)`;

			case Status.MODIFIED:
M
Marc Kassay 已提交
254 255
			case Status.BOTH_ADDED:
			case Status.BOTH_MODIFIED:
J
Joao Moreno 已提交
256 257 258 259 260 261
				return `${basename} (Working Tree)`;
		}

		return '';
	}

J
Joao Moreno 已提交
262
	@command('git.clone')
263 264 265 266 267 268 269
	async clone(url?: string): Promise<void> {
		if (!url) {
			url = await window.showInputBox({
				prompt: localize('repourl', "Repository URL"),
				ignoreFocusOut: true
			});
		}
J
Joao Moreno 已提交
270 271

		if (!url) {
K
kieferrm 已提交
272
			/* __GDPR__
K
kieferrm 已提交
273 274 275 276
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
277 278
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
			return;
J
Joao Moreno 已提交
279 280
		}

281
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
282
		const value = config.get<string>('defaultCloneDirectory') || os.homedir();
283

J
Joao Moreno 已提交
284 285
		const parentPath = await window.showInputBox({
			prompt: localize('parent', "Parent Directory"),
J
Joao Moreno 已提交
286
			value,
J
Joao Moreno 已提交
287
			ignoreFocusOut: true
J
Joao Moreno 已提交
288 289 290
		});

		if (!parentPath) {
K
kieferrm 已提交
291
			/* __GDPR__
K
kieferrm 已提交
292 293 294 295
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
296 297
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
			return;
J
Joao Moreno 已提交
298 299
		}

J
Joao Moreno 已提交
300
		const clonePromise = this.git.clone(url, parentPath);
M
Maryam Archie 已提交
301

J
Joao Moreno 已提交
302

303
		try {
M
Maryam Archie 已提交
304 305 306
			window.withProgress({ location: ProgressLocation.SourceControl, title: localize('cloning', "Cloning git repository...") }, () => clonePromise);
			window.withProgress({ location: ProgressLocation.Window, title: localize('cloning', "Cloning git repository...") }, () => clonePromise);

307 308 309 310 311 312
			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;
K
kieferrm 已提交
313
			/* __GDPR__
K
kieferrm 已提交
314 315 316 317 318
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
					"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
				}
			*/
319 320 321 322
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: openFolder ? 1 : 0 });
			if (openFolder) {
				commands.executeCommand('vscode.openFolder', Uri.file(repositoryPath));
			}
323 324
		} catch (err) {
			if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
K
kieferrm 已提交
325
				/* __GDPR__
K
kieferrm 已提交
326 327 328 329
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
330 331
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
			} else {
K
kieferrm 已提交
332
				/* __GDPR__
K
kieferrm 已提交
333 334 335 336
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
337
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
338 339
			}
			throw err;
J
Joao Moreno 已提交
340 341 342
		}
	}

J
Joao Moreno 已提交
343
	@command('git.init')
J
Joao Moreno 已提交
344
	async init(): Promise<void> {
J
Joao Moreno 已提交
345 346 347 348 349 350 351 352 353 354 355
		const homeUri = Uri.file(os.homedir());
		const defaultUri = workspace.workspaceFolders && workspace.workspaceFolders.length > 0
			? Uri.file(workspace.workspaceFolders[0].uri.fsPath)
			: homeUri;

		const result = await window.showOpenDialog({
			canSelectFiles: false,
			canSelectFolders: true,
			canSelectMany: false,
			defaultUri,
			openLabel: localize('init repo', "Initialize Repository")
J
Joao Moreno 已提交
356 357
		});

J
Joao Moreno 已提交
358
		if (!result || result.length === 0) {
J
Joao Moreno 已提交
359 360 361
			return;
		}

J
Joao Moreno 已提交
362 363 364 365 366 367 368 369 370 371 372 373
		const uri = result[0];

		if (homeUri.toString().startsWith(uri.toString())) {
			const yes = localize('create repo', "Initialize Repository");
			const answer = await window.showWarningMessage(localize('are you sure', "This will create a Git repository in '{0}'. Are you sure you want to continue?", uri.fsPath), yes);

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

		const path = uri.fsPath;
J
Joao Moreno 已提交
374 375
		await this.git.init(path);
		await this.model.tryOpenRepository(path);
J
Joao Moreno 已提交
376 377
	}

J
Joao Moreno 已提交
378 379 380 381 382
	@command('git.close', { repository: true })
	async close(repository: Repository): Promise<void> {
		this.model.close(repository);
	}

J
Joao Moreno 已提交
383 384
	@command('git.openFile')
	async openFile(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
385
		const preserveFocus = arg instanceof Resource;
J
Joao Moreno 已提交
386

387
		let uris: Uri[] | undefined;
388 389 390

		if (arg instanceof Uri) {
			if (arg.scheme === 'git') {
391
				uris = [Uri.file(fromGitUri(arg).path)];
392
			} else if (arg.scheme === 'file') {
393
				uris = [arg];
394 395 396 397 398 399
			}
		} else {
			let resource = arg;

			if (!(resource instanceof Resource)) {
				// can happen when called from a keybinding
400
				resource = this.getSCMResource();
401 402 403
			}

			if (resource) {
404
				uris = [...resourceStates.map(r => r.resourceUri), resource.resourceUri];
405
			}
J
Joao Moreno 已提交
406 407
		}

408
		if (!uris) {
J
Joao Moreno 已提交
409
			return;
J
Joao Moreno 已提交
410 411
		}

412 413 414 415
		const preview = uris.length === 1 ? true : false;
		const activeTextEditor = window.activeTextEditor;
		for (const uri of uris) {
			const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
416
				preserveFocus,
417
				preview: preview,
418
				viewColumn: ViewColumn.Active
419 420
			};

421
			if (activeTextEditor && activeTextEditor.document.uri.toString() === uri.toString()) {
B
Benjamin Pasero 已提交
422 423 424
				opts.selection = activeTextEditor.selection;
			}

425 426
			const document = await workspace.openTextDocument(uri);
			await window.showTextDocument(document, opts);
J
Joao Moreno 已提交
427
		}
J
Joao Moreno 已提交
428 429
	}

J
Joao Moreno 已提交
430 431
	@command('git.openHEADFile')
	async openHEADFile(arg?: Resource | Uri): Promise<void> {
D
Duroktar 已提交
432 433 434 435 436
		let resource: Resource | undefined = undefined;

		if (arg instanceof Resource) {
			resource = arg;
		} else if (arg instanceof Uri) {
437
			resource = this.getSCMResource(arg);
D
Duroktar 已提交
438
		} else {
439
			resource = this.getSCMResource();
D
Duroktar 已提交
440 441 442 443 444 445
		}

		if (!resource) {
			return;
		}

J
Joao Moreno 已提交
446
		const HEAD = this.getLeftResource(resource);
D
Duroktar 已提交
447

J
Joao Moreno 已提交
448 449 450
		if (!HEAD) {
			window.showWarningMessage(localize('HEAD not available', "HEAD version of '{0}' is not available.", path.basename(resource.resourceUri.fsPath)));
			return;
D
Duroktar 已提交
451
		}
J
Joao Moreno 已提交
452 453

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

J
Joao Moreno 已提交
456 457
	@command('git.openChange')
	async openChange(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
458
		const preserveFocus = arg instanceof Resource;
J
Joao 已提交
459
		const preserveSelection = arg instanceof Uri || !arg;
460
		let resources: Resource[] | undefined = undefined;
461

462
		if (arg instanceof Uri) {
463
			const resource = this.getSCMResource(arg);
464 465 466
			if (resource !== undefined) {
				resources = [resource];
			}
467
		} else {
468
			let resource: Resource | undefined = undefined;
J
Joao Moreno 已提交
469

470 471 472
			if (arg instanceof Resource) {
				resource = arg;
			} else {
473
				resource = this.getSCMResource();
474 475 476 477 478
			}

			if (resource) {
				resources = [...resourceStates as Resource[], resource];
			}
J
Joao Moreno 已提交
479 480
		}

481
		if (!resources) {
J
Joao Moreno 已提交
482
			return;
J
Joao Moreno 已提交
483
		}
J
Joao Moreno 已提交
484

485 486
		const preview = resources.length === 1 ? undefined : false;
		for (const resource of resources) {
J
Joao 已提交
487
			await this._openResource(resource, preview, preserveFocus, preserveSelection);
488
		}
J
Joao Moreno 已提交
489 490
	}

491 492
	@command('git.stage')
	async stage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
493
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
494
			const resource = this.getSCMResource();
495 496 497 498 499 500 501 502

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

503
		const selection = resourceStates.filter(s => s instanceof Resource) as Resource[];
J
Joao Moreno 已提交
504 505 506 507 508 509 510 511 512 513 514 515 516 517
		const merge = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge);
		const bothModified = merge.filter(s => s.type === Status.BOTH_MODIFIED);
		const promises = bothModified.map(s => grep(s.resourceUri.fsPath, /^<{7}|^={7}|^>{7}/));
		const unresolvedBothModified = await Promise.all<boolean>(promises);
		const resolvedConflicts = bothModified.filter((s, i) => !unresolvedBothModified[i]);
		const unresolvedConflicts = [
			...merge.filter(s => s.type !== Status.BOTH_MODIFIED),
			...bothModified.filter((s, i) => unresolvedBothModified[i])
		];

		if (unresolvedConflicts.length > 0) {
			const message = unresolvedConflicts.length > 1
				? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", unresolvedConflicts.length)
				: localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(unresolvedConflicts[0].resourceUri.fsPath));
518

519 520 521 522 523 524 525 526
			const yes = localize('yes', "Yes");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

J
Joao Moreno 已提交
527 528
		const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree);
		const scmResources = [...workingTree, ...resolvedConflicts, ...unresolvedConflicts];
529

530
		if (!scmResources.length) {
J
Joao Moreno 已提交
531 532
			return;
		}
J
Joao Moreno 已提交
533

534
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
535
		await this.runByRepository(resources, async (repository, resources) => repository.add(resources));
J
Joao Moreno 已提交
536 537
	}

J
Joao Moreno 已提交
538 539
	@command('git.stageAll', { repository: true })
	async stageAll(repository: Repository): Promise<void> {
540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
		const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[];
		const mergeConflicts = resources.filter(s => s.resourceGroupType === ResourceGroupType.Merge);

		if (mergeConflicts.length > 0) {
			const message = mergeConflicts.length > 1
				? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", mergeConflicts.length)
				: localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(mergeConflicts[0].resourceUri.fsPath));

			const yes = localize('yes', "Yes");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

J
Joao Moreno 已提交
556
		await repository.add([]);
J
Joao Moreno 已提交
557 558
	}

J
Joao Moreno 已提交
559 560 561 562 563
	@command('git.stageChange')
	async stageChange(change: LineChange): Promise<void> {
		await this.stageChanges([change]);
	}

J
Joao Moreno 已提交
564
	@command('git.stageSelectedRanges', { diff: true })
J
Joao Moreno 已提交
565
	async stageChanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
566 567 568 569 570 571 572 573 574
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

J
Joao Moreno 已提交
575
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
576 577 578
			return;
		}

J
Joao Moreno 已提交
579
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
580
		const originalDocument = await workspace.openTextDocument(originalUri);
581
		const selectedLines = toLineRanges(textEditor.selections, modifiedDocument);
J
Joao Moreno 已提交
582
		const selectedDiffs = changes
583 584
			.map(diff => selectedLines.reduce<LineChange | null>((result, range) => result || intersectDiffWithRange(modifiedDocument, diff, range), null))
			.filter(d => !!d) as LineChange[];
J
Joao Moreno 已提交
585 586 587 588 589

		if (!selectedDiffs.length) {
			return;
		}

590 591
		const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);

J
Joao Moreno 已提交
592
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
593
	}
J
Joao Moreno 已提交
594

J
Joao Moreno 已提交
595 596 597 598 599
	@command('git.revertChange')
	async revertChange(change: LineChange): Promise<void> {
		await this.revertChanges([change]);
	}

J
Joao Moreno 已提交
600
	@command('git.revertSelectedRanges', { diff: true })
J
Joao Moreno 已提交
601
	async revertChanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
602 603 604 605 606 607 608 609 610 611 612 613 614
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

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

J
Joao Moreno 已提交
615
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638
		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;
		}

639
		const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);
J
Joao Moreno 已提交
640 641 642 643 644
		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 已提交
645 646
	@command('git.unstage')
	async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
647
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
648
			const resource = this.getSCMResource();
649 650 651 652 653 654 655 656

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

657
		const scmResources = resourceStates
J
Joao Moreno 已提交
658
			.filter(s => s instanceof Resource && s.resourceGroupType === ResourceGroupType.Index) as Resource[];
659

660
		if (!scmResources.length) {
J
Joao Moreno 已提交
661 662 663
			return;
		}

664
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
665
		await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
J
Joao Moreno 已提交
666 667
	}

J
Joao Moreno 已提交
668 669
	@command('git.unstageAll', { repository: true })
	async unstageAll(repository: Repository): Promise<void> {
670
		await repository.revert([]);
J
Joao Moreno 已提交
671
	}
J
Joao Moreno 已提交
672

J
Joao Moreno 已提交
673 674
	@command('git.unstageSelectedRanges', { diff: true })
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
675 676 677 678 679 680 681 682 683
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

684 685 686 687 688 689 690
		if (modifiedUri.scheme !== 'git') {
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
J
Joao Moreno 已提交
691 692 693
			return;
		}

J
Joao Moreno 已提交
694
		const originalUri = toGitUri(modifiedUri, 'HEAD');
J
Joao Moreno 已提交
695
		const originalDocument = await workspace.openTextDocument(originalUri);
696 697 698 699
		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 已提交
700 701 702 703 704

		if (!selectedDiffs.length) {
			return;
		}

705 706
		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
J
Joao Moreno 已提交
707

J
Joao Moreno 已提交
708
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
709 710
	}

J
Joao Moreno 已提交
711 712
	@command('git.clean')
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
713
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
714
			const resource = this.getSCMResource();
715 716 717 718 719 720 721 722

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

J
Joao Moreno 已提交
723
		const scmResources = resourceStates
J
Joao Moreno 已提交
724
			.filter(s => s instanceof Resource && s.resourceGroupType === ResourceGroupType.WorkingTree) as Resource[];
725

J
Joao Moreno 已提交
726
		if (!scmResources.length) {
J
Joao Moreno 已提交
727 728
			return;
		}
J
Joao Moreno 已提交
729

J
Joao Moreno 已提交
730
		const untrackedCount = scmResources.reduce((s, r) => s + (r.type === Status.UNTRACKED ? 1 : 0), 0);
J
Joao Moreno 已提交
731 732 733
		let message: string;
		let yes = localize('discard', "Discard Changes");

J
Joao Moreno 已提交
734
		if (scmResources.length === 1) {
J
Joao Moreno 已提交
735
			if (untrackedCount > 0) {
J
Joao Moreno 已提交
736
				message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
737 738
				yes = localize('delete file', "Delete file");
			} else {
J
Joao Moreno 已提交
739
				message = localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
740 741
			}
		} else {
J
Joao Moreno 已提交
742
			message = localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", scmResources.length);
J
Joao Moreno 已提交
743 744 745 746 747

			if (untrackedCount > 0) {
				message = `${message}\n\n${localize('warn untracked', "This will DELETE {0} untracked files!", untrackedCount)}`;
			}
		}
748

J
Joao Moreno 已提交
749
		const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
750

J
Joao Moreno 已提交
751 752 753 754
		if (pick !== yes) {
			return;
		}

J
Joao Moreno 已提交
755
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
756
		await this.runByRepository(resources, async (repository, resources) => repository.clean(resources));
J
Joao Moreno 已提交
757
	}
J
Joao Moreno 已提交
758

J
Joao Moreno 已提交
759 760
	@command('git.cleanAll', { repository: true })
	async cleanAll(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
761
		let resources = repository.workingTreeGroup.resourceStates;
J
Joao Moreno 已提交
762 763 764 765 766

		if (resources.length === 0) {
			return;
		}

J
Joao Moreno 已提交
767 768
		const trackedResources = resources.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
		const untrackedResources = resources.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);
J
Joao Moreno 已提交
769

J
Joao Moreno 已提交
770 771 772 773 774 775
		if (untrackedResources.length === 0) {
			const message = resources.length === 1
				? localize('confirm discard all single', "Are you sure you want to discard changes in {0}?", path.basename(resources[0].resourceUri.fsPath))
				: localize('confirm discard all', "Are you sure you want to discard ALL changes in {0} files?\nThis is IRREVERSIBLE!\nYour current working set will be FOREVER LOST.", resources.length);
			const yes = resources.length === 1
				? localize('discardAll multiple', "Discard 1 File")
J
Joao Moreno 已提交
776
				: localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
777
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
778

J
Joao Moreno 已提交
779
			if (pick !== yes) {
J
Joao Moreno 已提交
780 781 782
				return;
			}

J
Joao 已提交
783
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
784 785 786 787 788 789 790 791
			return;
		} else if (resources.length === 1) {
			const message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(resources[0].resourceUri.fsPath));
			const yes = localize('delete file', "Delete file");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

			if (pick !== yes) {
				return;
J
Joao Moreno 已提交
792 793
			}

J
Joao 已提交
794
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
795 796 797 798
		} else if (trackedResources.length === 0) {
			const message = localize('confirm delete multiple', "Are you sure you want to DELETE {0} files?", resources.length);
			const yes = localize('delete files', "Delete Files");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
799

J
Joao Moreno 已提交
800 801 802
			if (pick !== yes) {
				return;
			}
J
Joao Moreno 已提交
803

J
Joao 已提交
804
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
805

J
Joao Moreno 已提交
806 807 808 809
		} else { // resources.length > 1 && untrackedResources.length > 0 && trackedResources.length > 0
			const untrackedMessage = untrackedResources.length === 1
				? localize('there are untracked files single', "The following untracked file will be DELETED FROM DISK if discarded: {0}.", path.basename(untrackedResources[0].resourceUri.fsPath))
				: localize('there are untracked files', "There are {0} untracked files which will be DELETED FROM DISK if discarded.", untrackedResources.length);
J
Joao Moreno 已提交
810

J
Joao Moreno 已提交
811 812 813 814 815
			const message = localize('confirm discard all 2', "{0}\n\nThis is IRREVERSIBLE, your current working set will be FOREVER LOST.", untrackedMessage, resources.length);

			const yesTracked = trackedResources.length === 1
				? localize('yes discard tracked', "Discard 1 Tracked File", trackedResources.length)
				: localize('yes discard tracked multiple', "Discard {0} Tracked Files", trackedResources.length);
J
Joao Moreno 已提交
816

J
Joao Moreno 已提交
817
			const yesAll = localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
818 819 820 821 822 823 824 825
			const pick = await window.showWarningMessage(message, { modal: true }, yesTracked, yesAll);

			if (pick === yesTracked) {
				resources = trackedResources;
			} else if (pick !== yesAll) {
				return;
			}

J
Joao 已提交
826
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
827
		}
J
Joao Moreno 已提交
828 829
	}

J
Joao Moreno 已提交
830
	private async smartCommit(
J
Joao Moreno 已提交
831
		repository: Repository,
832
		getCommitMessage: () => Promise<string | undefined>,
J
Joao Moreno 已提交
833 834
		opts?: CommitOptions
	): Promise<boolean> {
835 836
		const config = workspace.getConfiguration('git');
		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
837
		const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
J
Joao Moreno 已提交
838 839
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
840 841

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

J
Joao Moreno 已提交
844 845
			// 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?");
846 847 848 849 850
			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 已提交
851 852 853
				config.update('enableSmartCommit', true, true);
			} else if (pick !== yes) {
				return false; // do not commit on cancel
854 855 856
			}
		}

J
Joao Moreno 已提交
857
		if (!opts) {
858
			opts = { all: noStagedChanges };
J
Joao Moreno 已提交
859 860
		}

861 862 863
		// enable signing of commits if configurated
		opts.signCommit = enableCommitSigning;

J
Joao Moreno 已提交
864 865
		if (
			// no changes
866
			(noStagedChanges && noUnstagedChanges)
J
Joao Moreno 已提交
867
			// or no staged changes and not `all`
868
			|| (!opts.all && noStagedChanges)
J
Joao Moreno 已提交
869
		) {
J
Joao Moreno 已提交
870 871 872 873
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
874
		const message = await getCommitMessage();
J
Joao Moreno 已提交
875 876 877 878 879

		if (!message) {
			return false;
		}

J
Joao Moreno 已提交
880
		await repository.commit(message, opts);
J
Joao Moreno 已提交
881 882 883 884

		return true;
	}

J
Joao Moreno 已提交
885
	private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
J
Joao Moreno 已提交
886
		const message = repository.inputBox.value;
J
Joao Moreno 已提交
887
		const getCommitMessage = async () => {
J
Joao Moreno 已提交
888 889 890 891 892 893
			if (message) {
				return message;
			}

			return await window.showInputBox({
				placeHolder: localize('commit message', "Commit message"),
J
Joao Moreno 已提交
894 895
				prompt: localize('provide commit message', "Please provide a commit message"),
				ignoreFocusOut: true
J
Joao Moreno 已提交
896
			});
J
Joao Moreno 已提交
897 898
		};

J
Joao Moreno 已提交
899
		const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
J
Joao Moreno 已提交
900 901

		if (message && didCommit) {
J
Joao Moreno 已提交
902
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
903
		}
J
Joao Moreno 已提交
904 905
	}

J
Joao Moreno 已提交
906
	@command('git.commit', { repository: true })
J
Joao Moreno 已提交
907 908
	async commit(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository);
J
Joao Moreno 已提交
909 910
	}

J
Joao Moreno 已提交
911
	@command('git.commitWithInput', { repository: true })
J
Joao Moreno 已提交
912
	async commitWithInput(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
913
		if (!repository.inputBox.value) {
J
Joao Moreno 已提交
914 915 916
			return;
		}

J
Joao Moreno 已提交
917
		const didCommit = await this.smartCommit(repository, async () => repository.inputBox.value);
J
Joao Moreno 已提交
918 919

		if (didCommit) {
J
Joao Moreno 已提交
920
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
921
		}
J
Joao Moreno 已提交
922 923
	}

J
Joao Moreno 已提交
924
	@command('git.commitStaged', { repository: true })
J
Joao Moreno 已提交
925 926
	async commitStaged(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false });
J
Joao Moreno 已提交
927 928
	}

J
Joao Moreno 已提交
929
	@command('git.commitStagedSigned', { repository: true })
J
Joao Moreno 已提交
930 931
	async commitStagedSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, signoff: true });
J
Joao Moreno 已提交
932 933
	}

J
Joao Moreno 已提交
934
	@command('git.commitStagedAmend', { repository: true })
J
Joao Moreno 已提交
935 936
	async commitStagedAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, amend: true });
K
Krzysztof Cieślak 已提交
937 938
	}

J
Joao Moreno 已提交
939
	@command('git.commitAll', { repository: true })
J
Joao Moreno 已提交
940 941
	async commitAll(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true });
J
Joao Moreno 已提交
942 943
	}

J
Joao Moreno 已提交
944
	@command('git.commitAllSigned', { repository: true })
J
Joao Moreno 已提交
945 946
	async commitAllSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, signoff: true });
J
Joao Moreno 已提交
947 948
	}

J
Joao Moreno 已提交
949
	@command('git.commitAllAmend', { repository: true })
J
Joao Moreno 已提交
950 951
	async commitAllAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, amend: true });
K
Krzysztof Cieślak 已提交
952 953
	}

J
Joao Moreno 已提交
954
	@command('git.undoCommit', { repository: true })
J
Joao Moreno 已提交
955 956
	async undoCommit(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
957 958 959 960 961

		if (!HEAD || !HEAD.commit) {
			return;
		}

J
Joao Moreno 已提交
962 963
		const commit = await repository.getCommit('HEAD');
		await repository.reset('HEAD~');
J
Joao Moreno 已提交
964
		repository.inputBox.value = commit.message;
J
Joao Moreno 已提交
965 966
	}

J
Joao Moreno 已提交
967
	@command('git.checkout', { repository: true })
J
Joao Moreno 已提交
968
	async checkout(repository: Repository, treeish: string): Promise<void> {
J
Joao Moreno 已提交
969
		if (typeof treeish === 'string') {
J
Joao Moreno 已提交
970
			return await repository.checkout(treeish);
J
Joao Moreno 已提交
971 972
		}

J
Joao Moreno 已提交
973
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
974
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
975 976 977
		const includeTags = checkoutType === 'all' || checkoutType === 'tags';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
978
		const createBranch = new CreateBranchItem(this);
J
Joao Moreno 已提交
979

J
Joao Moreno 已提交
980
		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
981 982
			.map(ref => new CheckoutItem(ref));

J
Joao Moreno 已提交
983
		const tags = (includeTags ? repository.refs.filter(ref => ref.type === RefType.Tag) : [])
J
Joao Moreno 已提交
984 985
			.map(ref => new CheckoutTagItem(ref));

J
Joao Moreno 已提交
986
		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
987 988
			.map(ref => new CheckoutRemoteHeadItem(ref));

J
Joao Moreno 已提交
989
		const picks = [createBranch, ...heads, ...tags, ...remoteHeads];
990
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
J
Joao Moreno 已提交
991
		const choice = await window.showQuickPick(picks, { placeHolder });
J
Joao Moreno 已提交
992 993 994 995 996

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
997
		await choice.run(repository);
J
Joao Moreno 已提交
998 999
	}

J
Joao Moreno 已提交
1000
	@command('git.branch', { repository: true })
J
Joao Moreno 已提交
1001
	async branch(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1002
		const result = await window.showInputBox({
J
Joao Moreno 已提交
1003
			placeHolder: localize('branch name', "Branch name"),
J
Joao Moreno 已提交
1004 1005
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true
J
Joao Moreno 已提交
1006
		});
J
Joao Moreno 已提交
1007

J
Joao Moreno 已提交
1008 1009 1010
		if (!result) {
			return;
		}
J
Joao Moreno 已提交
1011

J
Joao Moreno 已提交
1012
		const name = result.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-');
J
Joao Moreno 已提交
1013
		await repository.branch(name);
J
Joao Moreno 已提交
1014 1015
	}

J
Joao Moreno 已提交
1016
	@command('git.deleteBranch', { repository: true })
J
Joao Moreno 已提交
1017
	async deleteBranch(repository: Repository, name: string, force?: boolean): Promise<void> {
1018 1019
		let run: (force?: boolean) => Promise<void>;
		if (typeof name === 'string') {
J
Joao Moreno 已提交
1020
			run = force => repository.deleteBranch(name, force);
1021
		} else {
J
Joao Moreno 已提交
1022 1023
			const currentHead = repository.HEAD && repository.HEAD.name;
			const heads = repository.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead)
1024
				.map(ref => new BranchDeleteItem(ref));
M
Maik Riechert 已提交
1025

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

M
Maik Riechert 已提交
1029
			if (!choice || !choice.branchName) {
1030 1031
				return;
			}
M
Maik Riechert 已提交
1032
			name = choice.branchName;
J
Joao Moreno 已提交
1033
			run = force => choice.run(repository, force);
M
Maik Riechert 已提交
1034 1035
		}

1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050
		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 已提交
1051 1052
	}

J
Joao Moreno 已提交
1053
	@command('git.merge', { repository: true })
J
Joao Moreno 已提交
1054
	async merge(repository: Repository): Promise<void> {
1055 1056 1057 1058
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string>('checkoutType') || 'all';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1059
		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
1060 1061
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1062

J
Joao Moreno 已提交
1063
		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
1064 1065
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1066 1067

		const picks = [...heads, ...remoteHeads];
1068 1069
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
1070 1071 1072 1073 1074

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1075
		try {
J
Joao Moreno 已提交
1076
			await choice.run(repository);
J
Joao Moreno 已提交
1077 1078 1079 1080 1081 1082 1083 1084
		} 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);
		}
1085 1086
	}

J
Joao Moreno 已提交
1087
	@command('git.createTag', { repository: true })
J
Joao Moreno 已提交
1088
	async createTag(repository: Repository): Promise<void> {
1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100
		const inputTagName = await window.showInputBox({
			placeHolder: localize('tag name', "Tag name"),
			prompt: localize('provide tag name', "Please provide a tag name"),
			ignoreFocusOut: true
		});

		if (!inputTagName) {
			return;
		}

		const inputMessage = await window.showInputBox({
			placeHolder: localize('tag message', "Message"),
J
Joao Moreno 已提交
1101
			prompt: localize('provide tag message', "Please provide a message to annotate the tag"),
1102 1103 1104 1105 1106
			ignoreFocusOut: true
		});

		const name = inputTagName.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-');
		const message = inputMessage || name;
J
Joao Moreno 已提交
1107
		await repository.tag(name, message);
1108 1109
	}

J
Joao Moreno 已提交
1110
	@command('git.pullFrom', { repository: true })
J
Joao Moreno 已提交
1111 1112
	async pullFrom(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136

		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;
		}

J
Joao Moreno 已提交
1137
		repository.pull(false, pick.label, branchName);
1138 1139
	}

J
Joao Moreno 已提交
1140
	@command('git.pull', { repository: true })
J
Joao Moreno 已提交
1141 1142
	async pull(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1143 1144 1145 1146 1147 1148

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from."));
			return;
		}

J
Joao Moreno 已提交
1149
		await repository.pull();
J
Joao Moreno 已提交
1150 1151
	}

J
Joao Moreno 已提交
1152
	@command('git.pullRebase', { repository: true })
J
Joao Moreno 已提交
1153 1154
	async pullRebase(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1155 1156 1157 1158 1159 1160

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from."));
			return;
		}

J
Joao Moreno 已提交
1161
		await repository.pullWithRebase();
J
Joao Moreno 已提交
1162 1163
	}

J
Joao Moreno 已提交
1164
	@command('git.push', { repository: true })
J
Joao Moreno 已提交
1165 1166
	async push(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1167 1168 1169 1170 1171 1172

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
			return;
		}

J
Joao Moreno 已提交
1173
		await repository.push();
J
Joao Moreno 已提交
1174 1175
	}

J
Joao Moreno 已提交
1176
	@command('git.pushWithTags', { repository: true })
J
Joao Moreno 已提交
1177 1178
	async pushWithTags(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1179 1180 1181 1182 1183 1184

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
			return;
		}

J
Joao Moreno 已提交
1185
		await repository.pushTags();
1186 1187 1188 1189

		window.showInformationMessage(localize('push with tags success', "Successfully pushed with tags."));
	}

J
Joao Moreno 已提交
1190
	@command('git.pushTo', { repository: true })
J
Joao Moreno 已提交
1191 1192
	async pushTo(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1193 1194 1195 1196 1197 1198

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
			return;
		}

J
Joao Moreno 已提交
1199
		if (!repository.HEAD || !repository.HEAD.name) {
J
Joao Moreno 已提交
1200 1201 1202 1203
			window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			return;
		}

J
Joao Moreno 已提交
1204
		const branchName = repository.HEAD.name;
J
Joao Moreno 已提交
1205 1206 1207 1208 1209 1210 1211 1212
		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 已提交
1213
		repository.pushTo(pick.label, branchName);
J
Joao Moreno 已提交
1214 1215
	}

J
Joao Moreno 已提交
1216
	@command('git.sync', { repository: true })
J
Joao Moreno 已提交
1217 1218
	async sync(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239

		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 已提交
1240
		await repository.sync();
J
Joao Moreno 已提交
1241 1242
	}

J
Joao 已提交
1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255
	@command('git._syncAll')
	async syncAll(): Promise<void> {
		await Promise.all(this.model.repositories.map(async repository => {
			const HEAD = repository.HEAD;

			if (!HEAD || !HEAD.upstream) {
				return;
			}

			await repository.sync();
		}));
	}

J
Joao Moreno 已提交
1256
	@command('git.publish', { repository: true })
J
Joao Moreno 已提交
1257 1258
	async publish(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1259 1260 1261 1262 1263 1264

		if (remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to publish', "Your repository has no remotes configured to publish to."));
			return;
		}

J
Joao Moreno 已提交
1265
		const branchName = repository.HEAD && repository.HEAD.name || '';
M
Markus Wolf 已提交
1266 1267 1268 1269 1270 1271
		const selectRemote = async () => {
			const picks = repository.remotes.map(r => r.name);
			const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
			return await window.showQuickPick(picks, { placeHolder });
		};
		const choice = remotes.length === 1 ? remotes[0].name : await selectRemote();
J
Joao Moreno 已提交
1272 1273 1274 1275 1276

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1277
		await repository.pushTo(choice, branchName, true);
J
Joao Moreno 已提交
1278 1279
	}

J
Joao Moreno 已提交
1280
	@command('git.showOutput')
J
Joao Moreno 已提交
1281 1282 1283 1284
	showOutput(): void {
		this.outputChannel.show();
	}

1285 1286
	@command('git.ignore')
	async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
1287
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
1288
			const resource = this.getSCMResource();
N
NKumar2 已提交
1289

1290
			if (!resource) {
J
Joao Moreno 已提交
1291 1292 1293
				return;
			}

1294
			resourceStates = [resource];
J
Joao Moreno 已提交
1295 1296
		}

1297
		const resources = resourceStates
J
Joao Moreno 已提交
1298 1299 1300
			.filter(s => s instanceof Resource)
			.map(r => r.resourceUri);

1301
		if (!resources.length) {
N
NKumar2 已提交
1302 1303 1304
			return;
		}

1305
		await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
N
NKumar2 已提交
1306 1307
	}

J
Joao Moreno 已提交
1308
	@command('git.stash', { repository: true })
J
Joao Moreno 已提交
1309 1310
	async stash(repository: Repository): Promise<void> {
		if (repository.workingTreeGroup.resourceStates.length === 0) {
K
Krzysztof Cieślak 已提交
1311 1312 1313
			window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
			return;
		}
J
Joao Moreno 已提交
1314 1315 1316 1317 1318 1319 1320 1321 1322 1323

		const message = await window.showInputBox({
			prompt: localize('provide stash message', "Optionally provide a stash message"),
			placeHolder: localize('stash message', "Stash message")
		});

		if (typeof message === 'undefined') {
			return;
		}

J
Joao Moreno 已提交
1324
		await repository.createStash(message);
K
Krzysztof Cieślak 已提交
1325 1326
	}

J
Joao Moreno 已提交
1327
	@command('git.stashPop', { repository: true })
J
Joao Moreno 已提交
1328 1329
	async stashPop(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
1330 1331

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1332 1333 1334 1335
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

J
Joao Moreno 已提交
1336 1337
		const picks = stashes.map(r => ({ label: `#${r.index}:  ${r.description}`, description: '', details: '', id: r.index }));
		const placeHolder = localize('pick stash to pop', "Pick a stash to pop");
K
Krzysztof Cieślak 已提交
1338 1339 1340 1341 1342
		const choice = await window.showQuickPick(picks, { placeHolder });

		if (!choice) {
			return;
		}
J
Joao Moreno 已提交
1343

J
Joao Moreno 已提交
1344
		await repository.popStash(choice.id);
K
Krzysztof Cieślak 已提交
1345 1346
	}

J
Joao Moreno 已提交
1347
	@command('git.stashPopLatest', { repository: true })
J
Joao Moreno 已提交
1348 1349
	async stashPopLatest(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
1350 1351

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1352 1353 1354 1355
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

J
Joao Moreno 已提交
1356
		await repository.popStash();
J
Joao Moreno 已提交
1357
	}
K
Krzysztof Cieślak 已提交
1358

J
Joao Moreno 已提交
1359
	private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any {
1360
		const result = (...args) => {
J
Joao Moreno 已提交
1361 1362
			let result: Promise<any>;

J
Joao Moreno 已提交
1363
			if (!options.repository) {
J
Joao Moreno 已提交
1364 1365
				result = Promise.resolve(method.apply(this, args));
			} else {
J
Joao Moreno 已提交
1366 1367
				// try to guess the repository based on the first argument
				const repository = this.model.getRepository(args[0]);
J
Joao Moreno 已提交
1368 1369 1370 1371 1372 1373 1374 1375 1376
				let repositoryPromise: Promise<Repository | undefined>;

				if (repository) {
					repositoryPromise = Promise.resolve(repository);
				} else if (this.model.repositories.length === 1) {
					repositoryPromise = Promise.resolve(this.model.repositories[0]);
				} else {
					repositoryPromise = this.model.pickRepository();
				}
1377

J
Joao Moreno 已提交
1378
				result = repositoryPromise.then(repository => {
J
Joao Moreno 已提交
1379
					if (!repository) {
J
Joao 已提交
1380
						return Promise.resolve();
J
Joao Moreno 已提交
1381 1382
					}

J
Joao Moreno 已提交
1383
					return Promise.resolve(method.apply(this, [repository, ...args]));
J
Joao Moreno 已提交
1384
				});
J
Joao Moreno 已提交
1385 1386
			}

K
kieferrm 已提交
1387
			/* __GDPR__
K
kieferrm 已提交
1388 1389 1390 1391
				"git.command" : {
					"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
J
Joao Moreno 已提交
1392 1393
			this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

J
Joao Moreno 已提交
1394 1395 1396 1397
			return result.catch(async err => {
				let message: string;

				switch (err.gitErrorCode) {
1398
					case GitErrorCodes.DirtyWorkTree:
J
Joao Moreno 已提交
1399 1400
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
1401 1402 1403
					case GitErrorCodes.PushRejected:
						message = localize('cant push', "Can't push refs to remote. Run 'Pull' first to integrate your changes.");
						break;
J
Joao Moreno 已提交
1404
					default:
1405 1406 1407
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
J
Joao Moreno 已提交
1408
							.split(/[\r\n]/)
1409 1410 1411 1412 1413 1414
							.filter(line => !!line)
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432

						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();
				}
			});
		};
1433 1434 1435 1436 1437

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

		return result;
J
Joao Moreno 已提交
1438 1439
	}

1440
	private getSCMResource(uri?: Uri): Resource | undefined {
1441
		uri = uri ? uri : window.activeTextEditor && window.activeTextEditor.document.uri;
J
Joao Moreno 已提交
1442 1443

		if (!uri) {
1444
			return undefined;
J
Joao Moreno 已提交
1445 1446 1447
		}

		if (uri.scheme === 'git') {
J
Joao Moreno 已提交
1448 1449
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
1450 1451 1452 1453
		}

		if (uri.scheme === 'file') {
			const uriString = uri.toString();
J
Joao Moreno 已提交
1454
			const repository = this.model.getRepository(uri);
1455

J
Joao Moreno 已提交
1456
			if (!repository) {
1457 1458
				return undefined;
			}
J
Joao Moreno 已提交
1459

J
Joao Moreno 已提交
1460 1461
			return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
				|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
J
Joao Moreno 已提交
1462 1463 1464
		}
	}

J
Joao Moreno 已提交
1465
	private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;
J
Joao Moreno 已提交
1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495
	private runByRepository<T>(resources: Uri[], fn: (repository: Repository, resources: Uri[]) => Promise<T>): Promise<T[]>;
	private async runByRepository<T>(arg: Uri | Uri[], fn: (repository: Repository, resources: any) => Promise<T>): Promise<T[]> {
		const resources = arg instanceof Uri ? [arg] : arg;
		const isSingleResource = arg instanceof Uri;

		const groups = resources.reduce((result, resource) => {
			const repository = this.model.getRepository(resource);

			if (!repository) {
				console.warn('Could not find git repository for ', resource);
				return result;
			}

			const tuple = result.filter(p => p[0] === repository)[0];

			if (tuple) {
				tuple.resources.push(resource);
			} else {
				result.push({ repository, resources: [resource] });
			}

			return result;
		}, [] as { repository: Repository, resources: Uri[] }[]);

		const promises = groups
			.map(({ repository, resources }) => fn(repository as Repository, isSingleResource ? resources[0] : resources));

		return Promise.all(promises);
	}

J
Joao Moreno 已提交
1496 1497 1498
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
Joao Moreno 已提交
1499
}