commands.ts 45.4 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';
13
import { applyLineChanges, intersectDiffWithRange, toLineRanges, invertLineChange } from './staging';
J
Joao Moreno 已提交
14
import * as path from 'path';
J
Joao Moreno 已提交
15
import * as os from 'os';
J
Joao Moreno 已提交
16
import TelemetryReporter from 'vscode-extension-telemetry';
J
Joao Moreno 已提交
17 18 19
import * as nls from 'vscode-nls';

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

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

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

		if (!ref) {
			return;
		}

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

class CheckoutTagItem extends CheckoutItem {

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

class CheckoutRemoteHeadItem extends CheckoutItem {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

				if (!repository) {
					return;
				}

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

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

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

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

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

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

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

		return '';
	}

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

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

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

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

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

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

J
Joao Moreno 已提交
301

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

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

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

		const path = await window.showInputBox({
			placeHolder: localize('path to init', "Folder path"),
			prompt: localize('provide path', "Please provide a folder path to initialize a Git repository"),
			value,
			ignoreFocusOut: true
		});

		if (!path) {
			return;
		}

		await this.git.init(path);
		await this.model.tryOpenRepository(path);
J
Joao Moreno 已提交
361 362
	}

J
Joao Moreno 已提交
363 364 365 366 367
	@command('git.close', { repository: true })
	async close(repository: Repository): Promise<void> {
		this.model.close(repository);
	}

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

372
		let uris: Uri[] | undefined;
373 374 375

		if (arg instanceof Uri) {
			if (arg.scheme === 'git') {
376
				uris = [Uri.file(fromGitUri(arg).path)];
377
			} else if (arg.scheme === 'file') {
378
				uris = [arg];
379 380 381 382 383 384
			}
		} else {
			let resource = arg;

			if (!(resource instanceof Resource)) {
				// can happen when called from a keybinding
385
				resource = this.getSCMResource();
386 387 388
			}

			if (resource) {
389
				uris = [...resourceStates.map(r => r.resourceUri), resource.resourceUri];
390
			}
J
Joao Moreno 已提交
391 392
		}

393
		if (!uris) {
J
Joao Moreno 已提交
394
			return;
J
Joao Moreno 已提交
395 396
		}

397 398 399 400
		const preview = uris.length === 1 ? true : false;
		const activeTextEditor = window.activeTextEditor;
		for (const uri of uris) {
			const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
401
				preserveFocus,
402
				preview: preview,
403
				viewColumn: ViewColumn.Active
404 405
			};

406
			if (activeTextEditor && activeTextEditor.document.uri.toString() === uri.toString()) {
B
Benjamin Pasero 已提交
407 408 409
				opts.selection = activeTextEditor.selection;
			}

410 411
			const document = await workspace.openTextDocument(uri);
			await window.showTextDocument(document, opts);
J
Joao Moreno 已提交
412
		}
J
Joao Moreno 已提交
413 414
	}

J
Joao Moreno 已提交
415 416
	@command('git.openHEADFile')
	async openHEADFile(arg?: Resource | Uri): Promise<void> {
D
Duroktar 已提交
417 418 419 420 421
		let resource: Resource | undefined = undefined;

		if (arg instanceof Resource) {
			resource = arg;
		} else if (arg instanceof Uri) {
422
			resource = this.getSCMResource(arg);
D
Duroktar 已提交
423
		} else {
424
			resource = this.getSCMResource();
D
Duroktar 已提交
425 426 427 428 429 430
		}

		if (!resource) {
			return;
		}

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

J
Joao Moreno 已提交
433 434 435
		if (!HEAD) {
			window.showWarningMessage(localize('HEAD not available', "HEAD version of '{0}' is not available.", path.basename(resource.resourceUri.fsPath)));
			return;
D
Duroktar 已提交
436
		}
J
Joao Moreno 已提交
437 438

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

J
Joao Moreno 已提交
441 442
	@command('git.openChange')
	async openChange(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
443
		const preserveFocus = arg instanceof Resource;
J
Joao 已提交
444
		const preserveSelection = arg instanceof Uri || !arg;
445
		let resources: Resource[] | undefined = undefined;
446

447
		if (arg instanceof Uri) {
448
			const resource = this.getSCMResource(arg);
449 450 451
			if (resource !== undefined) {
				resources = [resource];
			}
452
		} else {
453
			let resource: Resource | undefined = undefined;
J
Joao Moreno 已提交
454

455 456 457
			if (arg instanceof Resource) {
				resource = arg;
			} else {
458
				resource = this.getSCMResource();
459 460 461 462 463
			}

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

466
		if (!resources) {
J
Joao Moreno 已提交
467
			return;
J
Joao Moreno 已提交
468
		}
J
Joao Moreno 已提交
469

470 471
		const preview = resources.length === 1 ? undefined : false;
		for (const resource of resources) {
J
Joao 已提交
472
			await this._openResource(resource, preview, preserveFocus, preserveSelection);
473
		}
J
Joao Moreno 已提交
474 475
	}

476 477
	@command('git.stage')
	async stage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
478
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
479
			const resource = this.getSCMResource();
480 481 482 483 484 485 486 487

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

488 489 490
		const selection = resourceStates.filter(s => s instanceof Resource) as Resource[];
		const mergeConflicts = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge);

491 492
		if (mergeConflicts.length > 0) {
			const message = mergeConflicts.length > 1
493 494 495
				? 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));

496 497 498 499 500 501 502 503
			const yes = localize('yes', "Yes");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

504 505 506 507
		const workingTree = selection
			.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree);

		const scmResources = [...workingTree, ...mergeConflicts];
508

509
		if (!scmResources.length) {
J
Joao Moreno 已提交
510 511
			return;
		}
J
Joao Moreno 已提交
512

513
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
514
		await this.runByRepository(resources, async (repository, resources) => repository.add(resources));
J
Joao Moreno 已提交
515 516
	}

J
Joao Moreno 已提交
517 518
	@command('git.stageAll', { repository: true })
	async stageAll(repository: Repository): Promise<void> {
519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
		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 已提交
535
		await repository.add([]);
J
Joao Moreno 已提交
536 537
	}

J
Joao Moreno 已提交
538 539
	@command('git.stageSelectedRanges', { diff: true })
	async stageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
540 541 542 543 544 545 546 547 548
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

J
Joao Moreno 已提交
549
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
550 551 552
			return;
		}

J
Joao Moreno 已提交
553
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
554
		const originalDocument = await workspace.openTextDocument(originalUri);
555 556 557 558
		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 已提交
559 560 561 562 563

		if (!selectedDiffs.length) {
			return;
		}

564 565
		const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);

J
Joao Moreno 已提交
566
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
567
	}
J
Joao Moreno 已提交
568

J
Joao Moreno 已提交
569 570
	@command('git.revertSelectedRanges', { diff: true })
	async revertSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
571 572 573 574 575 576 577 578 579 580 581 582 583
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

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

J
Joao Moreno 已提交
584
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607
		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;
		}

608
		const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);
J
Joao Moreno 已提交
609 610 611 612 613
		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 已提交
614 615
	@command('git.unstage')
	async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
616
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
617
			const resource = this.getSCMResource();
618 619 620 621 622 623 624 625

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

629
		if (!scmResources.length) {
J
Joao Moreno 已提交
630 631 632
			return;
		}

633
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
634
		await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
J
Joao Moreno 已提交
635 636
	}

J
Joao Moreno 已提交
637 638
	@command('git.unstageAll', { repository: true })
	async unstageAll(repository: Repository): Promise<void> {
639
		await repository.revert([]);
J
Joao Moreno 已提交
640
	}
J
Joao Moreno 已提交
641

J
Joao Moreno 已提交
642 643
	@command('git.unstageSelectedRanges', { diff: true })
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
644 645 646 647 648 649 650 651 652
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

653 654 655 656 657 658 659
		if (modifiedUri.scheme !== 'git') {
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
J
Joao Moreno 已提交
660 661 662
			return;
		}

J
Joao Moreno 已提交
663
		const originalUri = toGitUri(modifiedUri, 'HEAD');
J
Joao Moreno 已提交
664
		const originalDocument = await workspace.openTextDocument(originalUri);
665 666 667 668
		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 已提交
669 670 671 672 673

		if (!selectedDiffs.length) {
			return;
		}

674 675
		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
J
Joao Moreno 已提交
676

J
Joao Moreno 已提交
677
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
678 679
	}

J
Joao Moreno 已提交
680 681
	@command('git.clean')
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
682
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
683
			const resource = this.getSCMResource();
684 685 686 687 688 689 690 691

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

J
Joao Moreno 已提交
695
		if (!scmResources.length) {
J
Joao Moreno 已提交
696 697
			return;
		}
J
Joao Moreno 已提交
698

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

J
Joao Moreno 已提交
703
		if (scmResources.length === 1) {
J
Joao Moreno 已提交
704
			if (untrackedCount > 0) {
J
Joao Moreno 已提交
705
				message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
706 707
				yes = localize('delete file', "Delete file");
			} else {
J
Joao Moreno 已提交
708
				message = localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
709 710
			}
		} else {
J
Joao Moreno 已提交
711
			message = localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", scmResources.length);
J
Joao Moreno 已提交
712 713 714 715 716

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

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

J
Joao Moreno 已提交
720 721 722 723
		if (pick !== yes) {
			return;
		}

J
Joao Moreno 已提交
724
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
725
		await this.runByRepository(resources, async (repository, resources) => repository.clean(resources));
J
Joao Moreno 已提交
726
	}
J
Joao Moreno 已提交
727

J
Joao Moreno 已提交
728 729
	@command('git.cleanAll', { repository: true })
	async cleanAll(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
730
		let resources = repository.workingTreeGroup.resourceStates;
J
Joao Moreno 已提交
731 732 733 734 735

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

J
Joao Moreno 已提交
736 737
		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 已提交
738

J
Joao Moreno 已提交
739 740 741 742 743 744
		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 已提交
745
				: localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
746
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
747

J
Joao Moreno 已提交
748
			if (pick !== yes) {
J
Joao Moreno 已提交
749 750 751
				return;
			}

J
Joao 已提交
752
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
753 754 755 756 757 758 759 760
			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 已提交
761 762
			}

J
Joao 已提交
763
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
764 765 766 767
		} 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 已提交
768

J
Joao Moreno 已提交
769 770 771
			if (pick !== yes) {
				return;
			}
J
Joao Moreno 已提交
772

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

J
Joao Moreno 已提交
775 776 777 778
		} 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 已提交
779

J
Joao Moreno 已提交
780 781 782 783 784
			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 已提交
785

J
Joao Moreno 已提交
786
			const yesAll = localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
787 788 789 790 791 792 793 794
			const pick = await window.showWarningMessage(message, { modal: true }, yesTracked, yesAll);

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

J
Joao 已提交
795
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
796
		}
J
Joao Moreno 已提交
797 798
	}

J
Joao Moreno 已提交
799
	private async smartCommit(
J
Joao Moreno 已提交
800
		repository: Repository,
801
		getCommitMessage: () => Promise<string | undefined>,
J
Joao Moreno 已提交
802 803
		opts?: CommitOptions
	): Promise<boolean> {
804 805
		const config = workspace.getConfiguration('git');
		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
806
		const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
J
Joao Moreno 已提交
807 808
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
809 810

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

J
Joao Moreno 已提交
813 814
			// 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?");
815 816 817 818 819
			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 已提交
820 821 822
				config.update('enableSmartCommit', true, true);
			} else if (pick !== yes) {
				return false; // do not commit on cancel
823 824 825
			}
		}

J
Joao Moreno 已提交
826
		if (!opts) {
827
			opts = { all: noStagedChanges };
J
Joao Moreno 已提交
828 829
		}

830 831 832
		// enable signing of commits if configurated
		opts.signCommit = enableCommitSigning;

J
Joao Moreno 已提交
833 834
		if (
			// no changes
835
			(noStagedChanges && noUnstagedChanges)
J
Joao Moreno 已提交
836
			// or no staged changes and not `all`
837
			|| (!opts.all && noStagedChanges)
J
Joao Moreno 已提交
838
		) {
J
Joao Moreno 已提交
839 840 841 842
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
843
		const message = await getCommitMessage();
J
Joao Moreno 已提交
844 845 846 847 848

		if (!message) {
			return false;
		}

J
Joao Moreno 已提交
849
		await repository.commit(message, opts);
J
Joao Moreno 已提交
850 851 852 853

		return true;
	}

J
Joao Moreno 已提交
854
	private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
J
Joao Moreno 已提交
855
		const message = repository.inputBox.value;
J
Joao Moreno 已提交
856
		const getCommitMessage = async () => {
J
Joao Moreno 已提交
857 858 859 860 861 862
			if (message) {
				return message;
			}

			return await window.showInputBox({
				placeHolder: localize('commit message', "Commit message"),
J
Joao Moreno 已提交
863 864
				prompt: localize('provide commit message', "Please provide a commit message"),
				ignoreFocusOut: true
J
Joao Moreno 已提交
865
			});
J
Joao Moreno 已提交
866 867
		};

J
Joao Moreno 已提交
868
		const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
J
Joao Moreno 已提交
869 870

		if (message && didCommit) {
J
Joao Moreno 已提交
871
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
872
		}
J
Joao Moreno 已提交
873 874
	}

J
Joao Moreno 已提交
875
	@command('git.commit', { repository: true })
J
Joao Moreno 已提交
876 877
	async commit(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository);
J
Joao Moreno 已提交
878 879
	}

J
Joao Moreno 已提交
880
	@command('git.commitWithInput', { repository: true })
J
Joao Moreno 已提交
881
	async commitWithInput(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
882
		if (!repository.inputBox.value) {
J
Joao Moreno 已提交
883 884 885
			return;
		}

J
Joao Moreno 已提交
886
		const didCommit = await this.smartCommit(repository, async () => repository.inputBox.value);
J
Joao Moreno 已提交
887 888

		if (didCommit) {
J
Joao Moreno 已提交
889
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
890
		}
J
Joao Moreno 已提交
891 892
	}

J
Joao Moreno 已提交
893
	@command('git.commitStaged', { repository: true })
J
Joao Moreno 已提交
894 895
	async commitStaged(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false });
J
Joao Moreno 已提交
896 897
	}

J
Joao Moreno 已提交
898
	@command('git.commitStagedSigned', { repository: true })
J
Joao Moreno 已提交
899 900
	async commitStagedSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, signoff: true });
J
Joao Moreno 已提交
901 902
	}

J
Joao Moreno 已提交
903
	@command('git.commitStagedAmend', { repository: true })
J
Joao Moreno 已提交
904 905
	async commitStagedAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, amend: true });
K
Krzysztof Cieślak 已提交
906 907
	}

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

J
Joao Moreno 已提交
913
	@command('git.commitAllSigned', { repository: true })
J
Joao Moreno 已提交
914 915
	async commitAllSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, signoff: true });
J
Joao Moreno 已提交
916 917
	}

J
Joao Moreno 已提交
918
	@command('git.commitAllAmend', { repository: true })
J
Joao Moreno 已提交
919 920
	async commitAllAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, amend: true });
K
Krzysztof Cieślak 已提交
921 922
	}

J
Joao Moreno 已提交
923
	@command('git.undoCommit', { repository: true })
J
Joao Moreno 已提交
924 925
	async undoCommit(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
926 927 928 929 930

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

J
Joao Moreno 已提交
931 932
		const commit = await repository.getCommit('HEAD');
		await repository.reset('HEAD~');
J
Joao Moreno 已提交
933
		repository.inputBox.value = commit.message;
J
Joao Moreno 已提交
934 935
	}

J
Joao Moreno 已提交
936
	@command('git.checkout', { repository: true })
J
Joao Moreno 已提交
937
	async checkout(repository: Repository, treeish: string): Promise<void> {
J
Joao Moreno 已提交
938
		if (typeof treeish === 'string') {
J
Joao Moreno 已提交
939
			return await repository.checkout(treeish);
J
Joao Moreno 已提交
940 941
		}

J
Joao Moreno 已提交
942
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
943
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
944 945 946
		const includeTags = checkoutType === 'all' || checkoutType === 'tags';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
947
		const createBranch = new CreateBranchItem(this);
J
Joao Moreno 已提交
948

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

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

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

J
Joao Moreno 已提交
958
		const picks = [createBranch, ...heads, ...tags, ...remoteHeads];
959
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
J
Joao Moreno 已提交
960
		const choice = await window.showQuickPick(picks, { placeHolder });
J
Joao Moreno 已提交
961 962 963 964 965

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
966
		await choice.run(repository);
J
Joao Moreno 已提交
967 968
	}

J
Joao Moreno 已提交
969
	@command('git.branch', { repository: true })
J
Joao Moreno 已提交
970
	async branch(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
971
		const result = await window.showInputBox({
J
Joao Moreno 已提交
972
			placeHolder: localize('branch name', "Branch name"),
J
Joao Moreno 已提交
973 974
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true
J
Joao Moreno 已提交
975
		});
J
Joao Moreno 已提交
976

J
Joao Moreno 已提交
977 978 979
		if (!result) {
			return;
		}
J
Joao Moreno 已提交
980

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

J
Joao Moreno 已提交
985
	@command('git.deleteBranch', { repository: true })
J
Joao Moreno 已提交
986
	async deleteBranch(repository: Repository, name: string, force?: boolean): Promise<void> {
987 988
		let run: (force?: boolean) => Promise<void>;
		if (typeof name === 'string') {
J
Joao Moreno 已提交
989
			run = force => repository.deleteBranch(name, force);
990
		} else {
J
Joao Moreno 已提交
991 992
			const currentHead = repository.HEAD && repository.HEAD.name;
			const heads = repository.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead)
993
				.map(ref => new BranchDeleteItem(ref));
M
Maik Riechert 已提交
994

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

M
Maik Riechert 已提交
998
			if (!choice || !choice.branchName) {
999 1000
				return;
			}
M
Maik Riechert 已提交
1001
			name = choice.branchName;
J
Joao Moreno 已提交
1002
			run = force => choice.run(repository, force);
M
Maik Riechert 已提交
1003 1004
		}

1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019
		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 已提交
1020 1021
	}

J
Joao Moreno 已提交
1022
	@command('git.merge', { repository: true })
J
Joao Moreno 已提交
1023
	async merge(repository: Repository): Promise<void> {
1024 1025 1026 1027
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string>('checkoutType') || 'all';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1028
		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
1029 1030
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1031

J
Joao Moreno 已提交
1032
		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
1033 1034
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1035 1036

		const picks = [...heads, ...remoteHeads];
1037 1038
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
1039 1040 1041 1042 1043

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1044
		try {
J
Joao Moreno 已提交
1045
			await choice.run(repository);
J
Joao Moreno 已提交
1046 1047 1048 1049 1050 1051 1052 1053
		} 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);
		}
1054 1055
	}

J
Joao Moreno 已提交
1056
	@command('git.createTag', { repository: true })
J
Joao Moreno 已提交
1057
	async createTag(repository: Repository): Promise<void> {
1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069
		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 已提交
1070
			prompt: localize('provide tag message', "Please provide a message to annotate the tag"),
1071 1072 1073 1074 1075
			ignoreFocusOut: true
		});

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

J
Joao Moreno 已提交
1079
	@command('git.pullFrom', { repository: true })
J
Joao Moreno 已提交
1080 1081
	async pullFrom(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105

		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 已提交
1106
		repository.pull(false, pick.label, branchName);
1107 1108
	}

J
Joao Moreno 已提交
1109
	@command('git.pull', { repository: true })
J
Joao Moreno 已提交
1110 1111
	async pull(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1112 1113 1114 1115 1116 1117

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

J
Joao Moreno 已提交
1118
		await repository.pull();
J
Joao Moreno 已提交
1119 1120
	}

J
Joao Moreno 已提交
1121
	@command('git.pullRebase', { repository: true })
J
Joao Moreno 已提交
1122 1123
	async pullRebase(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1124 1125 1126 1127 1128 1129

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

J
Joao Moreno 已提交
1130
		await repository.pullWithRebase();
J
Joao Moreno 已提交
1131 1132
	}

J
Joao Moreno 已提交
1133
	@command('git.push', { repository: true })
J
Joao Moreno 已提交
1134 1135
	async push(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1136 1137 1138 1139 1140 1141

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

J
Joao Moreno 已提交
1142
		await repository.push();
J
Joao Moreno 已提交
1143 1144
	}

J
Joao Moreno 已提交
1145
	@command('git.pushWithTags', { repository: true })
J
Joao Moreno 已提交
1146 1147
	async pushWithTags(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1148 1149 1150 1151 1152 1153

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

J
Joao Moreno 已提交
1154
		await repository.pushTags();
1155 1156 1157 1158

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

J
Joao Moreno 已提交
1159
	@command('git.pushTo', { repository: true })
J
Joao Moreno 已提交
1160 1161
	async pushTo(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1162 1163 1164 1165 1166 1167

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

J
Joao Moreno 已提交
1168
		if (!repository.HEAD || !repository.HEAD.name) {
J
Joao Moreno 已提交
1169 1170 1171 1172
			window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			return;
		}

J
Joao Moreno 已提交
1173
		const branchName = repository.HEAD.name;
J
Joao Moreno 已提交
1174 1175 1176 1177 1178 1179 1180 1181
		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 已提交
1182
		repository.pushTo(pick.label, branchName);
J
Joao Moreno 已提交
1183 1184
	}

J
Joao Moreno 已提交
1185
	@command('git.sync', { repository: true })
J
Joao Moreno 已提交
1186 1187
	async sync(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208

		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 已提交
1209
		await repository.sync();
J
Joao Moreno 已提交
1210 1211
	}

J
Joao 已提交
1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224
	@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 已提交
1225
	@command('git.publish', { repository: true })
J
Joao Moreno 已提交
1226 1227
	async publish(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1228 1229 1230 1231 1232 1233

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

J
Joao Moreno 已提交
1234
		const branchName = repository.HEAD && repository.HEAD.name || '';
M
Markus Wolf 已提交
1235 1236 1237 1238 1239 1240
		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 已提交
1241 1242 1243 1244 1245

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1246
		await repository.pushTo(choice, branchName, true);
J
Joao Moreno 已提交
1247 1248
	}

J
Joao Moreno 已提交
1249
	@command('git.showOutput')
J
Joao Moreno 已提交
1250 1251 1252 1253
	showOutput(): void {
		this.outputChannel.show();
	}

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

1259
			if (!resource) {
J
Joao Moreno 已提交
1260 1261 1262
				return;
			}

1263
			resourceStates = [resource];
J
Joao Moreno 已提交
1264 1265
		}

1266
		const resources = resourceStates
J
Joao Moreno 已提交
1267 1268 1269
			.filter(s => s instanceof Resource)
			.map(r => r.resourceUri);

1270
		if (!resources.length) {
N
NKumar2 已提交
1271 1272 1273
			return;
		}

1274
		await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
N
NKumar2 已提交
1275 1276
	}

J
Joao Moreno 已提交
1277
	@command('git.stash', { repository: true })
J
Joao Moreno 已提交
1278 1279
	async stash(repository: Repository): Promise<void> {
		if (repository.workingTreeGroup.resourceStates.length === 0) {
K
Krzysztof Cieślak 已提交
1280 1281 1282
			window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
			return;
		}
J
Joao Moreno 已提交
1283 1284 1285 1286 1287 1288 1289 1290 1291 1292

		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 已提交
1293
		await repository.createStash(message);
K
Krzysztof Cieślak 已提交
1294 1295
	}

J
Joao Moreno 已提交
1296
	@command('git.stashPop', { repository: true })
J
Joao Moreno 已提交
1297 1298
	async stashPop(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
1299 1300

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1301 1302 1303 1304
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

J
Joao Moreno 已提交
1305 1306
		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 已提交
1307 1308 1309 1310 1311
		const choice = await window.showQuickPick(picks, { placeHolder });

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

J
Joao Moreno 已提交
1313
		await repository.popStash(choice.id);
K
Krzysztof Cieślak 已提交
1314 1315
	}

J
Joao Moreno 已提交
1316
	@command('git.stashPopLatest', { repository: true })
J
Joao Moreno 已提交
1317 1318
	async stashPopLatest(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
1319 1320

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1321 1322 1323 1324
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

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

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

J
Joao Moreno 已提交
1332
			if (!options.repository) {
J
Joao Moreno 已提交
1333 1334
				result = Promise.resolve(method.apply(this, args));
			} else {
J
Joao Moreno 已提交
1335 1336
				// try to guess the repository based on the first argument
				const repository = this.model.getRepository(args[0]);
J
Joao Moreno 已提交
1337 1338 1339 1340 1341 1342 1343 1344 1345
				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();
				}
1346

J
Joao Moreno 已提交
1347
				result = repositoryPromise.then(repository => {
J
Joao Moreno 已提交
1348
					if (!repository) {
J
Joao 已提交
1349
						return Promise.resolve();
J
Joao Moreno 已提交
1350 1351
					}

J
Joao Moreno 已提交
1352
					return Promise.resolve(method.apply(this, [repository, ...args]));
J
Joao Moreno 已提交
1353
				});
J
Joao Moreno 已提交
1354 1355
			}

K
kieferrm 已提交
1356
			/* __GDPR__
K
kieferrm 已提交
1357 1358 1359 1360
				"git.command" : {
					"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
J
Joao Moreno 已提交
1361 1362
			this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

J
Joao Moreno 已提交
1363 1364 1365 1366
			return result.catch(async err => {
				let message: string;

				switch (err.gitErrorCode) {
1367
					case GitErrorCodes.DirtyWorkTree:
J
Joao Moreno 已提交
1368 1369
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
1370 1371 1372
					case GitErrorCodes.PushRejected:
						message = localize('cant push', "Can't push refs to remote. Run 'Pull' first to integrate your changes.");
						break;
J
Joao Moreno 已提交
1373
					default:
1374 1375 1376
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
J
Joao Moreno 已提交
1377
							.split(/[\r\n]/)
1378 1379 1380 1381 1382 1383
							.filter(line => !!line)
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401

						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();
				}
			});
		};
1402 1403 1404 1405 1406

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

		return result;
J
Joao Moreno 已提交
1407 1408
	}

1409
	private getSCMResource(uri?: Uri): Resource | undefined {
1410
		uri = uri ? uri : window.activeTextEditor && window.activeTextEditor.document.uri;
J
Joao Moreno 已提交
1411 1412

		if (!uri) {
1413
			return undefined;
J
Joao Moreno 已提交
1414 1415 1416
		}

		if (uri.scheme === 'git') {
J
Joao Moreno 已提交
1417 1418
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
1419 1420 1421 1422
		}

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

J
Joao Moreno 已提交
1425
			if (!repository) {
1426 1427
				return undefined;
			}
J
Joao Moreno 已提交
1428

J
Joao Moreno 已提交
1429 1430
			return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
				|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
J
Joao Moreno 已提交
1431 1432 1433
		}
	}

J
Joao Moreno 已提交
1434
	private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;
J
Joao Moreno 已提交
1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464
	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 已提交
1465 1466 1467
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
Joao Moreno 已提交
1468
}