commands.ts 79.2 KB
Newer Older
J
Joao Moreno 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import { lstat, Stats } from 'fs';
J
Joao Moreno 已提交
7
import * as os from 'os';
8 9
import * as path from 'path';
import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder } from 'vscode';
J
Joao Moreno 已提交
10
import TelemetryReporter from 'vscode-extension-telemetry';
J
Joao Moreno 已提交
11
import * as nls from 'vscode-nls';
12 13 14 15 16
import { Branch, GitErrorCodes, Ref, RefType, Status } from './api/git';
import { CommitOptions, ForcePushMode, Git, Stash } from './git';
import { Model } from './model';
import { Repository, Resource, ResourceGroupType } from './repository';
import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging';
J
Joao Moreno 已提交
17
import { fromGitUri, toGitUri, isGitUri } from './uri';
18
import { grep, isDescendant, pathEquals } from './util';
J
Joao Moreno 已提交
19 20

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

J
Joao Moreno 已提交
22 23 24 25 26 27
class CheckoutItem implements QuickPickItem {

	protected get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); }
	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> {
31
		const ref = this.ref.name;
J
Joao Moreno 已提交
32 33 34 35 36

		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
	async run(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
55
		if (!this.ref.name) {
J
Joao Moreno 已提交
56 57 58
			return;
		}

J
Joao Moreno 已提交
59 60 61 62
		const branches = await repository.findTrackingBranches(this.ref.name);

		if (branches.length > 0) {
			await repository.checkout(branches[0].name!);
63
		} else {
J
Joao Moreno 已提交
64
			await repository.checkoutTracking(this.ref.name);
65
		}
J
Joao Moreno 已提交
66 67 68
	}
}

M
Maik Riechert 已提交
69 70
class BranchDeleteItem implements QuickPickItem {

71 72 73
	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 已提交
74 75
	get description(): string { return this.shortCommit; }

76
	constructor(private ref: Ref) { }
M
Maik Riechert 已提交
77

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

86 87 88 89 90 91
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 已提交
92

J
Joao Moreno 已提交
93 94
	async run(repository: Repository): Promise<void> {
		await repository.merge(this.ref.name! || this.ref.commit!);
J
Joao Moreno 已提交
95
	}
96 97
}

J
Joao Moreno 已提交
98 99
class CreateBranchItem implements QuickPickItem {

J
Joao Moreno 已提交
100 101
	constructor(private cc: CommandCenter) { }

D
Daniel Imms 已提交
102
	get label(): string { return '$(plus) ' + localize('create branch', 'Create new branch...'); }
J
Joao Moreno 已提交
103 104
	get description(): string { return ''; }

C
Christof Marti 已提交
105
	get alwaysShow(): boolean { return true; }
106

J
Joao Moreno 已提交
107
	async run(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
108
		await this.cc.branch(repository);
J
Joao Moreno 已提交
109 110 111
	}
}

J
Joao Moreno 已提交
112 113 114 115
class CreateBranchFromItem implements QuickPickItem {

	constructor(private cc: CommandCenter) { }

D
Daniel Imms 已提交
116
	get label(): string { return '$(plus) ' + localize('create branch from', 'Create new branch from...'); }
J
Joao Moreno 已提交
117 118 119 120 121 122 123 124 125
	get description(): string { return ''; }

	get alwaysShow(): boolean { return true; }

	async run(repository: Repository): Promise<void> {
		await this.cc.branch(repository);
	}
}

J
Joao Moreno 已提交
126 127 128 129 130 131 132 133 134
class HEADItem implements QuickPickItem {

	constructor(private repository: Repository) { }

	get label(): string { return 'HEAD'; }
	get description(): string { return (this.repository.HEAD && this.repository.HEAD.commit || '').substr(0, 8); }
	get alwaysShow(): boolean { return true; }
}

135 136 137 138
class AddRemoteItem implements QuickPickItem {

	constructor(private cc: CommandCenter) { }

D
Daniel Imms 已提交
139
	get label(): string { return '$(plus) ' + localize('add remote', 'Add a new remote...'); }
140 141 142 143 144 145 146 147 148
	get description(): string { return ''; }

	get alwaysShow(): boolean { return true; }

	async run(repository: Repository): Promise<void> {
		await this.cc.addRemote(repository);
	}
}

J
Joao Moreno 已提交
149
interface CommandOptions {
J
Joao Moreno 已提交
150
	repository?: boolean;
J
Joao Moreno 已提交
151 152 153
	diff?: boolean;
}

154 155 156 157
interface Command {
	commandId: string;
	key: string;
	method: Function;
J
Joao Moreno 已提交
158
	options: CommandOptions;
159 160 161
}

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

J
Joao Moreno 已提交
163
function command(commandId: string, options: CommandOptions = {}): Function {
164
	return (_target: any, key: string, descriptor: any) => {
J
Joao Moreno 已提交
165 166 167 168
		if (!(typeof descriptor.value === 'function')) {
			throw new Error('not supported');
		}

J
Joao Moreno 已提交
169
		Commands.push({ commandId, key, method: descriptor.value, options });
J
Joao Moreno 已提交
170 171
	};
}
J
Joao Moreno 已提交
172

J
Joao Moreno 已提交
173 174 175 176 177 178 179 180
// const ImageMimetypes = [
// 	'image/png',
// 	'image/gif',
// 	'image/jpeg',
// 	'image/webp',
// 	'image/tiff',
// 	'image/bmp'
// ];
J
Joao Moreno 已提交
181

J
Joao Moreno 已提交
182
async function categorizeResourceByResolution(resources: Resource[]): Promise<{ merge: Resource[], resolved: Resource[], unresolved: Resource[], deletionConflicts: Resource[] }> {
183 184 185
	const selection = resources.filter(s => s instanceof Resource) as Resource[];
	const merge = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge);
	const isBothAddedOrModified = (s: Resource) => s.type === Status.BOTH_MODIFIED || s.type === Status.BOTH_ADDED;
J
Joao Moreno 已提交
186
	const isAnyDeleted = (s: Resource) => s.type === Status.DELETED_BY_THEM || s.type === Status.DELETED_BY_US;
187 188 189
	const possibleUnresolved = merge.filter(isBothAddedOrModified);
	const promises = possibleUnresolved.map(s => grep(s.resourceUri.fsPath, /^<{7}|^={7}|^>{7}/));
	const unresolvedBothModified = await Promise.all<boolean>(promises);
190
	const resolved = possibleUnresolved.filter((_s, i) => !unresolvedBothModified[i]);
J
Joao Moreno 已提交
191
	const deletionConflicts = merge.filter(s => isAnyDeleted(s));
192
	const unresolved = [
J
Joao Moreno 已提交
193
		...merge.filter(s => !isBothAddedOrModified(s) && !isAnyDeleted(s)),
194
		...possibleUnresolved.filter((_s, i) => unresolvedBothModified[i])
195 196
	];

J
Joao Moreno 已提交
197
	return { merge, resolved, unresolved, deletionConflicts };
198 199
}

J
Joao Moreno 已提交
200
function createCheckoutItems(repository: Repository): CheckoutItem[] {
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
	const config = workspace.getConfiguration('git');
	const checkoutType = config.get<string>('checkoutType') || 'all';
	const includeTags = checkoutType === 'all' || checkoutType === 'tags';
	const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

	const heads = repository.refs.filter(ref => ref.type === RefType.Head)
		.map(ref => new CheckoutItem(ref));
	const tags = (includeTags ? repository.refs.filter(ref => ref.type === RefType.Tag) : [])
		.map(ref => new CheckoutTagItem(ref));
	const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
		.map(ref => new CheckoutRemoteHeadItem(ref));

	return [...heads, ...tags, ...remoteHeads];
}

J
Joao Moreno 已提交
216 217 218 219
class TagItem implements QuickPickItem {
	get label(): string { return this.ref.name ?? ''; }
	get description(): string { return this.ref.commit?.substr(0, 8) ?? ''; }
	constructor(readonly ref: Ref) { }
220 221
}

222 223 224
enum PushType {
	Push,
	PushTo,
225
	PushFollowTags,
226 227 228 229 230
}

interface PushOptions {
	pushType: PushType;
	forcePush?: boolean;
J
Joao Moreno 已提交
231
	silent?: boolean;
232 233
}

J
Joao Moreno 已提交
234
export class CommandCenter {
J
Joao Moreno 已提交
235 236

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

J
Joao Moreno 已提交
238
	constructor(
J
Joao Moreno 已提交
239
		private git: Git,
J
Joao Moreno 已提交
240
		private model: Model,
J
Joao Moreno 已提交
241 242
		private outputChannel: OutputChannel,
		private telemetryReporter: TelemetryReporter
J
Joao Moreno 已提交
243
	) {
J
Joao Moreno 已提交
244 245
		this.disposables = Commands.map(({ commandId, key, method, options }) => {
			const command = this.createCommand(commandId, key, method, options);
246

J
Joao Moreno 已提交
247
			if (options.diff) {
J
Joao Moreno 已提交
248 249 250 251 252
				return commands.registerDiffInformationCommand(commandId, command);
			} else {
				return commands.registerCommand(commandId, command);
			}
		});
J
Joao Moreno 已提交
253 254
	}

J
Joao Moreno 已提交
255 256
	@command('git.refresh', { repository: true })
	async refresh(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
257
		await repository.status();
J
Joao Moreno 已提交
258
	}
J
Joao Moreno 已提交
259

J
Joao Moreno 已提交
260 261
	@command('git.openResource')
	async openResource(resource: Resource): Promise<void> {
J
Joao Moreno 已提交
262 263 264 265 266 267 268 269 270 271 272 273 274 275
		const repository = this.model.getRepository(resource.resourceUri);

		if (!repository) {
			return;
		}

		const config = workspace.getConfiguration('git', Uri.file(repository.root));
		const openDiffOnClick = config.get<boolean>('openDiffOnClick');

		if (openDiffOnClick) {
			await this._openResource(resource, undefined, true, false);
		} else {
			await this.openFile(resource);
		}
J
Joao Moreno 已提交
276 277
	}

J
Joao 已提交
278
	private async _openResource(resource: Resource, preview?: boolean, preserveFocus?: boolean, preserveSelection?: boolean): Promise<void> {
279
		let stat: Stats | undefined;
280

281 282 283 284
		try {
			stat = await new Promise<Stats>((c, e) => lstat(resource.resourceUri.fsPath, (err, stat) => err ? e(err) : c(stat)));
		} catch (err) {
			// noop
285
		}
286

287 288 289 290
		let left: Uri | undefined;
		let right: Uri | undefined;

		if (stat && stat.isDirectory()) {
291 292 293 294
			const repository = this.model.getRepositoryForSubmodule(resource.resourceUri);

			if (repository) {
				right = toGitUri(resource.resourceUri, resource.resourceGroupType === ResourceGroupType.Index ? 'index' : 'wt', { submoduleOf: repository.root });
295 296
			}
		} else {
J
Joao Moreno 已提交
297
			if (resource.type !== Status.DELETED_BY_THEM) {
J
Joao Moreno 已提交
298
				left = this.getLeftResource(resource);
J
Joao Moreno 已提交
299 300
			}

J
Joao Moreno 已提交
301
			right = this.getRightResource(resource);
302
		}
303

J
Joao Moreno 已提交
304 305
		const title = this.getTitle(resource);

J
Joao Moreno 已提交
306 307 308 309 310
		if (!right) {
			// TODO
			console.error('oh no');
			return;
		}
J
Joao Moreno 已提交
311

J
Joao Moreno 已提交
312
		const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
313 314
			preserveFocus,
			preview,
315
			viewColumn: ViewColumn.Active
J
Joao Moreno 已提交
316 317
		};

J
Joao Moreno 已提交
318 319
		const activeTextEditor = window.activeTextEditor;

320 321 322
		// Check if active text editor has same path as other editor. we cannot compare via
		// URI.toString() here because the schemas can be different. Instead we just go by path.
		if (preserveSelection && activeTextEditor && activeTextEditor.document.uri.path === right.path) {
J
Joao Moreno 已提交
323 324 325
			opts.selection = activeTextEditor.selection;
		}

326
		if (!left) {
J
Joao Moreno 已提交
327
			await commands.executeCommand<void>('vscode.open', right, opts, title);
J
Joao Moreno 已提交
328 329 330 331
		} else {
			await commands.executeCommand<void>('vscode.diff', left, right, title, opts);
		}
	}
J
Joao Moreno 已提交
332

J
Joao Moreno 已提交
333
	private getLeftResource(resource: Resource): Uri | undefined {
J
Joao Moreno 已提交
334 335 336
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
D
Darrien Singleton 已提交
337
			case Status.INDEX_ADDED:
J
Joao Moreno 已提交
338
				return toGitUri(resource.original, 'HEAD');
J
Joao Moreno 已提交
339 340

			case Status.MODIFIED:
341
			case Status.UNTRACKED:
J
Joao Moreno 已提交
342
				return toGitUri(resource.resourceUri, '~');
343 344

			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
345
				return toGitUri(resource.resourceUri, '');
J
Joao Moreno 已提交
346
		}
347
		return undefined;
J
Joao Moreno 已提交
348
	}
J
Joao Moreno 已提交
349

J
Joao Moreno 已提交
350
	private getRightResource(resource: Resource): Uri | undefined {
J
Joao Moreno 已提交
351 352 353 354 355
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_ADDED:
			case Status.INDEX_COPIED:
			case Status.INDEX_RENAMED:
J
Joao Moreno 已提交
356
				return toGitUri(resource.resourceUri, '');
J
Joao Moreno 已提交
357 358 359

			case Status.INDEX_DELETED:
			case Status.DELETED:
J
Joao Moreno 已提交
360
				return toGitUri(resource.resourceUri, 'HEAD');
J
Joao Moreno 已提交
361

J
Joao Moreno 已提交
362
			case Status.DELETED_BY_US:
J
Joao Moreno 已提交
363
				return toGitUri(resource.resourceUri, '~3');
J
Joao Moreno 已提交
364 365

			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
366
				return toGitUri(resource.resourceUri, '~2');
J
Joao Moreno 已提交
367

J
Joao Moreno 已提交
368 369 370
			case Status.MODIFIED:
			case Status.UNTRACKED:
			case Status.IGNORED:
371
			case Status.INTENT_TO_ADD:
J
Joao Moreno 已提交
372 373 374 375 376 377
				const repository = this.model.getRepository(resource.resourceUri);

				if (!repository) {
					return;
				}

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

J
Joao Moreno 已提交
381 382
				if (indexStatus && indexStatus.renameResourceUri) {
					return indexStatus.renameResourceUri;
J
Joao Moreno 已提交
383 384
				}

J
Joao Moreno 已提交
385
				return resource.resourceUri;
J
Joao Moreno 已提交
386

387
			case Status.BOTH_ADDED:
J
Joao Moreno 已提交
388
			case Status.BOTH_MODIFIED:
J
Joao Moreno 已提交
389
				return resource.resourceUri;
J
Joao Moreno 已提交
390
		}
391
		return undefined;
J
Joao Moreno 已提交
392 393 394
	}

	private getTitle(resource: Resource): string {
J
Joao Moreno 已提交
395
		const basename = path.basename(resource.resourceUri.fsPath);
J
Joao Moreno 已提交
396 397 398 399

		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
D
Darrien Singleton 已提交
400
			case Status.INDEX_ADDED:
J
Joao Moreno 已提交
401 402 403
				return `${basename} (Index)`;

			case Status.MODIFIED:
M
Marc Kassay 已提交
404 405
			case Status.BOTH_ADDED:
			case Status.BOTH_MODIFIED:
J
Joao Moreno 已提交
406
				return `${basename} (Working Tree)`;
J
Joao Moreno 已提交
407 408 409 410 411 412

			case Status.DELETED_BY_US:
				return `${basename} (Theirs)`;

			case Status.DELETED_BY_THEM:
				return `${basename} (Ours)`;
D
Darrien Singleton 已提交
413 414 415 416

			case Status.UNTRACKED:

				return `${basename} (Untracked)`;
J
Joao Moreno 已提交
417 418 419 420 421
		}

		return '';
	}

J
Joao Moreno 已提交
422
	@command('git.clone')
423 424 425 426 427 428 429
	async clone(url?: string): Promise<void> {
		if (!url) {
			url = await window.showInputBox({
				prompt: localize('repourl', "Repository URL"),
				ignoreFocusOut: true
			});
		}
J
Joao Moreno 已提交
430 431

		if (!url) {
K
kieferrm 已提交
432
			/* __GDPR__
K
kieferrm 已提交
433 434 435 436
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
437 438
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
			return;
J
Joao Moreno 已提交
439 440
		}

441
		url = url.trim().replace(/^git\s+clone\s+/, '');
H
Howard Hung 已提交
442

443
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
444 445 446 447 448 449 450 451 452
		let defaultCloneDirectory = config.get<string>('defaultCloneDirectory') || os.homedir();
		defaultCloneDirectory = defaultCloneDirectory.replace(/^~/, os.homedir());

		const uris = await window.showOpenDialog({
			canSelectFiles: false,
			canSelectFolders: true,
			canSelectMany: false,
			defaultUri: Uri.file(defaultCloneDirectory),
			openLabel: localize('selectFolder', "Select Repository Location")
J
Joao Moreno 已提交
453 454
		});

J
Joao Moreno 已提交
455
		if (!uris || uris.length === 0) {
K
kieferrm 已提交
456
			/* __GDPR__
K
kieferrm 已提交
457 458 459 460
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
461 462
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
			return;
J
Joao Moreno 已提交
463 464
		}

J
Joao Moreno 已提交
465 466 467
		const uri = uris[0];
		const parentPath = uri.fsPath;

468
		try {
469 470 471 472 473
			const opts = {
				location: ProgressLocation.Notification,
				title: localize('cloning', "Cloning git repository '{0}'...", url),
				cancellable: true
			};
M
Maryam Archie 已提交
474

475 476
			const repositoryPath = await window.withProgress(
				opts,
J
Joao Moreno 已提交
477
				(progress, token) => this.git.clone(url!, parentPath, progress, token)
478
			);
479

J
Joao Moreno 已提交
480 481 482 483
			let message = localize('proposeopen', "Would you like to open the cloned repository?");
			const open = localize('openrepo', "Open");
			const openNewWindow = localize('openreponew', "Open in New Window");
			const choices = [open, openNewWindow];
484 485 486

			const addToWorkspace = localize('add', "Add to Workspace");
			if (workspace.workspaceFolders) {
J
Joao Moreno 已提交
487
				message = localize('proposeopen2', "Would you like to open the cloned repository, or add it to the current workspace?");
488 489 490 491
				choices.push(addToWorkspace);
			}

			const result = await window.showInformationMessage(message, ...choices);
492 493

			const openFolder = result === open;
K
kieferrm 已提交
494
			/* __GDPR__
K
kieferrm 已提交
495 496 497 498 499
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
					"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
				}
			*/
500
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: openFolder ? 1 : 0 });
501 502 503

			const uri = Uri.file(repositoryPath);

504
			if (openFolder) {
505 506 507
				commands.executeCommand('vscode.openFolder', uri);
			} else if (result === addToWorkspace) {
				workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
508 509
			} else if (result === openNewWindow) {
				commands.executeCommand('vscode.openFolder', uri, true);
510
			}
511 512
		} catch (err) {
			if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
K
kieferrm 已提交
513
				/* __GDPR__
K
kieferrm 已提交
514 515 516 517
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
518
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
J
Joao Moreno 已提交
519 520
			} else if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
				return;
521
			} else {
K
kieferrm 已提交
522
				/* __GDPR__
K
kieferrm 已提交
523 524 525 526
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
527
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
528
			}
J
Joao Moreno 已提交
529

530
			throw err;
J
Joao Moreno 已提交
531 532 533
		}
	}

J
Joao Moreno 已提交
534
	@command('git.init')
J
Joao Moreno 已提交
535
	async init(): Promise<void> {
J
Joao Moreno 已提交
536
		let repositoryPath: string | undefined = undefined;
J
Joao Moreno 已提交
537
		let askToOpen = true;
J
Joao Moreno 已提交
538

J
Joao Moreno 已提交
539
		if (workspace.workspaceFolders) {
J
Joao Moreno 已提交
540
			const placeHolder = localize('init', "Pick workspace folder to initialize git repo in");
J
Joao Moreno 已提交
541 542 543 544 545
			const pick = { label: localize('choose', "Choose Folder...") };
			const items: { label: string, folder?: WorkspaceFolder }[] = [
				...workspace.workspaceFolders.map(folder => ({ label: folder.name, description: folder.uri.fsPath, folder })),
				pick
			];
J
Joao Moreno 已提交
546 547 548 549
			const item = await window.showQuickPick(items, { placeHolder, ignoreFocusOut: true });

			if (!item) {
				return;
J
Joao Moreno 已提交
550 551
			} else if (item.folder) {
				repositoryPath = item.folder.uri.fsPath;
J
Joao Moreno 已提交
552
				askToOpen = false;
J
Joao Moreno 已提交
553 554
			}
		}
J
Joao Moreno 已提交
555

J
Joao Moreno 已提交
556
		if (!repositoryPath) {
J
Joao Moreno 已提交
557 558 559 560 561 562 563 564 565 566 567 568
			const homeUri = Uri.file(os.homedir());
			const defaultUri = workspace.workspaceFolders && workspace.workspaceFolders.length > 0
				? Uri.file(workspace.workspaceFolders[0].uri.fsPath)
				: homeUri;

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

J
Joao Moreno 已提交
570
			if (!result || result.length === 0) {
J
Joao Moreno 已提交
571 572
				return;
			}
J
Joao Moreno 已提交
573 574 575 576 577 578 579 580 581 582 583 584

			const uri = result[0];

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

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

J
Joao Moreno 已提交
585
			repositoryPath = uri.fsPath;
J
Joao Moreno 已提交
586 587 588 589

			if (workspace.workspaceFolders && workspace.workspaceFolders.some(w => w.uri.toString() === uri.toString())) {
				askToOpen = false;
			}
J
Joao Moreno 已提交
590 591
		}

J
Joao Moreno 已提交
592
		await this.git.init(repositoryPath);
I
Ivan Sučić 已提交
593

J
Joao Moreno 已提交
594 595 596 597
		let message = localize('proposeopen init', "Would you like to open the initialized repository?");
		const open = localize('openrepo', "Open");
		const openNewWindow = localize('openreponew', "Open in New Window");
		const choices = [open, openNewWindow];
J
Joao Moreno 已提交
598

J
Joao Moreno 已提交
599 600 601 602
		if (!askToOpen) {
			return;
		}

J
Joao Moreno 已提交
603 604
		const addToWorkspace = localize('add', "Add to Workspace");
		if (workspace.workspaceFolders) {
J
Joao Moreno 已提交
605
			message = localize('proposeopen2 init', "Would you like to open the initialized repository, or add it to the current workspace?");
J
Joao Moreno 已提交
606 607
			choices.push(addToWorkspace);
		}
I
Ivan Sučić 已提交
608 609

		const result = await window.showInformationMessage(message, ...choices);
J
Joao Moreno 已提交
610
		const uri = Uri.file(repositoryPath);
I
Ivan Sučić 已提交
611

J
Joao Moreno 已提交
612 613 614 615
		if (result === open) {
			commands.executeCommand('vscode.openFolder', uri);
		} else if (result === addToWorkspace) {
			workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
616 617
		} else if (result === openNewWindow) {
			commands.executeCommand('vscode.openFolder', uri, true);
J
Joao Moreno 已提交
618 619
		} else {
			await this.model.openRepository(repositoryPath);
I
Ivan Sučić 已提交
620
		}
J
Joao Moreno 已提交
621 622
	}

J
Joao Moreno 已提交
623 624
	@command('git.openRepository', { repository: false })
	async openRepository(path?: string): Promise<void> {
625
		if (!path) {
J
Joao Moreno 已提交
626 627 628 629 630 631
			const result = await window.showOpenDialog({
				canSelectFiles: false,
				canSelectFolders: true,
				canSelectMany: false,
				defaultUri: Uri.file(os.homedir()),
				openLabel: localize('open repo', "Open Repository")
632 633
			});

J
Joao Moreno 已提交
634
			if (!result || result.length === 0) {
635 636 637
				return;
			}

J
Joao Moreno 已提交
638
			path = result[0].fsPath;
639
		}
J
Joao Moreno 已提交
640 641

		await this.model.openRepository(path);
642 643
	}

J
Joao Moreno 已提交
644 645 646 647 648
	@command('git.close', { repository: true })
	async close(repository: Repository): Promise<void> {
		this.model.close(repository);
	}

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

653
		let uris: Uri[] | undefined;
654 655

		if (arg instanceof Uri) {
J
Joao Moreno 已提交
656
			if (isGitUri(arg)) {
657
				uris = [Uri.file(fromGitUri(arg).path)];
658
			} else if (arg.scheme === 'file') {
659
				uris = [arg];
660 661 662 663 664 665
			}
		} else {
			let resource = arg;

			if (!(resource instanceof Resource)) {
				// can happen when called from a keybinding
666
				resource = this.getSCMResource();
667 668 669
			}

			if (resource) {
J
Joao Moreno 已提交
670 671 672 673 674
				uris = ([resource, ...resourceStates] as Resource[])
					.filter(r => r.type !== Status.DELETED && r.type !== Status.INDEX_DELETED)
					.map(r => r.resourceUri);
			} else if (window.activeTextEditor) {
				uris = [window.activeTextEditor.document.uri];
675
			}
J
Joao Moreno 已提交
676 677
		}

678
		if (!uris) {
J
Joao Moreno 已提交
679
			return;
J
Joao Moreno 已提交
680 681
		}

682
		const activeTextEditor = window.activeTextEditor;
J
Joao Moreno 已提交
683

684 685
		for (const uri of uris) {
			const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
686
				preserveFocus,
J
Joao Moreno 已提交
687
				preview: false,
688
				viewColumn: ViewColumn.Active
689 690
			};

M
Micah Smith 已提交
691 692 693 694 695 696 697
			let document;
			try {
				document = await workspace.openTextDocument(uri);
			} catch (error) {
				await commands.executeCommand<void>('vscode.open', uri, opts);
				continue;
			}
E
Eric Gang 已提交
698

699 700 701
			// Check if active text editor has same path as other editor. we cannot compare via
			// URI.toString() here because the schemas can be different. Instead we just go by path.
			if (activeTextEditor && activeTextEditor.document.uri.path === uri.path) {
E
Eric Gang 已提交
702
				// preserve not only selection but also visible range
B
Benjamin Pasero 已提交
703
				opts.selection = activeTextEditor.selection;
E
Eric Gang 已提交
704 705 706 707 708
				const previousVisibleRanges = activeTextEditor.visibleRanges;
				const editor = await window.showTextDocument(document, opts);
				editor.revealRange(previousVisibleRanges[0]);
			} else {
				await window.showTextDocument(document, opts);
B
Benjamin Pasero 已提交
709
			}
J
Joao Moreno 已提交
710
		}
J
Joao Moreno 已提交
711 712
	}

J
Joao Moreno 已提交
713 714 715 716 717
	@command('git.openFile2')
	async openFile2(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
		this.openFile(arg, ...resourceStates);
	}

J
Joao Moreno 已提交
718 719
	@command('git.openHEADFile')
	async openHEADFile(arg?: Resource | Uri): Promise<void> {
D
Duroktar 已提交
720
		let resource: Resource | undefined = undefined;
J
Joao Moreno 已提交
721
		const preview = !(arg instanceof Resource);
D
Duroktar 已提交
722 723 724 725

		if (arg instanceof Resource) {
			resource = arg;
		} else if (arg instanceof Uri) {
726
			resource = this.getSCMResource(arg);
D
Duroktar 已提交
727
		} else {
728
			resource = this.getSCMResource();
D
Duroktar 已提交
729 730 731 732 733 734
		}

		if (!resource) {
			return;
		}

J
Joao Moreno 已提交
735
		const HEAD = this.getLeftResource(resource);
736 737
		const basename = path.basename(resource.resourceUri.fsPath);
		const title = `${basename} (HEAD)`;
D
Duroktar 已提交
738

J
Joao Moreno 已提交
739 740 741
		if (!HEAD) {
			window.showWarningMessage(localize('HEAD not available', "HEAD version of '{0}' is not available.", path.basename(resource.resourceUri.fsPath)));
			return;
D
Duroktar 已提交
742
		}
J
Joao Moreno 已提交
743

J
Joao Moreno 已提交
744 745 746 747
		const opts: TextDocumentShowOptions = {
			preview
		};

748
		return await commands.executeCommand<void>('vscode.open', HEAD, opts, title);
D
Duroktar 已提交
749 750
	}

J
Joao Moreno 已提交
751 752
	@command('git.openChange')
	async openChange(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
753
		const preserveFocus = arg instanceof Resource;
J
Joao Moreno 已提交
754 755
		const preview = !(arg instanceof Resource);

J
Joao 已提交
756
		const preserveSelection = arg instanceof Uri || !arg;
757
		let resources: Resource[] | undefined = undefined;
758

759
		if (arg instanceof Uri) {
760
			const resource = this.getSCMResource(arg);
761 762 763
			if (resource !== undefined) {
				resources = [resource];
			}
764
		} else {
765
			let resource: Resource | undefined = undefined;
J
Joao Moreno 已提交
766

767 768 769
			if (arg instanceof Resource) {
				resource = arg;
			} else {
770
				resource = this.getSCMResource();
771 772 773 774 775
			}

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

778
		if (!resources) {
J
Joao Moreno 已提交
779
			return;
J
Joao Moreno 已提交
780
		}
J
Joao Moreno 已提交
781

782
		for (const resource of resources) {
J
Joao 已提交
783
			await this._openResource(resource, preview, preserveFocus, preserveSelection);
784
		}
J
Joao Moreno 已提交
785 786
	}

787 788
	@command('git.stage')
	async stage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
789 790
		this.outputChannel.appendLine(`git.stage ${resourceStates.length}`);

791 792
		resourceStates = resourceStates.filter(s => !!s);

J
Joao Moreno 已提交
793
		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
794
			const resource = this.getSCMResource();
795

J
Joao Moreno 已提交
796 797
			this.outputChannel.appendLine(`git.stage.getSCMResource ${resource ? resource.resourceUri.toString() : null}`);

798 799 800 801 802 803 804
			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

805
		const selection = resourceStates.filter(s => s instanceof Resource) as Resource[];
J
Joao Moreno 已提交
806
		const { resolved, unresolved, deletionConflicts } = await categorizeResourceByResolution(selection);
807 808 809 810 811

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

813 814 815 816 817 818 819 820
			const yes = localize('yes', "Yes");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

J
Joao Moreno 已提交
821 822 823 824 825 826 827 828 829 830 831 832 833 834
		try {
			await this.runByRepository(deletionConflicts.map(r => r.resourceUri), async (repository, resources) => {
				for (const resource of resources) {
					await this._stageDeletionConflict(repository, resource);
				}
			});
		} catch (err) {
			if (/Cancelled/.test(err.message)) {
				return;
			}

			throw err;
		}

J
Joao Moreno 已提交
835
		const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree);
836 837
		const untracked = selection.filter(s => s.resourceGroupType === ResourceGroupType.Untracked);
		const scmResources = [...workingTree, ...untracked, ...resolved, ...unresolved];
838

J
Joao Moreno 已提交
839
		this.outputChannel.appendLine(`git.stage.scmResources ${scmResources.length}`);
840
		if (!scmResources.length) {
J
Joao Moreno 已提交
841 842
			return;
		}
J
Joao Moreno 已提交
843

844
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
845
		await this.runByRepository(resources, async (repository, resources) => repository.add(resources));
J
Joao Moreno 已提交
846 847
	}

J
Joao Moreno 已提交
848 849
	@command('git.stageAll', { repository: true })
	async stageAll(repository: Repository): Promise<void> {
850
		const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[];
J
Joao Moreno 已提交
851 852 853 854 855 856 857 858 859 860 861 862 863
		const { merge, unresolved, deletionConflicts } = await categorizeResourceByResolution(resources);

		try {
			for (const deletionConflict of deletionConflicts) {
				await this._stageDeletionConflict(repository, deletionConflict.resourceUri);
			}
		} catch (err) {
			if (/Cancelled/.test(err.message)) {
				return;
			}

			throw err;
		}
864 865 866

		if (unresolved.length > 0) {
			const message = unresolved.length > 1
J
Joao Moreno 已提交
867 868
				? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", merge.length)
				: localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(merge[0].resourceUri.fsPath));
869 870 871 872 873 874 875 876 877

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

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

878
		const config = workspace.getConfiguration('git', Uri.file(repository.root));
879 880
		const untrackedChanges = config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
		await repository.add([], untrackedChanges === 'mixed' ? undefined : { update: true });
J
Joao Moreno 已提交
881 882
	}

J
Joao Moreno 已提交
883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917
	private async _stageDeletionConflict(repository: Repository, uri: Uri): Promise<void> {
		const uriString = uri.toString();
		const resource = repository.mergeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];

		if (!resource) {
			return;
		}

		if (resource.type === Status.DELETED_BY_THEM) {
			const keepIt = localize('keep ours', "Keep Our Version");
			const deleteIt = localize('delete', "Delete File");
			const result = await window.showInformationMessage(localize('deleted by them', "File '{0}' was deleted by them and modified by us.\n\nWhat would you like to do?", path.basename(uri.fsPath)), { modal: true }, keepIt, deleteIt);

			if (result === keepIt) {
				await repository.add([uri]);
			} else if (result === deleteIt) {
				await repository.rm([uri]);
			} else {
				throw new Error('Cancelled');
			}
		} else if (resource.type === Status.DELETED_BY_US) {
			const keepIt = localize('keep theirs', "Keep Their Version");
			const deleteIt = localize('delete', "Delete File");
			const result = await window.showInformationMessage(localize('deleted by us', "File '{0}' was deleted by us and modified by them.\n\nWhat would you like to do?", path.basename(uri.fsPath)), { modal: true }, keepIt, deleteIt);

			if (result === keepIt) {
				await repository.add([uri]);
			} else if (result === deleteIt) {
				await repository.rm([uri]);
			} else {
				throw new Error('Cancelled');
			}
		}
	}

918 919 920 921 922 923 924 925 926
	@command('git.stageAllTracked', { repository: true })
	async stageAllTracked(repository: Repository): Promise<void> {
		const resources = repository.workingTreeGroup.resourceStates
			.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
		const uris = resources.map(r => r.resourceUri);

		await repository.add(uris);
	}

927 928 929 930 931 932 933 934 935
	@command('git.stageAllUntracked', { repository: true })
	async stageAllUntracked(repository: Repository): Promise<void> {
		const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates]
			.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);
		const uris = resources.map(r => r.resourceUri);

		await repository.add(uris);
	}

J
Joao Moreno 已提交
936
	@command('git.stageChange')
J
Joao Moreno 已提交
937 938 939 940 941 942 943 944
	async stageChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
		const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];

		if (!textEditor) {
			return;
		}

		await this._stageChanges(textEditor, [changes[index]]);
J
Joao Moreno 已提交
945 946
	}

J
Joao Moreno 已提交
947
	@command('git.stageSelectedRanges', { diff: true })
J
Joao Moreno 已提交
948
	async stageSelectedChanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
949 950 951 952 953 954 955
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
J
Joao Moreno 已提交
956 957 958 959
		const selectedLines = toLineRanges(textEditor.selections, modifiedDocument);
		const selectedChanges = changes
			.map(diff => selectedLines.reduce<LineChange | null>((result, range) => result || intersectDiffWithRange(modifiedDocument, diff, range), null))
			.filter(d => !!d) as LineChange[];
J
Joao Moreno 已提交
960

J
Joao Moreno 已提交
961
		if (!selectedChanges.length) {
J
Joao Moreno 已提交
962 963 964
			return;
		}

J
Joao Moreno 已提交
965 966
		await this._stageChanges(textEditor, selectedChanges);
	}
J
Joao Moreno 已提交
967

J
Joao Moreno 已提交
968
	private async _stageChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
969 970 971
		const modifiedDocument = textEditor.document;
		const modifiedUri = modifiedDocument.uri;

J
Joao Moreno 已提交
972
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
973 974 975
			return;
		}

J
Joao Moreno 已提交
976
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
977
		const originalDocument = await workspace.openTextDocument(originalUri);
J
Joao Moreno 已提交
978
		const result = applyLineChanges(originalDocument, modifiedDocument, changes);
J
Joao Moreno 已提交
979

J
Joao Moreno 已提交
980
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
981
	}
J
Joao Moreno 已提交
982

J
Joao Moreno 已提交
983
	@command('git.revertChange')
J
Joao Moreno 已提交
984 985 986 987
	async revertChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
		const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];

		if (!textEditor) {
J
Joao Moreno 已提交
988 989 990
			return;
		}

J
Joao Moreno 已提交
991
		await this._revertChanges(textEditor, [...changes.slice(0, index), ...changes.slice(index + 1)]);
J
Joao Moreno 已提交
992
	}
J
Joao Moreno 已提交
993

J
Joao Moreno 已提交
994
	@command('git.revertSelectedRanges', { diff: true })
J
Joao Moreno 已提交
995
	async revertSelectedRanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
996 997 998 999 1000 1001 1002
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
J
Joao Moreno 已提交
1003 1004
		const selections = textEditor.selections;
		const selectedChanges = changes.filter(change => {
J
Joao Moreno 已提交
1005
			const modifiedRange = getModifiedRange(modifiedDocument, change);
J
Joao Moreno 已提交
1006 1007 1008 1009
			return selections.every(selection => !selection.intersection(modifiedRange));
		});

		if (selectedChanges.length === changes.length) {
J
Joao Moreno 已提交
1010 1011 1012
			return;
		}

J
Joao Moreno 已提交
1013 1014
		await this._revertChanges(textEditor, selectedChanges);
	}
J
Joao Moreno 已提交
1015

J
Joao Moreno 已提交
1016 1017 1018
	private async _revertChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
		const modifiedDocument = textEditor.document;
		const modifiedUri = modifiedDocument.uri;
J
Joao Moreno 已提交
1019

J
Joao Moreno 已提交
1020
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
1021 1022 1023
			return;
		}

J
Joao Moreno 已提交
1024 1025
		const originalUri = toGitUri(modifiedUri, '~');
		const originalDocument = await workspace.openTextDocument(originalUri);
1026 1027
		const selectionsBeforeRevert = textEditor.selections;
		const visibleRangesBeforeRevert = textEditor.visibleRanges;
J
Joao Moreno 已提交
1028
		const result = applyLineChanges(originalDocument, modifiedDocument, changes);
1029

J
Joao Moreno 已提交
1030 1031 1032
		const edit = new WorkspaceEdit();
		edit.replace(modifiedUri, new Range(new Position(0, 0), modifiedDocument.lineAt(modifiedDocument.lineCount - 1).range.end), result);
		workspace.applyEdit(edit);
1033

J
Joao Moreno 已提交
1034 1035
		await modifiedDocument.save();

1036 1037
		textEditor.selections = selectionsBeforeRevert;
		textEditor.revealRange(visibleRangesBeforeRevert[0]);
J
Joao Moreno 已提交
1038 1039
	}

J
Joao Moreno 已提交
1040 1041
	@command('git.unstage')
	async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
1042 1043
		resourceStates = resourceStates.filter(s => !!s);

J
Joao Moreno 已提交
1044
		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
1045
			const resource = this.getSCMResource();
1046 1047 1048 1049 1050 1051 1052 1053

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

1057
		if (!scmResources.length) {
J
Joao Moreno 已提交
1058 1059 1060
			return;
		}

1061
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
1062
		await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
J
Joao Moreno 已提交
1063 1064
	}

J
Joao Moreno 已提交
1065 1066
	@command('git.unstageAll', { repository: true })
	async unstageAll(repository: Repository): Promise<void> {
1067
		await repository.revert([]);
J
Joao Moreno 已提交
1068
	}
J
Joao Moreno 已提交
1069

J
Joao Moreno 已提交
1070 1071
	@command('git.unstageSelectedRanges', { diff: true })
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
1072 1073 1074 1075 1076 1077 1078 1079 1080
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

J
Joao Moreno 已提交
1081
		if (!isGitUri(modifiedUri)) {
1082 1083 1084 1085 1086 1087
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
J
Joao Moreno 已提交
1088 1089 1090
			return;
		}

J
Joao Moreno 已提交
1091
		const originalUri = toGitUri(modifiedUri, 'HEAD');
J
Joao Moreno 已提交
1092
		const originalDocument = await workspace.openTextDocument(originalUri);
1093 1094 1095 1096
		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 已提交
1097 1098 1099 1100 1101

		if (!selectedDiffs.length) {
			return;
		}

1102 1103
		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
J
Joao Moreno 已提交
1104

J
Joao Moreno 已提交
1105
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
1106 1107
	}

J
Joao Moreno 已提交
1108 1109
	@command('git.clean')
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
1110 1111
		resourceStates = resourceStates.filter(s => !!s);

J
Joao Moreno 已提交
1112
		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
1113
			const resource = this.getSCMResource();
1114 1115 1116 1117 1118 1119 1120 1121

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

1122 1123
		const scmResources = resourceStates.filter(s => s instanceof Resource
			&& (s.resourceGroupType === ResourceGroupType.WorkingTree || s.resourceGroupType === ResourceGroupType.Untracked)) as Resource[];
1124

J
Joao Moreno 已提交
1125
		if (!scmResources.length) {
J
Joao Moreno 已提交
1126 1127
			return;
		}
J
Joao Moreno 已提交
1128

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

J
Joao Moreno 已提交
1133
		if (scmResources.length === 1) {
J
Joao Moreno 已提交
1134
			if (untrackedCount > 0) {
J
Joao Moreno 已提交
1135
				message = localize('confirm delete', "Are you sure you want to DELETE {0}?\nThis is IRREVERSIBLE!\nThis file will be FOREVER LOST.", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
1136 1137
				yes = localize('delete file', "Delete file");
			} else {
1138 1139 1140 1141 1142 1143
				if (scmResources[0].type === Status.DELETED) {
					yes = localize('restore file', "Restore file");
					message = localize('confirm restore', "Are you sure you want to restore {0}?", path.basename(scmResources[0].resourceUri.fsPath));
				} else {
					message = localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(scmResources[0].resourceUri.fsPath));
				}
J
Joao Moreno 已提交
1144 1145
			}
		} else {
1146 1147 1148 1149 1150 1151
			if (scmResources.every(resource => resource.type === Status.DELETED)) {
				yes = localize('restore files', "Restore files");
				message = localize('confirm restore multiple', "Are you sure you want to restore {0} files?", scmResources.length);
			} else {
				message = localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", scmResources.length);
			}
J
Joao Moreno 已提交
1152 1153

			if (untrackedCount > 0) {
J
Joao Moreno 已提交
1154
				message = `${message}\n\n${localize('warn untracked', "This will DELETE {0} untracked files!\nThis is IRREVERSIBLE!\nThese files will be FOREVER LOST.", untrackedCount)}`;
J
Joao Moreno 已提交
1155 1156
			}
		}
1157

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

J
Joao Moreno 已提交
1160 1161 1162 1163
		if (pick !== yes) {
			return;
		}

J
Joao Moreno 已提交
1164
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
1165
		await this.runByRepository(resources, async (repository, resources) => repository.clean(resources));
J
Joao Moreno 已提交
1166
	}
J
Joao Moreno 已提交
1167

J
Joao Moreno 已提交
1168 1169
	@command('git.cleanAll', { repository: true })
	async cleanAll(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1170
		let resources = repository.workingTreeGroup.resourceStates;
J
Joao Moreno 已提交
1171 1172 1173 1174 1175

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

J
Joao Moreno 已提交
1176 1177
		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 已提交
1178

J
Joao Moreno 已提交
1179
		if (untrackedResources.length === 0) {
1180
			await this._cleanTrackedChanges(repository, resources);
1181 1182
		} else if (resources.length === 1) {
			await this._cleanUntrackedChange(repository, resources[0]);
J
Joao Moreno 已提交
1183
		} else if (trackedResources.length === 0) {
1184
			await this._cleanUntrackedChanges(repository, resources);
J
Joao Moreno 已提交
1185 1186 1187 1188
		} 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 已提交
1189

J
Joao Moreno 已提交
1190 1191 1192 1193 1194
			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 已提交
1195

J
Joao Moreno 已提交
1196
			const yesAll = localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
1197 1198 1199 1200 1201 1202 1203 1204
			const pick = await window.showWarningMessage(message, { modal: true }, yesTracked, yesAll);

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

J
Joao 已提交
1205
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
1206
		}
J
Joao Moreno 已提交
1207 1208
	}

1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220
	@command('git.cleanAllTracked', { repository: true })
	async cleanAllTracked(repository: Repository): Promise<void> {
		const resources = repository.workingTreeGroup.resourceStates
			.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);

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

		await this._cleanTrackedChanges(repository, resources);
	}

1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236
	@command('git.cleanAllUntracked', { repository: true })
	async cleanAllUntracked(repository: Repository): Promise<void> {
		const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates]
			.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);

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

		if (resources.length === 1) {
			await this._cleanUntrackedChange(repository, resources[0]);
		} else {
			await this._cleanUntrackedChanges(repository, resources);
		}
	}

1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252
	private async _cleanTrackedChanges(repository: Repository, resources: Resource[]): Promise<void> {
		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")
			: localize('discardAll', "Discard All {0} Files", resources.length);
		const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

		await repository.clean(resources.map(r => r.resourceUri));
	}

1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276
	private async _cleanUntrackedChange(repository: Repository, resource: Resource): Promise<void> {
		const message = localize('confirm delete', "Are you sure you want to DELETE {0}?\nThis is IRREVERSIBLE!\nThis file will be FOREVER LOST.", path.basename(resource.resourceUri.fsPath));
		const yes = localize('delete file', "Delete file");
		const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

		await repository.clean([resource.resourceUri]);
	}

	private async _cleanUntrackedChanges(repository: Repository, resources: Resource[]): Promise<void> {
		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);

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

		await repository.clean(resources.map(r => r.resourceUri));
	}

J
Joao Moreno 已提交
1277
	private async smartCommit(
J
Joao Moreno 已提交
1278
		repository: Repository,
1279
		getCommitMessage: () => Promise<string | undefined>,
J
Joao Moreno 已提交
1280 1281
		opts?: CommitOptions
	): Promise<boolean> {
1282
		const config = workspace.getConfiguration('git', Uri.file(repository.root));
1283
		let promptToSaveFilesBeforeCommit = config.get<'always' | 'staged' | 'never'>('promptToSaveFilesBeforeCommit');
J
Joao Moreno 已提交
1284

1285 1286 1287 1288 1289 1290 1291
		// migration
		if (promptToSaveFilesBeforeCommit as any === true) {
			promptToSaveFilesBeforeCommit = 'always';
		} else if (promptToSaveFilesBeforeCommit as any === false) {
			promptToSaveFilesBeforeCommit = 'never';
		}

J
Joao Moreno 已提交
1292 1293
		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;

1294 1295 1296 1297
		if (promptToSaveFilesBeforeCommit !== 'never') {
			let documents = workspace.textDocuments
				.filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath));

J
Joao Moreno 已提交
1298
			if (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) {
1299
				documents = documents
J
Joao Moreno 已提交
1300
					.filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath)));
1301
			}
1302

1303 1304
			if (documents.length > 0) {
				const message = documents.length === 1
J
Joao Moreno 已提交
1305
					? localize('unsaved files single', "The following file has unsaved changes which won't be included in the commit if you proceed: {0}.\n\nWould you like to save it before committing?", path.basename(documents[0].uri.fsPath))
J
Joao Moreno 已提交
1306
					: localize('unsaved files', "There are {0} unsaved files.\n\nWould you like to save them before committing?", documents.length);
J
Joao Moreno 已提交
1307 1308 1309 1310 1311
				const saveAndCommit = localize('save and commit', "Save All & Commit");
				const commit = localize('commit', "Commit Anyway");
				const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit);

				if (pick === saveAndCommit) {
1312
					await Promise.all(documents.map(d => d.save()));
D
Dipen Ved 已提交
1313
					await repository.add([]);
J
Joao Moreno 已提交
1314 1315 1316
				} else if (pick !== commit) {
					return false; // do not commit on cancel
				}
1317 1318 1319
			}
		}

1320
		const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
J
Joao Moreno 已提交
1321 1322
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
1323 1324

		// no changes, and the user has not configured to commit all in this case
1325
		if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit) {
1326
			const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
J
Joao Moreno 已提交
1327

1328 1329 1330 1331
			if (!suggestSmartCommit) {
				return false;
			}

J
Joao Moreno 已提交
1332 1333
			// 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?");
1334 1335
			const yes = localize('yes', "Yes");
			const always = localize('always', "Always");
1336 1337
			const never = localize('never', "Never");
			const pick = await window.showWarningMessage(message, { modal: true }, yes, always, never);
1338 1339

			if (pick === always) {
J
Joao Moreno 已提交
1340
				config.update('enableSmartCommit', true, true);
1341 1342 1343
			} else if (pick === never) {
				config.update('suggestSmartCommit', false, true);
				return false;
J
Joao Moreno 已提交
1344 1345
			} else if (pick !== yes) {
				return false; // do not commit on cancel
1346 1347 1348
			}
		}

J
Joao Moreno 已提交
1349
		if (!opts) {
1350
			opts = { all: noStagedChanges };
J
Joao Moreno 已提交
1351 1352
		} else if (!opts.all && noStagedChanges) {
			opts = { ...opts, all: true };
J
Joao Moreno 已提交
1353 1354
		}

1355 1356 1357
		// enable signing of commits if configurated
		opts.signCommit = enableCommitSigning;

1358 1359 1360 1361
		if (config.get<boolean>('alwaysSignOff')) {
			opts.signoff = true;
		}

J
Joao Moreno 已提交
1362
		if (
1363
			(
J
Joao Moreno 已提交
1364 1365 1366 1367 1368
				// no changes
				(noStagedChanges && noUnstagedChanges)
				// or no staged changes and not `all`
				|| (!opts.all && noStagedChanges)
			)
1369
			&& !opts.empty
J
Joao Moreno 已提交
1370
		) {
J
Joao Moreno 已提交
1371 1372 1373 1374
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
1375
		const message = await getCommitMessage();
J
Joao Moreno 已提交
1376 1377 1378 1379 1380

		if (!message) {
			return false;
		}

J
Joao Moreno 已提交
1381 1382
		if (opts.all && config.get<'all' | 'tracked'>('smartCommitChanges') === 'tracked') {
			opts.all = 'tracked';
1383 1384
		}

1385
		if (opts.all && config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges') !== 'mixed') {
1386
			opts.all = 'tracked';
J
Joao Moreno 已提交
1387 1388
		}

J
Joao Moreno 已提交
1389
		await repository.commit(message, opts);
J
Joao Moreno 已提交
1390

J
Joao Moreno 已提交
1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401
		const postCommitCommand = config.get<'none' | 'push' | 'sync'>('postCommitCommand');

		switch (postCommitCommand) {
			case 'push':
				await this._push(repository, { pushType: PushType.Push, silent: true });
				break;
			case 'sync':
				await this.sync(repository);
				break;
		}

J
Joao Moreno 已提交
1402 1403 1404
		return true;
	}

J
Joao Moreno 已提交
1405
	private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
J
Joao Moreno 已提交
1406
		const message = repository.inputBox.value;
J
Joao Moreno 已提交
1407
		const getCommitMessage = async () => {
1408
			let _message: string | undefined = message;
1409

1410 1411
			if (!_message) {
				let value: string | undefined = undefined;
J
Joao Moreno 已提交
1412

1413 1414 1415
				if (opts && opts.amend && repository.HEAD && repository.HEAD.commit) {
					value = (await repository.getCommit(repository.HEAD.commit)).message;
				}
1416

1417 1418
				const branchName = repository.headShortName;
				let placeHolder: string;
1419

1420 1421 1422 1423 1424
				if (branchName) {
					placeHolder = localize('commitMessageWithHeadLabel2', "Message (commit on '{0}')", branchName);
				} else {
					placeHolder = localize('commit message', "Commit message");
				}
J
Joao Moreno 已提交
1425

1426 1427
				_message = await window.showInputBox({
					value,
1428
					placeHolder,
1429 1430 1431
					prompt: localize('provide commit message', "Please provide a commit message"),
					ignoreFocusOut: true
				});
A
💄  
al 已提交
1432
			}
J
Joao Moreno 已提交
1433

1434
			return _message;
J
Joao Moreno 已提交
1435 1436
		};

J
Joao Moreno 已提交
1437
		const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
J
Joao Moreno 已提交
1438 1439

		if (message && didCommit) {
J
Joao Moreno 已提交
1440
			repository.inputBox.value = await repository.getInputTemplate();
J
Joao Moreno 已提交
1441
		}
J
Joao Moreno 已提交
1442 1443
	}

J
Joao Moreno 已提交
1444
	@command('git.commit', { repository: true })
J
Joao Moreno 已提交
1445 1446
	async commit(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository);
J
Joao Moreno 已提交
1447 1448
	}

J
Joao Moreno 已提交
1449
	@command('git.commitStaged', { repository: true })
J
Joao Moreno 已提交
1450 1451
	async commitStaged(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false });
J
Joao Moreno 已提交
1452 1453
	}

J
Joao Moreno 已提交
1454
	@command('git.commitStagedSigned', { repository: true })
J
Joao Moreno 已提交
1455 1456
	async commitStagedSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, signoff: true });
J
Joao Moreno 已提交
1457 1458
	}

J
Joao Moreno 已提交
1459
	@command('git.commitStagedAmend', { repository: true })
J
Joao Moreno 已提交
1460
	async commitStagedAmend(repository: Repository): Promise<void> {
1461
		await this.commitWithAnyInput(repository, { all: false, amend: true });
K
Krzysztof Cieślak 已提交
1462 1463
	}

J
Joao Moreno 已提交
1464
	@command('git.commitAll', { repository: true })
J
Joao Moreno 已提交
1465 1466
	async commitAll(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true });
J
Joao Moreno 已提交
1467 1468
	}

J
Joao Moreno 已提交
1469
	@command('git.commitAllSigned', { repository: true })
J
Joao Moreno 已提交
1470 1471
	async commitAllSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, signoff: true });
J
Joao Moreno 已提交
1472 1473
	}

J
Joao Moreno 已提交
1474
	@command('git.commitAllAmend', { repository: true })
J
Joao Moreno 已提交
1475 1476
	async commitAllAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, amend: true });
K
Krzysztof Cieślak 已提交
1477 1478
	}

J
Joao Moreno 已提交
1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500
	@command('git.commitEmpty', { repository: true })
	async commitEmpty(repository: Repository): Promise<void> {
		const root = Uri.file(repository.root);
		const config = workspace.getConfiguration('git', root);
		const shouldPrompt = config.get<boolean>('confirmEmptyCommits') === true;

		if (shouldPrompt) {
			const message = localize('confirm emtpy commit', "Are you sure you want to create an empty commit?");
			const yes = localize('yes', "Yes");
			const neverAgain = localize('yes never again', "Yes, Don't Show Again");
			const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);

			if (pick === neverAgain) {
				await config.update('confirmEmptyCommits', false, true);
			} else if (pick !== yes) {
				return;
			}
		}

		await this.commitWithAnyInput(repository, { empty: true });
	}

J
Joao Moreno 已提交
1501 1502 1503 1504 1505
	@command('git.restoreCommitTemplate', { repository: true })
	async restoreCommitTemplate(repository: Repository): Promise<void> {
		repository.inputBox.value = await repository.getCommitTemplate();
	}

J
Joao Moreno 已提交
1506
	@command('git.undoCommit', { repository: true })
J
Joao Moreno 已提交
1507 1508
	async undoCommit(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
1509 1510

		if (!HEAD || !HEAD.commit) {
J
Joao Moreno 已提交
1511
			window.showWarningMessage(localize('no more', "Can't undo because HEAD doesn't point to any commit."));
J
Joao Moreno 已提交
1512 1513 1514
			return;
		}

J
Joao Moreno 已提交
1515
		const commit = await repository.getCommit('HEAD');
J
Joao Moreno 已提交
1516

1517 1518
		if (commit.parents.length > 1) {
			const yes = localize('undo commit', "Undo merge commit");
J
Joao Moreno 已提交
1519
			const result = await window.showWarningMessage(localize('merge commit', "The last commit was a merge commit. Are you sure you want to undo it?"), { modal: true }, yes);
1520 1521 1522 1523 1524 1525

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

J
Joao Moreno 已提交
1526
		if (commit.parents.length > 0) {
1527 1528 1529 1530 1531
			await repository.reset('HEAD~');
		} else {
			await repository.deleteRef('HEAD');
			await this.unstageAll(repository);
		}
J
Joao Moreno 已提交
1532

J
Joao Moreno 已提交
1533
		repository.inputBox.value = commit.message;
J
Joao Moreno 已提交
1534 1535
	}

J
Joao Moreno 已提交
1536
	@command('git.checkout', { repository: true })
1537
	async checkout(repository: Repository, treeish: string): Promise<boolean> {
J
Joao Moreno 已提交
1538
		if (typeof treeish === 'string') {
1539 1540
			await repository.checkout(treeish);
			return true;
J
Joao Moreno 已提交
1541
		}
J
Joao Moreno 已提交
1542

J
Joao Moreno 已提交
1543 1544 1545
		const createBranch = new CreateBranchItem(this);
		const createBranchFrom = new CreateBranchFromItem(this);
		const picks = [createBranch, createBranchFrom, ...createCheckoutItems(repository)];
1546
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
1547 1548 1549 1550 1551 1552 1553 1554

		const quickpick = window.createQuickPick();
		quickpick.items = picks;
		quickpick.placeholder = placeHolder;
		quickpick.show();

		const choice = await new Promise<QuickPickItem | undefined>(c => quickpick.onDidAccept(() => c(quickpick.activeItems[0])));
		quickpick.hide();
J
Joao Moreno 已提交
1555 1556

		if (!choice) {
1557
			return false;
J
Joao Moreno 已提交
1558 1559
		}

1560 1561
		if (choice === createBranch) {
			await this._branch(repository, quickpick.value);
J
Joao Moreno 已提交
1562 1563
		} else if (choice === createBranchFrom) {
			await this._branch(repository, quickpick.value, true);
1564 1565 1566 1567
		} else {
			await (choice as CheckoutItem).run(repository);
		}

1568
		return true;
J
Joao Moreno 已提交
1569 1570
	}

J
Joao Moreno 已提交
1571
	@command('git.branch', { repository: true })
J
Joao Moreno 已提交
1572
	async branch(repository: Repository): Promise<void> {
1573 1574
		await this._branch(repository);
	}
J
Joao Moreno 已提交
1575

J
Joao Moreno 已提交
1576 1577 1578 1579 1580
	@command('git.branchFrom', { repository: true })
	async branchFrom(repository: Repository): Promise<void> {
		await this._branch(repository, undefined, true);
	}

J
Joao Moreno 已提交
1581
	private async promptForBranchName(defaultName?: string): Promise<string> {
1582 1583 1584 1585
		const config = workspace.getConfiguration('git');
		const branchWhitespaceChar = config.get<string>('branchWhitespaceChar')!;
		const branchValidationRegex = config.get<string>('branchValidationRegex')!;
		const sanitize = (name: string) => name ?
J
Joao Moreno 已提交
1586
			name.trim().replace(/^-+/, '').replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, branchWhitespaceChar)
1587 1588
			: name;

J
Joao Moreno 已提交
1589
		const rawBranchName = defaultName || await window.showInputBox({
1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602
			placeHolder: localize('branch name', "Branch name"),
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true,
			validateInput: (name: string) => {
				const validateName = new RegExp(branchValidationRegex);
				if (validateName.test(sanitize(name))) {
					return null;
				}

				return localize('branch name format invalid', "Branch name needs to match regex: {0}", branchValidationRegex);
			}
		});

J
Joao Moreno 已提交
1603 1604 1605 1606 1607
		return sanitize(rawBranchName || '');
	}

	private async _branch(repository: Repository, defaultName?: string, from = false): Promise<void> {
		const branchName = await this.promptForBranchName(defaultName);
1608 1609

		if (!branchName) {
1610 1611
			return;
		}
J
Joao Moreno 已提交
1612

J
Joao Moreno 已提交
1613
		let target = 'HEAD';
J
Joao Moreno 已提交
1614

J
Joao Moreno 已提交
1615 1616 1617 1618 1619 1620 1621 1622 1623 1624
		if (from) {
			const picks = [new HEADItem(repository), ...createCheckoutItems(repository)];
			const placeHolder = localize('select a ref to create a new branch from', 'Select a ref to create the \'{0}\' branch from', branchName);
			const choice = await window.showQuickPick(picks, { placeHolder });

			if (!choice) {
				return;
			}

			target = choice.label;
J
Joao Moreno 已提交
1625
		}
J
Joao Moreno 已提交
1626

J
Joao Moreno 已提交
1627
		await repository.branch(branchName, true, target);
J
Joao Moreno 已提交
1628 1629
	}

J
Joao Moreno 已提交
1630
	@command('git.deleteBranch', { repository: true })
J
Joao Moreno 已提交
1631
	async deleteBranch(repository: Repository, name: string, force?: boolean): Promise<void> {
1632 1633
		let run: (force?: boolean) => Promise<void>;
		if (typeof name === 'string') {
J
Joao Moreno 已提交
1634
			run = force => repository.deleteBranch(name, force);
1635
		} else {
J
Joao Moreno 已提交
1636 1637
			const currentHead = repository.HEAD && repository.HEAD.name;
			const heads = repository.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead)
1638
				.map(ref => new BranchDeleteItem(ref));
M
Maik Riechert 已提交
1639

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

M
Maik Riechert 已提交
1643
			if (!choice || !choice.branchName) {
1644 1645
				return;
			}
M
Maik Riechert 已提交
1646
			name = choice.branchName;
J
Joao Moreno 已提交
1647
			run = force => choice.run(repository, force);
M
Maik Riechert 已提交
1648 1649
		}

1650 1651 1652 1653 1654 1655 1656 1657 1658
		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");
1659
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
1660 1661 1662 1663 1664

			if (pick === yes) {
				await run(true);
			}
		}
M
Maik Riechert 已提交
1665 1666
	}

1667 1668
	@command('git.renameBranch', { repository: true })
	async renameBranch(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1669
		const branchName = await this.promptForBranchName();
J
Justin Horner 已提交
1670

J
Joao Moreno 已提交
1671
		if (!branchName) {
J
Justin Horner 已提交
1672 1673 1674 1675
			return;
		}

		try {
J
Joao Moreno 已提交
1676
			await repository.renameBranch(branchName);
J
Justin Horner 已提交
1677 1678 1679
		} catch (err) {
			switch (err.gitErrorCode) {
				case GitErrorCodes.InvalidBranchName:
1680 1681
					window.showErrorMessage(localize('invalid branch name', 'Invalid branch name'));
					return;
J
Justin Horner 已提交
1682
				case GitErrorCodes.BranchAlreadyExists:
J
Joao Moreno 已提交
1683
					window.showErrorMessage(localize('branch already exists', "A branch named '{0}' already exists", branchName));
1684 1685 1686
					return;
				default:
					throw err;
J
Justin Horner 已提交
1687 1688 1689 1690
			}
		}
	}

J
Joao Moreno 已提交
1691
	@command('git.merge', { repository: true })
J
Joao Moreno 已提交
1692
	async merge(repository: Repository): Promise<void> {
1693 1694 1695 1696
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string>('checkoutType') || 'all';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1697
		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
1698 1699
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1700

J
Joao Moreno 已提交
1701
		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
1702 1703
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1704 1705

		const picks = [...heads, ...remoteHeads];
1706 1707
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
1708 1709 1710 1711 1712

		if (!choice) {
			return;
		}

1713
		await choice.run(repository);
1714 1715
	}

J
Joao Moreno 已提交
1716
	@command('git.createTag', { repository: true })
J
Joao Moreno 已提交
1717
	async createTag(repository: Repository): Promise<void> {
1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729
		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 已提交
1730
			prompt: localize('provide tag message', "Please provide a message to annotate the tag"),
1731 1732 1733 1734 1735
			ignoreFocusOut: true
		});

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

X
Xhulio Hasani 已提交
1739 1740
	@command('git.deleteTag', { repository: true })
	async deleteTag(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1741 1742 1743 1744
		const picks = repository.refs.filter(ref => ref.type === RefType.Tag)
			.map(ref => new TagItem(ref));

		if (picks.length === 0) {
1745
			window.showWarningMessage(localize('no tags', "This repository has no tags."));
J
Joao Moreno 已提交
1746
			return;
1747
		}
J
Joao Moreno 已提交
1748

1749
		const placeHolder = localize('select a tag to delete', 'Select a tag to delete');
J
Joao Moreno 已提交
1750 1751
		const choice = await window.showQuickPick(picks, { placeHolder });

1752
		if (!choice) {
X
Xhulio Hasani 已提交
1753 1754
			return;
		}
J
Joao Moreno 已提交
1755 1756

		await repository.deleteTag(choice.label);
X
Xhulio Hasani 已提交
1757 1758
	}

J
Joao Moreno 已提交
1759 1760 1761 1762 1763 1764 1765
	@command('git.fetch', { repository: true })
	async fetch(repository: Repository): Promise<void> {
		if (repository.remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
			return;
		}

J
Joao Moreno 已提交
1766
		await repository.fetchDefault();
J
Joao Moreno 已提交
1767 1768
	}

R
Ryan Scott 已提交
1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779
	@command('git.fetchPrune', { repository: true })
	async fetchPrune(repository: Repository): Promise<void> {
		if (repository.remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
			return;
		}

		await repository.fetchPrune();
	}


J
Joao Moreno 已提交
1780 1781 1782 1783 1784 1785 1786 1787 1788 1789
	@command('git.fetchAll', { repository: true })
	async fetchAll(repository: Repository): Promise<void> {
		if (repository.remotes.length === 0) {
			window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
			return;
		}

		await repository.fetchAll();
	}

J
Joao Moreno 已提交
1790
	@command('git.pullFrom', { repository: true })
J
Joao Moreno 已提交
1791 1792
	async pullFrom(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1793 1794 1795 1796 1797 1798

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

1799
		const remotePicks = remotes.filter(r => r.fetchUrl !== undefined).map(r => ({ label: r.name, description: r.fetchUrl! }));
1800
		const placeHolder = localize('pick remote pull repo', "Pick a remote to pull the branch from");
D
Dozed12 已提交
1801
		const remotePick = await window.showQuickPick(remotePicks, { placeHolder });
1802

D
Dozed12 已提交
1803
		if (!remotePick) {
1804 1805 1806
			return;
		}

D
Dozed12 已提交
1807 1808
		const remoteRefs = repository.refs;
		const remoteRefsFiltered = remoteRefs.filter(r => (r.remote === remotePick.label));
J
Joao Moreno 已提交
1809
		const branchPicks = remoteRefsFiltered.map(r => ({ label: r.name! }));
J
Joao Moreno 已提交
1810 1811
		const branchPlaceHolder = localize('pick branch pull', "Pick a branch to pull from");
		const branchPick = await window.showQuickPick(branchPicks, { placeHolder: branchPlaceHolder });
1812

D
Dozed12 已提交
1813
		if (!branchPick) {
1814 1815 1816
			return;
		}

F
Francisco Moreira 已提交
1817 1818
		const remoteCharCnt = remotePick.label.length;

1819
		await repository.pullFrom(false, remotePick.label, branchPick.label.slice(remoteCharCnt + 1));
1820 1821
	}

J
Joao Moreno 已提交
1822
	@command('git.pull', { repository: true })
J
Joao Moreno 已提交
1823 1824
	async pull(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1825 1826 1827 1828 1829 1830

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

J
Joao Moreno 已提交
1831
		await repository.pull(repository.HEAD);
J
Joao Moreno 已提交
1832 1833
	}

J
Joao Moreno 已提交
1834
	@command('git.pullRebase', { repository: true })
J
Joao Moreno 已提交
1835 1836
	async pullRebase(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1837 1838 1839 1840 1841 1842

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

J
Joao Moreno 已提交
1843
		await repository.pullWithRebase(repository.HEAD);
J
Joao Moreno 已提交
1844 1845
	}

J
Joao Moreno 已提交
1846
	private async _push(repository: Repository, pushOptions: PushOptions) {
J
Joao Moreno 已提交
1847
		const remotes = repository.remotes;
1848

J
Joao Moreno 已提交
1849
		if (remotes.length === 0) {
J
Joao Moreno 已提交
1850 1851 1852
			if (!pushOptions.silent) {
				window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
			}
J
Joao Moreno 已提交
1853 1854
			return;
		}
1855

J
Joao Moreno 已提交
1856 1857 1858 1859 1860 1861
		const config = workspace.getConfiguration('git', Uri.file(repository.root));
		let forcePushMode: ForcePushMode | undefined = undefined;

		if (pushOptions.forcePush) {
			if (!config.get<boolean>('allowForcePush')) {
				await window.showErrorMessage(localize('force push not allowed', "Force push is not allowed, please enable it with the 'git.allowForcePush' setting."));
1862 1863
				return;
			}
J
Joao Moreno 已提交
1864

J
Joao Moreno 已提交
1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878
			forcePushMode = config.get<boolean>('useForcePushWithLease') === true ? ForcePushMode.ForceWithLease : ForcePushMode.Force;

			if (config.get<boolean>('confirmForcePush')) {
				const message = localize('confirm force push', "You are about to force push your changes, this can be destructive and could inadvertedly overwrite changes made by others.\n\nAre you sure to continue?");
				const yes = localize('ok', "OK");
				const neverAgain = localize('never ask again', "OK, Don't Ask Again");
				const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);

				if (pick === neverAgain) {
					config.update('confirmForcePush', false, true);
				} else if (pick !== yes) {
					return;
				}
			}
J
Joao Moreno 已提交
1879 1880
		}

1881 1882
		if (pushOptions.pushType === PushType.PushFollowTags) {
			await repository.pushFollowTags(undefined, forcePushMode);
1883 1884 1885
			return;
		}

1886
		if (!repository.HEAD || !repository.HEAD.name) {
J
Joao Moreno 已提交
1887 1888 1889
			if (!pushOptions.silent) {
				window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			}
1890 1891 1892
			return;
		}

1893 1894 1895 1896 1897 1898 1899
		if (pushOptions.pushType === PushType.Push) {
			try {
				await repository.push(repository.HEAD, forcePushMode);
			} catch (err) {
				if (err.gitErrorCode !== GitErrorCodes.NoUpstreamBranch) {
					throw err;
				}
1900

J
Joao Moreno 已提交
1901 1902 1903 1904
				if (pushOptions.silent) {
					return;
				}

1905 1906 1907 1908 1909 1910 1911 1912 1913 1914
				const branchName = repository.HEAD.name;
				const message = localize('confirm publish branch', "The branch '{0}' has no upstream branch. Would you like to publish this branch?", branchName);
				const yes = localize('ok', "OK");
				const pick = await window.showWarningMessage(message, { modal: true }, yes);

				if (pick === yes) {
					await this.publish(repository);
				}
			}
		} else {
1915
			const branchName = repository.HEAD.name;
J
Joao Moreno 已提交
1916 1917
			const addRemote = new AddRemoteItem(this);
			const picks = [...remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl })), addRemote];
1918
			const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
J
Joao Moreno 已提交
1919
			const choice = await window.showQuickPick(picks, { placeHolder });
1920

J
Joao Moreno 已提交
1921
			if (!choice) {
1922
				return;
1923
			}
1924

J
Joao Moreno 已提交
1925 1926 1927 1928 1929 1930 1931 1932 1933
			if (choice === addRemote) {
				const newRemote = await this.addRemote(repository);

				if (newRemote) {
					await repository.pushTo(newRemote, branchName, undefined, forcePushMode);
				}
			} else {
				await repository.pushTo(choice.label, branchName, undefined, forcePushMode);
			}
1934
		}
J
Joao Moreno 已提交
1935 1936
	}

1937 1938
	@command('git.push', { repository: true })
	async push(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1939
		await this._push(repository, { pushType: PushType.Push });
1940
	}
1941

1942 1943
	@command('git.pushForce', { repository: true })
	async pushForce(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1944
		await this._push(repository, { pushType: PushType.Push, forcePush: true });
1945
	}
1946

1947
	@command('git.pushWithTags', { repository: true })
1948 1949
	async pushFollowTags(repository: Repository): Promise<void> {
		await this._push(repository, { pushType: PushType.PushFollowTags });
1950
	}
1951

1952
	@command('git.pushWithTagsForce', { repository: true })
1953 1954
	async pushFollowTagsForce(repository: Repository): Promise<void> {
		await this._push(repository, { pushType: PushType.PushFollowTags, forcePush: true });
1955 1956
	}

J
Joao Moreno 已提交
1957
	@command('git.pushTo', { repository: true })
J
Joao Moreno 已提交
1958
	async pushTo(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1959
		await this._push(repository, { pushType: PushType.PushTo });
1960
	}
J
Joao Moreno 已提交
1961

1962 1963
	@command('git.pushToForce', { repository: true })
	async pushToForce(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1964
		await this._push(repository, { pushType: PushType.PushTo, forcePush: true });
J
Joao Moreno 已提交
1965 1966
	}

O
Omkar Manjrekar 已提交
1967
	@command('git.addRemote', { repository: true })
1968
	async addRemote(repository: Repository): Promise<string | undefined> {
1969 1970
		const remotes = repository.remotes;

O
Omkar Manjrekar 已提交
1971 1972
		const sanitize = (name: string) => {
			name = name.trim();
J
Joao Moreno 已提交
1973
			return name && name.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, '-');
O
Omkar Manjrekar 已提交
1974
		};
O
Omkar Manjrekar 已提交
1975

O
Omkar Manjrekar 已提交
1976
		const resultName = await window.showInputBox({
O
Omkar Manjrekar 已提交
1977 1978 1979 1980
			placeHolder: localize('remote name', "Remote name"),
			prompt: localize('provide remote name', "Please provide a remote name"),
			ignoreFocusOut: true,
			validateInput: (name: string) => {
O
Omkar Manjrekar 已提交
1981
				if (sanitize(name)) {
O
Omkar Manjrekar 已提交
1982 1983
					return null;
				}
O
Omkar Manjrekar 已提交
1984
				return localize('remote name format invalid', "Remote name format invalid");
O
Omkar Manjrekar 已提交
1985
			}
O
Omkar Manjrekar 已提交
1986
		});
O
Omkar Manjrekar 已提交
1987

O
Omkar Manjrekar 已提交
1988
		const name = sanitize(resultName || '');
O
Omkar Manjrekar 已提交
1989

O
Omkar Manjrekar 已提交
1990 1991 1992 1993 1994
		if (!name) {
			return;
		}

		if (remotes.find(r => r.name === name)) {
J
Joao Moreno 已提交
1995
			window.showErrorMessage(localize('remote already exists', "Remote '{0}' already exists.", name));
O
Omkar Manjrekar 已提交
1996
			return;
O
Omkar Manjrekar 已提交
1997
		}
O
Omkar Manjrekar 已提交
1998

J
Joao Moreno 已提交
1999
		const url = await window.showInputBox({
O
Omkar Manjrekar 已提交
2000
			placeHolder: localize('remote url', "Remote URL"),
O
Omkar Manjrekar 已提交
2001
			prompt: localize('provide remote URL', "Enter URL for remote \"{0}\"", name),
2002
			ignoreFocusOut: true
O
Omkar Manjrekar 已提交
2003 2004 2005 2006 2007 2008 2009
		});

		if (!url) {
			return;
		}

		await repository.addRemote(name, url);
2010 2011

		return name;
O
Omkar Manjrekar 已提交
2012 2013
	}

O
Omkar Manjrekar 已提交
2014 2015
	@command('git.removeRemote', { repository: true })
	async removeRemote(repository: Repository): Promise<void> {
O
Omkar Manjrekar 已提交
2016 2017 2018
		const remotes = repository.remotes;

		if (remotes.length === 0) {
2019
			window.showErrorMessage(localize('no remotes added', "Your repository has no remotes."));
O
Omkar Manjrekar 已提交
2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030
			return;
		}

		const picks = remotes.map(r => r.name);
		const placeHolder = localize('remove remote', "Pick a remote to remove");

		const remoteName = await window.showQuickPick(picks, { placeHolder });

		if (!remoteName) {
			return;
		}
O
Omkar Manjrekar 已提交
2031

O
Omkar Manjrekar 已提交
2032
		await repository.removeRemote(remoteName);
O
Omkar Manjrekar 已提交
2033
	}
O
Omkar Manjrekar 已提交
2034

J
Joao Moreno 已提交
2035
	private async _sync(repository: Repository, rebase: boolean): Promise<void> {
J
Joao Moreno 已提交
2036
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
2037

2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048
		if (!HEAD) {
			return;
		} else if (!HEAD.upstream) {
			const branchName = HEAD.name;
			const message = localize('confirm publish branch', "The branch '{0}' has no upstream branch. Would you like to publish this branch?", branchName);
			const yes = localize('ok', "OK");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

			if (pick === yes) {
				await this.publish(repository);
			}
J
Joao Moreno 已提交
2049 2050 2051
			return;
		}

J
Joao Moreno 已提交
2052 2053 2054 2055
		const remoteName = HEAD.remote || HEAD.upstream.remote;
		const remote = repository.remotes.find(r => r.name === remoteName);
		const isReadonly = remote && remote.isReadOnly;

J
Joao Moreno 已提交
2056
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
2057
		const shouldPrompt = !isReadonly && config.get<boolean>('confirmSync') === true;
J
Joao Moreno 已提交
2058 2059

		if (shouldPrompt) {
J
Joao Moreno 已提交
2060
			const message = localize('sync is unpredictable', "This action will push and pull commits to and from '{0}/{1}'.", HEAD.upstream.remote, HEAD.upstream.name);
J
Joao Moreno 已提交
2061
			const yes = localize('ok', "OK");
B
Benjamin Pasero 已提交
2062
			const neverAgain = localize('never again', "OK, Don't Show Again");
J
Joao Moreno 已提交
2063 2064 2065 2066 2067 2068 2069 2070 2071
			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 已提交
2072
		if (rebase) {
J
Joao Moreno 已提交
2073
			await repository.syncRebase(HEAD);
J
Joao Moreno 已提交
2074
		} else {
J
Joao Moreno 已提交
2075
			await repository.sync(HEAD);
J
Joao Moreno 已提交
2076 2077 2078 2079
		}
	}

	@command('git.sync', { repository: true })
2080 2081 2082 2083 2084 2085 2086 2087 2088 2089
	async sync(repository: Repository): Promise<void> {
		try {
			await this._sync(repository, false);
		} catch (err) {
			if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
				return;
			}

			throw err;
		}
J
Joao Moreno 已提交
2090 2091
	}

J
Joao 已提交
2092 2093 2094 2095 2096 2097 2098 2099 2100
	@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;
			}

J
Joao Moreno 已提交
2101
			await repository.sync(HEAD);
J
Joao 已提交
2102 2103 2104
		}));
	}

2105
	@command('git.syncRebase', { repository: true })
2106 2107 2108 2109 2110 2111 2112 2113 2114 2115
	async syncRebase(repository: Repository): Promise<void> {
		try {
			await this._sync(repository, true);
		} catch (err) {
			if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
				return;
			}

			throw err;
		}
J
Joao Moreno 已提交
2116 2117
	}

J
Joao Moreno 已提交
2118
	@command('git.publish', { repository: true })
J
Joao Moreno 已提交
2119 2120
	async publish(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
2121 2122 2123 2124 2125 2126

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

2127 2128 2129 2130 2131 2132
		const branchName = repository.HEAD && repository.HEAD.name || '';

		if (remotes.length === 1) {
			return await repository.pushTo(remotes[0].name, branchName, true);
		}

2133
		const addRemote = new AddRemoteItem(this);
J
Joao Moreno 已提交
2134
		const picks = [...repository.remotes.map(r => ({ label: r.name, description: r.pushUrl })), addRemote];
2135 2136
		const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
		const choice = await window.showQuickPick(picks, { placeHolder });
J
Joao Moreno 已提交
2137 2138 2139 2140 2141

		if (!choice) {
			return;
		}

2142 2143 2144 2145
		if (choice === addRemote) {
			const newRemote = await this.addRemote(repository);

			if (newRemote) {
J
Joao Moreno 已提交
2146
				await repository.pushTo(newRemote, branchName, true);
2147
			}
J
Joao Moreno 已提交
2148 2149
		} else {
			await repository.pushTo(choice.label, branchName, true);
2150
		}
J
Joao Moreno 已提交
2151 2152
	}

2153 2154
	@command('git.ignore')
	async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
2155 2156
		resourceStates = resourceStates.filter(s => !!s);

J
Joao Moreno 已提交
2157
		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
2158
			const resource = this.getSCMResource();
N
NKumar2 已提交
2159

2160
			if (!resource) {
J
Joao Moreno 已提交
2161 2162 2163
				return;
			}

2164
			resourceStates = [resource];
J
Joao Moreno 已提交
2165 2166
		}

2167
		const resources = resourceStates
J
Joao Moreno 已提交
2168 2169 2170
			.filter(s => s instanceof Resource)
			.map(r => r.resourceUri);

2171
		if (!resources.length) {
N
NKumar2 已提交
2172 2173 2174
			return;
		}

2175
		await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
N
NKumar2 已提交
2176 2177
	}

J
Joao Moreno 已提交
2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190
	@command('git.revealInExplorer')
	async revealInExplorer(resourceState: SourceControlResourceState): Promise<void> {
		if (!resourceState) {
			return;
		}

		if (!(resourceState.resourceUri instanceof Uri)) {
			return;
		}

		await commands.executeCommand('revealInExplorer', resourceState.resourceUri);
	}

J
Joao Moreno 已提交
2191
	private async _stash(repository: Repository, includeUntracked = false): Promise<void> {
2192 2193
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0
			&& (!includeUntracked || repository.untrackedGroup.resourceStates.length === 0);
2194
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
2195

2196
		if (noUnstagedChanges && noStagedChanges) {
K
Krzysztof Cieślak 已提交
2197 2198 2199
			window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
			return;
		}
J
Joao Moreno 已提交
2200

2201
		const message = await this.getStashMessage();
J
Joao Moreno 已提交
2202 2203 2204 2205 2206

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

J
Joao Moreno 已提交
2207
		await repository.createStash(message, includeUntracked);
K
Krzysztof Cieślak 已提交
2208 2209
	}

2210 2211 2212 2213 2214 2215 2216
	private async getStashMessage(): Promise<string | undefined> {
		return await window.showInputBox({
			prompt: localize('provide stash message', "Optionally provide a stash message"),
			placeHolder: localize('stash message', "Stash message")
		});
	}

J
Joao Moreno 已提交
2217 2218 2219 2220 2221 2222 2223 2224 2225 2226
	@command('git.stash', { repository: true })
	stash(repository: Repository): Promise<void> {
		return this._stash(repository);
	}

	@command('git.stashIncludeUntracked', { repository: true })
	stashIncludeUntracked(repository: Repository): Promise<void> {
		return this._stash(repository, true);
	}

J
Joao Moreno 已提交
2227
	@command('git.stashPop', { repository: true })
J
Joao Moreno 已提交
2228
	async stashPop(repository: Repository): Promise<void> {
2229
		const placeHolder = localize('pick stash to pop', "Pick a stash to pop");
J
Joao Moreno 已提交
2230
		const stash = await this.pickStash(repository, placeHolder);
2231

J
Joao Moreno 已提交
2232
		if (!stash) {
2233 2234 2235
			return;
		}

J
Joao Moreno 已提交
2236
		await repository.popStash(stash.index);
2237 2238 2239 2240
	}

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

J
Joao Moreno 已提交
2243 2244
		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
K
Krzysztof Cieślak 已提交
2245 2246 2247
			return;
		}

2248 2249 2250 2251 2252 2253
		await repository.popStash();
	}

	@command('git.stashApply', { repository: true })
	async stashApply(repository: Repository): Promise<void> {
		const placeHolder = localize('pick stash to apply', "Pick a stash to apply");
J
Joao Moreno 已提交
2254
		const stash = await this.pickStash(repository, placeHolder);
K
Krzysztof Cieślak 已提交
2255

J
Joao Moreno 已提交
2256
		if (!stash) {
K
Krzysztof Cieślak 已提交
2257 2258
			return;
		}
J
Joao Moreno 已提交
2259

J
Joao Moreno 已提交
2260
		await repository.applyStash(stash.index);
K
Krzysztof Cieślak 已提交
2261 2262
	}

2263 2264
	@command('git.stashApplyLatest', { repository: true })
	async stashApplyLatest(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
2265
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
2266

J
Joao Moreno 已提交
2267 2268
		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
2269 2270 2271 2272 2273 2274
			return;
		}

		await repository.applyStash();
	}

2275 2276
	@command('git.stashDrop', { repository: true })
	async stashDrop(repository: Repository): Promise<void> {
2277 2278 2279 2280 2281 2282 2283 2284 2285 2286
		const placeHolder = localize('pick stash to drop', "Pick a stash to drop");
		const stash = await this.pickStash(repository, placeHolder);

		if (!stash) {
			return;
		}

		await repository.dropStash(stash.index);
	}

J
Joao Moreno 已提交
2287
	private async pickStash(repository: Repository, placeHolder: string): Promise<Stash | undefined> {
2288 2289
		const stashes = await repository.getStashes();

J
Joao Moreno 已提交
2290 2291
		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
K
Krzysztof Cieślak 已提交
2292 2293 2294
			return;
		}

J
Joao Moreno 已提交
2295 2296 2297
		const picks = stashes.map(stash => ({ label: `#${stash.index}:  ${stash.description}`, description: '', details: '', stash }));
		const result = await window.showQuickPick(picks, { placeHolder });
		return result && result.stash;
J
Joao Moreno 已提交
2298
	}
K
Krzysztof Cieślak 已提交
2299

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

J
Joao Moreno 已提交
2304
			if (!options.repository) {
J
Joao Moreno 已提交
2305 2306
				result = Promise.resolve(method.apply(this, args));
			} else {
J
Joao Moreno 已提交
2307 2308
				// try to guess the repository based on the first argument
				const repository = this.model.getRepository(args[0]);
J
Joao Moreno 已提交
2309 2310 2311 2312 2313 2314 2315 2316 2317
				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();
				}
2318

J
Joao Moreno 已提交
2319
				result = repositoryPromise.then(repository => {
J
Joao Moreno 已提交
2320
					if (!repository) {
J
Joao 已提交
2321
						return Promise.resolve();
J
Joao Moreno 已提交
2322 2323
					}

J
Joao Moreno 已提交
2324
					return Promise.resolve(method.apply(this, [repository, ...args]));
J
Joao Moreno 已提交
2325
				});
J
Joao Moreno 已提交
2326 2327
			}

K
kieferrm 已提交
2328
			/* __GDPR__
K
kieferrm 已提交
2329 2330 2331 2332
				"git.command" : {
					"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
J
Joao Moreno 已提交
2333 2334
			this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

J
Joao Moreno 已提交
2335
			return result.catch(async err => {
J
Joao Moreno 已提交
2336
				const options: MessageOptions = {
2337
					modal: true
J
Joao Moreno 已提交
2338 2339
				};

J
Joao Moreno 已提交
2340
				let message: string;
2341
				let type: 'error' | 'warning' = 'error';
J
Joao Moreno 已提交
2342

J
Joao Moreno 已提交
2343 2344 2345 2346 2347
				const choices = new Map<string, () => void>();
				const openOutputChannelChoice = localize('open git log', "Open Git Log");
				const outputChannel = this.outputChannel as OutputChannel;
				choices.set(openOutputChannelChoice, () => outputChannel.show());

J
Joao Moreno 已提交
2348
				switch (err.gitErrorCode) {
2349
					case GitErrorCodes.DirtyWorkTree:
J
Joao Moreno 已提交
2350 2351
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
2352
					case GitErrorCodes.PushRejected:
J
Joao Moreno 已提交
2353
						message = localize('cant push', "Can't push refs to remote. Try running 'Pull' first to integrate your changes.");
2354
						break;
2355 2356 2357 2358 2359
					case GitErrorCodes.Conflict:
						message = localize('merge conflicts', "There are merge conflicts. Resolve them before committing.");
						type = 'warning';
						options.modal = false;
						break;
J
Joao Moreno 已提交
2360 2361 2362 2363 2364
					case GitErrorCodes.StashConflict:
						message = localize('stash merge conflicts', "There were merge conflicts while applying the stash.");
						type = 'warning';
						options.modal = false;
						break;
2365 2366
					case GitErrorCodes.NoUserNameConfigured:
					case GitErrorCodes.NoUserEmailConfigured:
J
Joao Moreno 已提交
2367 2368
						message = localize('missing user info', "Make sure you configure your 'user.name' and 'user.email' in git.");
						choices.set(localize('learn more', "Learn More"), () => commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup')));
2369
						break;
J
Joao Moreno 已提交
2370
					default:
2371 2372 2373
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
J
Joao Moreno 已提交
2374
							.split(/[\r\n]/)
J
João Moreno 已提交
2375
							.filter((line: string) => !!line)
2376 2377 2378 2379 2380
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
2381 2382 2383 2384 2385 2386 2387 2388 2389

						break;
				}

				if (!message) {
					console.error(err);
					return;
				}

J
Joao Moreno 已提交
2390 2391 2392 2393 2394 2395 2396
				const allChoices = Array.from(choices.keys());
				const result = type === 'error'
					? await window.showErrorMessage(message, options, ...allChoices)
					: await window.showWarningMessage(message, options, ...allChoices);

				if (result) {
					const resultFn = choices.get(result);
J
Joao Moreno 已提交
2397

J
Joao Moreno 已提交
2398 2399 2400
					if (resultFn) {
						resultFn();
					}
J
Joao Moreno 已提交
2401 2402 2403
				}
			});
		};
2404 2405

		// patch this object, so people can call methods directly
2406
		(this as any)[key] = result;
2407 2408

		return result;
J
Joao Moreno 已提交
2409 2410
	}

2411
	private getSCMResource(uri?: Uri): Resource | undefined {
J
Joao Moreno 已提交
2412
		uri = uri ? uri : (window.activeTextEditor && window.activeTextEditor.document.uri);
J
Joao Moreno 已提交
2413

J
Joao Moreno 已提交
2414
		this.outputChannel.appendLine(`git.getSCMResource.uri ${uri && uri.toString()}`);
J
Joao Moreno 已提交
2415

J
Joao Moreno 已提交
2416 2417 2418
		for (const r of this.model.repositories.map(r => r.root)) {
			this.outputChannel.appendLine(`repo root ${r}`);
		}
J
Joao Moreno 已提交
2419

J
Joao Moreno 已提交
2420
		if (!uri) {
2421
			return undefined;
J
Joao Moreno 已提交
2422 2423
		}

J
Joao Moreno 已提交
2424
		if (isGitUri(uri)) {
J
Joao Moreno 已提交
2425 2426
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
2427 2428 2429 2430
		}

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

J
Joao Moreno 已提交
2433
			if (!repository) {
2434 2435
				return undefined;
			}
J
Joao Moreno 已提交
2436

J
Joao Moreno 已提交
2437 2438
			return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
				|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
J
Joao Moreno 已提交
2439
		}
2440
		return undefined;
J
Joao Moreno 已提交
2441 2442
	}

J
Joao Moreno 已提交
2443
	private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;
J
Joao Moreno 已提交
2444 2445 2446 2447 2448 2449
	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) => {
2450
			let repository = this.model.getRepository(resource);
J
Joao Moreno 已提交
2451 2452 2453 2454 2455 2456

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

2457
			// Could it be a submodule?
2458
			if (pathEquals(resource.fsPath, repository.root)) {
2459 2460 2461
				repository = this.model.getRepositoryForSubmodule(resource) || repository;
			}

J
Joao Moreno 已提交
2462
			const tuple = result.filter(p => p.repository === repository)[0];
J
Joao Moreno 已提交
2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478

			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 已提交
2479 2480 2481
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
João Moreno 已提交
2482
}