commands.ts 45.9 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 356 357 358 359 360 361
		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 已提交
362 363
	}

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

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

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

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

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

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

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

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

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

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

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

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

		if (!resource) {
			return;
		}

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

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

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

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

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

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

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

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

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

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

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

489
		const selection = resourceStates.filter(s => s instanceof Resource) as Resource[];
J
Joao Moreno 已提交
490 491 492 493 494 495 496 497 498 499 500 501 502 503
		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));
504

505 506 507 508 509 510 511 512
			const yes = localize('yes', "Yes");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

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

516
		if (!scmResources.length) {
J
Joao Moreno 已提交
517 518
			return;
		}
J
Joao Moreno 已提交
519

520
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
521
		await this.runByRepository(resources, async (repository, resources) => repository.add(resources));
J
Joao Moreno 已提交
522 523
	}

J
Joao Moreno 已提交
524 525
	@command('git.stageAll', { repository: true })
	async stageAll(repository: Repository): Promise<void> {
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541
		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 已提交
542
		await repository.add([]);
J
Joao Moreno 已提交
543 544
	}

J
Joao Moreno 已提交
545 546
	@command('git.stageSelectedRanges', { diff: true })
	async stageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
547 548 549 550 551 552 553 554 555
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

J
Joao Moreno 已提交
556
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
557 558 559
			return;
		}

J
Joao Moreno 已提交
560
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
561
		const originalDocument = await workspace.openTextDocument(originalUri);
562 563 564 565
		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 已提交
566 567 568 569 570

		if (!selectedDiffs.length) {
			return;
		}

571 572
		const result = applyLineChanges(originalDocument, modifiedDocument, selectedDiffs);

J
Joao Moreno 已提交
573
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
574
	}
J
Joao Moreno 已提交
575

J
Joao Moreno 已提交
576 577
	@command('git.revertSelectedRanges', { diff: true })
	async revertSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
578 579 580 581 582 583 584 585 586 587 588 589 590
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

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

J
Joao Moreno 已提交
591
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614
		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;
		}

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

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

636
		if (!scmResources.length) {
J
Joao Moreno 已提交
637 638 639
			return;
		}

640
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
641
		await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
J
Joao Moreno 已提交
642 643
	}

J
Joao Moreno 已提交
644 645
	@command('git.unstageAll', { repository: true })
	async unstageAll(repository: Repository): Promise<void> {
646
		await repository.revert([]);
J
Joao Moreno 已提交
647
	}
J
Joao Moreno 已提交
648

J
Joao Moreno 已提交
649 650
	@command('git.unstageSelectedRanges', { diff: true })
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
651 652 653 654 655 656 657 658 659
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

660 661 662 663 664 665 666
		if (modifiedUri.scheme !== 'git') {
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
J
Joao Moreno 已提交
667 668 669
			return;
		}

J
Joao Moreno 已提交
670
		const originalUri = toGitUri(modifiedUri, 'HEAD');
J
Joao Moreno 已提交
671
		const originalDocument = await workspace.openTextDocument(originalUri);
672 673 674 675
		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 已提交
676 677 678 679 680

		if (!selectedDiffs.length) {
			return;
		}

681 682
		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
J
Joao Moreno 已提交
683

J
Joao Moreno 已提交
684
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
685 686
	}

J
Joao Moreno 已提交
687 688
	@command('git.clean')
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
689
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
690
			const resource = this.getSCMResource();
691 692 693 694 695 696 697 698

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

J
Joao Moreno 已提交
702
		if (!scmResources.length) {
J
Joao Moreno 已提交
703 704
			return;
		}
J
Joao Moreno 已提交
705

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

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

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

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

J
Joao Moreno 已提交
727 728 729 730
		if (pick !== yes) {
			return;
		}

J
Joao Moreno 已提交
731
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
732
		await this.runByRepository(resources, async (repository, resources) => repository.clean(resources));
J
Joao Moreno 已提交
733
	}
J
Joao Moreno 已提交
734

J
Joao Moreno 已提交
735 736
	@command('git.cleanAll', { repository: true })
	async cleanAll(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
737
		let resources = repository.workingTreeGroup.resourceStates;
J
Joao Moreno 已提交
738 739 740 741 742

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

J
Joao Moreno 已提交
743 744
		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 已提交
745

J
Joao Moreno 已提交
746 747 748 749 750 751
		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 已提交
752
				: localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
753
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
J
Joao Moreno 已提交
754

J
Joao Moreno 已提交
755
			if (pick !== yes) {
J
Joao Moreno 已提交
756 757 758
				return;
			}

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

J
Joao 已提交
770
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
771 772 773 774
		} 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 已提交
775

J
Joao Moreno 已提交
776 777 778
			if (pick !== yes) {
				return;
			}
J
Joao Moreno 已提交
779

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

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

J
Joao Moreno 已提交
787 788 789 790 791
			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 已提交
792

J
Joao Moreno 已提交
793
			const yesAll = localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
794 795 796 797 798 799 800 801
			const pick = await window.showWarningMessage(message, { modal: true }, yesTracked, yesAll);

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

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

J
Joao Moreno 已提交
806
	private async smartCommit(
J
Joao Moreno 已提交
807
		repository: Repository,
808
		getCommitMessage: () => Promise<string | undefined>,
J
Joao Moreno 已提交
809 810
		opts?: CommitOptions
	): Promise<boolean> {
811 812
		const config = workspace.getConfiguration('git');
		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
813
		const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
J
Joao Moreno 已提交
814 815
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
816 817

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

J
Joao Moreno 已提交
820 821
			// 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?");
822 823 824 825 826
			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 已提交
827 828 829
				config.update('enableSmartCommit', true, true);
			} else if (pick !== yes) {
				return false; // do not commit on cancel
830 831 832
			}
		}

J
Joao Moreno 已提交
833
		if (!opts) {
834
			opts = { all: noStagedChanges };
J
Joao Moreno 已提交
835 836
		}

837 838 839
		// enable signing of commits if configurated
		opts.signCommit = enableCommitSigning;

J
Joao Moreno 已提交
840 841
		if (
			// no changes
842
			(noStagedChanges && noUnstagedChanges)
J
Joao Moreno 已提交
843
			// or no staged changes and not `all`
844
			|| (!opts.all && noStagedChanges)
J
Joao Moreno 已提交
845
		) {
J
Joao Moreno 已提交
846 847 848 849
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
850
		const message = await getCommitMessage();
J
Joao Moreno 已提交
851 852 853 854 855

		if (!message) {
			return false;
		}

J
Joao Moreno 已提交
856
		await repository.commit(message, opts);
J
Joao Moreno 已提交
857 858 859 860

		return true;
	}

J
Joao Moreno 已提交
861
	private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
J
Joao Moreno 已提交
862
		const message = repository.inputBox.value;
J
Joao Moreno 已提交
863
		const getCommitMessage = async () => {
J
Joao Moreno 已提交
864 865 866 867 868 869
			if (message) {
				return message;
			}

			return await window.showInputBox({
				placeHolder: localize('commit message', "Commit message"),
J
Joao Moreno 已提交
870 871
				prompt: localize('provide commit message', "Please provide a commit message"),
				ignoreFocusOut: true
J
Joao Moreno 已提交
872
			});
J
Joao Moreno 已提交
873 874
		};

J
Joao Moreno 已提交
875
		const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
J
Joao Moreno 已提交
876 877

		if (message && didCommit) {
J
Joao Moreno 已提交
878
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
879
		}
J
Joao Moreno 已提交
880 881
	}

J
Joao Moreno 已提交
882
	@command('git.commit', { repository: true })
J
Joao Moreno 已提交
883 884
	async commit(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository);
J
Joao Moreno 已提交
885 886
	}

J
Joao Moreno 已提交
887
	@command('git.commitWithInput', { repository: true })
J
Joao Moreno 已提交
888
	async commitWithInput(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
889
		if (!repository.inputBox.value) {
J
Joao Moreno 已提交
890 891 892
			return;
		}

J
Joao Moreno 已提交
893
		const didCommit = await this.smartCommit(repository, async () => repository.inputBox.value);
J
Joao Moreno 已提交
894 895

		if (didCommit) {
J
Joao Moreno 已提交
896
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
897
		}
J
Joao Moreno 已提交
898 899
	}

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

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

J
Joao Moreno 已提交
910
	@command('git.commitStagedAmend', { repository: true })
J
Joao Moreno 已提交
911 912
	async commitStagedAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, amend: true });
K
Krzysztof Cieślak 已提交
913 914
	}

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

J
Joao Moreno 已提交
920
	@command('git.commitAllSigned', { repository: true })
J
Joao Moreno 已提交
921 922
	async commitAllSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, signoff: true });
J
Joao Moreno 已提交
923 924
	}

J
Joao Moreno 已提交
925
	@command('git.commitAllAmend', { repository: true })
J
Joao Moreno 已提交
926 927
	async commitAllAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, amend: true });
K
Krzysztof Cieślak 已提交
928 929
	}

J
Joao Moreno 已提交
930
	@command('git.undoCommit', { repository: true })
J
Joao Moreno 已提交
931 932
	async undoCommit(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
933 934 935 936 937

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

J
Joao Moreno 已提交
938 939
		const commit = await repository.getCommit('HEAD');
		await repository.reset('HEAD~');
J
Joao Moreno 已提交
940
		repository.inputBox.value = commit.message;
J
Joao Moreno 已提交
941 942
	}

J
Joao Moreno 已提交
943
	@command('git.checkout', { repository: true })
J
Joao Moreno 已提交
944
	async checkout(repository: Repository, treeish: string): Promise<void> {
J
Joao Moreno 已提交
945
		if (typeof treeish === 'string') {
J
Joao Moreno 已提交
946
			return await repository.checkout(treeish);
J
Joao Moreno 已提交
947 948
		}

J
Joao Moreno 已提交
949
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
950
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
951 952 953
		const includeTags = checkoutType === 'all' || checkoutType === 'tags';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
954
		const createBranch = new CreateBranchItem(this);
J
Joao Moreno 已提交
955

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

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

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

J
Joao Moreno 已提交
965
		const picks = [createBranch, ...heads, ...tags, ...remoteHeads];
966
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
J
Joao Moreno 已提交
967
		const choice = await window.showQuickPick(picks, { placeHolder });
J
Joao Moreno 已提交
968 969 970 971 972

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
973
		await choice.run(repository);
J
Joao Moreno 已提交
974 975
	}

J
Joao Moreno 已提交
976
	@command('git.branch', { repository: true })
J
Joao Moreno 已提交
977
	async branch(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
978
		const result = await window.showInputBox({
J
Joao Moreno 已提交
979
			placeHolder: localize('branch name', "Branch name"),
J
Joao Moreno 已提交
980 981
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true
J
Joao Moreno 已提交
982
		});
J
Joao Moreno 已提交
983

J
Joao Moreno 已提交
984 985 986
		if (!result) {
			return;
		}
J
Joao Moreno 已提交
987

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

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

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

M
Maik Riechert 已提交
1005
			if (!choice || !choice.branchName) {
1006 1007
				return;
			}
M
Maik Riechert 已提交
1008
			name = choice.branchName;
J
Joao Moreno 已提交
1009
			run = force => choice.run(repository, force);
M
Maik Riechert 已提交
1010 1011
		}

1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026
		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 已提交
1027 1028
	}

J
Joao Moreno 已提交
1029
	@command('git.merge', { repository: true })
J
Joao Moreno 已提交
1030
	async merge(repository: Repository): Promise<void> {
1031 1032 1033 1034
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string>('checkoutType') || 'all';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1035
		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
1036 1037
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1038

J
Joao Moreno 已提交
1039
		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
1040 1041
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1042 1043

		const picks = [...heads, ...remoteHeads];
1044 1045
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
1046 1047 1048 1049 1050

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1051
		try {
J
Joao Moreno 已提交
1052
			await choice.run(repository);
J
Joao Moreno 已提交
1053 1054 1055 1056 1057 1058 1059 1060
		} 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);
		}
1061 1062
	}

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

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

J
Joao Moreno 已提交
1086
	@command('git.pullFrom', { repository: true })
J
Joao Moreno 已提交
1087 1088
	async pullFrom(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112

		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 已提交
1113
		repository.pull(false, pick.label, branchName);
1114 1115
	}

J
Joao Moreno 已提交
1116
	@command('git.pull', { repository: true })
J
Joao Moreno 已提交
1117 1118
	async pull(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1119 1120 1121 1122 1123 1124

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

J
Joao Moreno 已提交
1125
		await repository.pull();
J
Joao Moreno 已提交
1126 1127
	}

J
Joao Moreno 已提交
1128
	@command('git.pullRebase', { repository: true })
J
Joao Moreno 已提交
1129 1130
	async pullRebase(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
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;
		}

J
Joao Moreno 已提交
1137
		await repository.pullWithRebase();
J
Joao Moreno 已提交
1138 1139
	}

J
Joao Moreno 已提交
1140
	@command('git.push', { repository: true })
J
Joao Moreno 已提交
1141 1142
	async push(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 push', "Your repository has no remotes configured to push to."));
			return;
		}

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

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

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

J
Joao Moreno 已提交
1161
		await repository.pushTags();
1162 1163 1164 1165

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

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

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

J
Joao Moreno 已提交
1175
		if (!repository.HEAD || !repository.HEAD.name) {
J
Joao Moreno 已提交
1176 1177 1178 1179
			window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			return;
		}

J
Joao Moreno 已提交
1180
		const branchName = repository.HEAD.name;
J
Joao Moreno 已提交
1181 1182 1183 1184 1185 1186 1187 1188
		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 已提交
1189
		repository.pushTo(pick.label, branchName);
J
Joao Moreno 已提交
1190 1191
	}

J
Joao Moreno 已提交
1192
	@command('git.sync', { repository: true })
J
Joao Moreno 已提交
1193 1194
	async sync(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215

		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 已提交
1216
		await repository.sync();
J
Joao Moreno 已提交
1217 1218
	}

J
Joao 已提交
1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231
	@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 已提交
1232
	@command('git.publish', { repository: true })
J
Joao Moreno 已提交
1233 1234
	async publish(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1235 1236 1237 1238 1239 1240

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

J
Joao Moreno 已提交
1241
		const branchName = repository.HEAD && repository.HEAD.name || '';
M
Markus Wolf 已提交
1242 1243 1244 1245 1246 1247
		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 已提交
1248 1249 1250 1251 1252

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1253
		await repository.pushTo(choice, branchName, true);
J
Joao Moreno 已提交
1254 1255
	}

J
Joao Moreno 已提交
1256
	@command('git.showOutput')
J
Joao Moreno 已提交
1257 1258 1259 1260
	showOutput(): void {
		this.outputChannel.show();
	}

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

1266
			if (!resource) {
J
Joao Moreno 已提交
1267 1268 1269
				return;
			}

1270
			resourceStates = [resource];
J
Joao Moreno 已提交
1271 1272
		}

1273
		const resources = resourceStates
J
Joao Moreno 已提交
1274 1275 1276
			.filter(s => s instanceof Resource)
			.map(r => r.resourceUri);

1277
		if (!resources.length) {
N
NKumar2 已提交
1278 1279 1280
			return;
		}

1281
		await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
N
NKumar2 已提交
1282 1283
	}

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

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

J
Joao Moreno 已提交
1303
	@command('git.stashPop', { repository: true })
J
Joao Moreno 已提交
1304 1305
	async stashPop(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
1306 1307

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1308 1309 1310 1311
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

J
Joao Moreno 已提交
1312 1313
		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 已提交
1314 1315 1316 1317 1318
		const choice = await window.showQuickPick(picks, { placeHolder });

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

J
Joao Moreno 已提交
1320
		await repository.popStash(choice.id);
K
Krzysztof Cieślak 已提交
1321 1322
	}

J
Joao Moreno 已提交
1323
	@command('git.stashPopLatest', { repository: true })
J
Joao Moreno 已提交
1324 1325
	async stashPopLatest(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
1326 1327

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1328 1329 1330 1331
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

J
Joao Moreno 已提交
1332
		await repository.popStash();
J
Joao Moreno 已提交
1333
	}
K
Krzysztof Cieślak 已提交
1334

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

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

J
Joao Moreno 已提交
1354
				result = repositoryPromise.then(repository => {
J
Joao Moreno 已提交
1355
					if (!repository) {
J
Joao 已提交
1356
						return Promise.resolve();
J
Joao Moreno 已提交
1357 1358
					}

J
Joao Moreno 已提交
1359
					return Promise.resolve(method.apply(this, [repository, ...args]));
J
Joao Moreno 已提交
1360
				});
J
Joao Moreno 已提交
1361 1362
			}

K
kieferrm 已提交
1363
			/* __GDPR__
K
kieferrm 已提交
1364 1365 1366 1367
				"git.command" : {
					"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
J
Joao Moreno 已提交
1368 1369
			this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

J
Joao Moreno 已提交
1370 1371 1372 1373
			return result.catch(async err => {
				let message: string;

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

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408

						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();
				}
			});
		};
1409 1410 1411 1412 1413

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

		return result;
J
Joao Moreno 已提交
1414 1415
	}

1416
	private getSCMResource(uri?: Uri): Resource | undefined {
1417
		uri = uri ? uri : window.activeTextEditor && window.activeTextEditor.document.uri;
J
Joao Moreno 已提交
1418 1419

		if (!uri) {
1420
			return undefined;
J
Joao Moreno 已提交
1421 1422 1423
		}

		if (uri.scheme === 'git') {
J
Joao Moreno 已提交
1424 1425
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
1426 1427 1428 1429
		}

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

J
Joao Moreno 已提交
1432
			if (!repository) {
1433 1434
				return undefined;
			}
J
Joao Moreno 已提交
1435

J
Joao Moreno 已提交
1436 1437
			return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
				|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
J
Joao Moreno 已提交
1438 1439 1440
		}
	}

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