commands.ts 88.9 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
import * as path from 'path';
J
João Moreno 已提交
9
import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, QuickPick } from 'vscode';
J
Joao Moreno 已提交
10
import TelemetryReporter from 'vscode-extension-telemetry';
J
Joao Moreno 已提交
11
import * as nls from 'vscode-nls';
12
import { Branch, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourceProvider, RemoteSource } from './api/git';
13
import { ForcePushMode, Git, Stash } from './git';
14
import { Model } from './model';
15 16
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
import { Log, LogLevel } from './log';
20
import { GitTimelineItem } from './timelineProvider';
J
João Moreno 已提交
21
import { throttle, debounce } from './decorators';
J
João Moreno 已提交
22
import { ApiRepository } from './api/api1';
J
Joao Moreno 已提交
23 24

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

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

J
Joao Moreno 已提交
34
	async run(repository: Repository): Promise<void> {
35
		const ref = this.ref.name;
J
Joao Moreno 已提交
36 37 38 39 40

		if (!ref) {
			return;
		}

J
Joao Moreno 已提交
41
		await repository.checkout(ref);
J
Joao Moreno 已提交
42 43 44 45 46
	}
}

class CheckoutTagItem extends CheckoutItem {

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

class CheckoutRemoteHeadItem extends CheckoutItem {

J
Joao Moreno 已提交
54 55 56
	get description(): string {
		return localize('remote branch at', "Remote branch at {0}", this.shortCommit);
	}
J
Joao Moreno 已提交
57

58
	async run(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
59
		if (!this.ref.name) {
J
Joao Moreno 已提交
60 61 62
			return;
		}

J
Joao Moreno 已提交
63 64 65 66
		const branches = await repository.findTrackingBranches(this.ref.name);

		if (branches.length > 0) {
			await repository.checkout(branches[0].name!);
67
		} else {
J
Joao Moreno 已提交
68
			await repository.checkoutTracking(this.ref.name);
69
		}
J
Joao Moreno 已提交
70 71 72
	}
}

M
Maik Riechert 已提交
73 74
class BranchDeleteItem implements QuickPickItem {

75 76 77
	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 已提交
78 79
	get description(): string { return this.shortCommit; }

80
	constructor(private ref: Ref) { }
M
Maik Riechert 已提交
81

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

90 91 92 93 94 95
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 已提交
96

J
Joao Moreno 已提交
97 98
	async run(repository: Repository): Promise<void> {
		await repository.merge(this.ref.name! || this.ref.commit!);
J
Joao Moreno 已提交
99
	}
100 101
}

J
Joao Moreno 已提交
102 103
class CreateBranchItem implements QuickPickItem {

J
Joao Moreno 已提交
104 105
	constructor(private cc: CommandCenter) { }

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

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

J
Joao Moreno 已提交
111
	async run(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
112
		await this.cc.branch(repository);
J
Joao Moreno 已提交
113 114 115
	}
}

J
Joao Moreno 已提交
116 117 118 119
class CreateBranchFromItem implements QuickPickItem {

	constructor(private cc: CommandCenter) { }

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

	get alwaysShow(): boolean { return true; }

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

J
Joao Moreno 已提交
130 131 132 133 134 135 136 137 138
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; }
}

139 140 141 142
class AddRemoteItem implements QuickPickItem {

	constructor(private cc: CommandCenter) { }

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

	get alwaysShow(): boolean { return true; }

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

J
Joao Moreno 已提交
153
interface CommandOptions {
J
Joao Moreno 已提交
154
	repository?: boolean;
J
Joao Moreno 已提交
155 156 157
	diff?: boolean;
}

158 159 160 161
interface Command {
	commandId: string;
	key: string;
	method: Function;
J
Joao Moreno 已提交
162
	options: CommandOptions;
163 164 165
}

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

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

J
Joao Moreno 已提交
173
		Commands.push({ commandId, key, method: descriptor.value, options });
J
Joao Moreno 已提交
174 175
	};
}
J
Joao Moreno 已提交
176

J
Joao Moreno 已提交
177 178 179 180 181 182 183 184
// const ImageMimetypes = [
// 	'image/png',
// 	'image/gif',
// 	'image/jpeg',
// 	'image/webp',
// 	'image/tiff',
// 	'image/bmp'
// ];
J
Joao Moreno 已提交
185

J
Joao Moreno 已提交
186
async function categorizeResourceByResolution(resources: Resource[]): Promise<{ merge: Resource[], resolved: Resource[], unresolved: Resource[], deletionConflicts: Resource[] }> {
187 188 189
	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 已提交
190
	const isAnyDeleted = (s: Resource) => s.type === Status.DELETED_BY_THEM || s.type === Status.DELETED_BY_US;
191 192 193
	const possibleUnresolved = merge.filter(isBothAddedOrModified);
	const promises = possibleUnresolved.map(s => grep(s.resourceUri.fsPath, /^<{7}|^={7}|^>{7}/));
	const unresolvedBothModified = await Promise.all<boolean>(promises);
194
	const resolved = possibleUnresolved.filter((_s, i) => !unresolvedBothModified[i]);
J
Joao Moreno 已提交
195
	const deletionConflicts = merge.filter(s => isAnyDeleted(s));
196
	const unresolved = [
J
Joao Moreno 已提交
197
		...merge.filter(s => !isBothAddedOrModified(s) && !isAnyDeleted(s)),
198
		...possibleUnresolved.filter((_s, i) => unresolvedBothModified[i])
199 200
	];

J
Joao Moreno 已提交
201
	return { merge, resolved, unresolved, deletionConflicts };
202 203
}

J
Joao Moreno 已提交
204
function createCheckoutItems(repository: Repository): CheckoutItem[] {
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
	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
João Moreno 已提交
220 221 222 223 224
function sanitizeRemoteName(name: string) {
	name = name.trim();
	return name && name.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, '-');
}

J
Joao Moreno 已提交
225 226 227 228
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) { }
229 230
}

231 232 233
enum PushType {
	Push,
	PushTo,
234
	PushFollowTags,
235 236 237 238 239
}

interface PushOptions {
	pushType: PushType;
	forcePush?: boolean;
J
Joao Moreno 已提交
240
	silent?: boolean;
241 242
}

J
João Moreno 已提交
243 244 245 246 247 248 249 250 251 252 253
async function getQuickPickResult<T extends QuickPickItem>(quickpick: QuickPick<T>): Promise<T | undefined> {
	const result = await new Promise<T | undefined>(c => {
		quickpick.onDidAccept(() => c(quickpick.selectedItems[0]));
		quickpick.onDidHide(() => c(undefined));
		quickpick.show();
	});

	quickpick.hide();
	return result;
}

254
class RemoteSourceProviderQuickPick {
J
João Moreno 已提交
255

256
	private quickpick: QuickPick<QuickPickItem & { remoteSource?: RemoteSource }>;
J
João Moreno 已提交
257

258
	constructor(private provider: RemoteSourceProvider) {
J
João Moreno 已提交
259 260 261
		this.quickpick = window.createQuickPick();
		this.quickpick.ignoreFocusOut = true;

262
		if (provider.supportsQuery) {
J
João Moreno 已提交
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
			this.quickpick.placeholder = localize('type to search', "Repository name (type to search)");
			this.quickpick.onDidChangeValue(this.onDidChangeValue, this);
		} else {
			this.quickpick.placeholder = localize('type to filter', "Repository name");
		}
	}

	@debounce(300)
	onDidChangeValue(): void {
		this.query();
	}

	@throttle
	async query(): Promise<void> {
		this.quickpick.busy = true;
278 279 280 281 282 283 284 285 286 287 288 289

		try {
			const remoteSources = await this.provider.getRemoteSources(this.quickpick.value) || [];

			if (remoteSources.length === 0) {
				this.quickpick.items = [{
					label: localize('none found', "No remote repositories found."),
					alwaysShow: true
				}];
			} else {
				this.quickpick.items = remoteSources.map(remoteSource => ({
					label: remoteSource.name,
J
João Moreno 已提交
290
					description: remoteSource.description || (typeof remoteSource.url === 'string' ? remoteSource.url : remoteSource.url[0]),
J
João Moreno 已提交
291
					remoteSource
292 293 294 295
				}));
			}
		} catch (err) {
			this.quickpick.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }];
J
João Moreno 已提交
296
			console.error(err);
297 298
		} finally {
			this.quickpick.busy = false;
299
		}
J
João Moreno 已提交
300 301
	}

302
	async pick(): Promise<RemoteSource | undefined> {
J
João Moreno 已提交
303 304
		this.query();
		const result = await getQuickPickResult(this.quickpick);
305
		return result?.remoteSource;
J
João Moreno 已提交
306 307 308
	}
}

J
Joao Moreno 已提交
309
export class CommandCenter {
J
Joao Moreno 已提交
310 311

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

J
Joao Moreno 已提交
313
	constructor(
J
Joao Moreno 已提交
314
		private git: Git,
J
Joao Moreno 已提交
315
		private model: Model,
J
Joao Moreno 已提交
316 317
		private outputChannel: OutputChannel,
		private telemetryReporter: TelemetryReporter
J
Joao Moreno 已提交
318
	) {
J
Joao Moreno 已提交
319 320
		this.disposables = Commands.map(({ commandId, key, method, options }) => {
			const command = this.createCommand(commandId, key, method, options);
321

J
Joao Moreno 已提交
322
			if (options.diff) {
J
Joao Moreno 已提交
323 324 325 326 327
				return commands.registerDiffInformationCommand(commandId, command);
			} else {
				return commands.registerCommand(commandId, command);
			}
		});
J
Joao Moreno 已提交
328 329
	}

J
Joao Moreno 已提交
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
	@command('git.setLogLevel')
	async setLogLevel(): Promise<void> {
		const createItem = (logLevel: LogLevel) => ({
			label: LogLevel[logLevel],
			logLevel,
			description: Log.logLevel === logLevel ? localize('current', "Current") : undefined
		});

		const items = [
			createItem(LogLevel.Trace),
			createItem(LogLevel.Debug),
			createItem(LogLevel.Info),
			createItem(LogLevel.Warning),
			createItem(LogLevel.Error),
			createItem(LogLevel.Critical),
			createItem(LogLevel.Off)
		];

		const choice = await window.showQuickPick(items, {
			placeHolder: localize('select log level', "Select log level")
		});

		if (!choice) {
			return;
		}

		Log.logLevel = choice.logLevel;
		this.outputChannel.appendLine(localize('changed', "Log level changed to: {0}", LogLevel[Log.logLevel]));
	}

J
Joao Moreno 已提交
360 361
	@command('git.refresh', { repository: true })
	async refresh(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
362
		await repository.status();
J
Joao Moreno 已提交
363
	}
J
Joao Moreno 已提交
364

J
Joao Moreno 已提交
365
	@command('git.openResource')
J
João Moreno 已提交
366
	async openResource(resource: Resource, preserveFocus: boolean): Promise<void> {
J
Joao Moreno 已提交
367 368 369 370 371 372 373 374 375 376
		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) {
J
João Moreno 已提交
377
			await this._openResource(resource, undefined, preserveFocus, false);
J
Joao Moreno 已提交
378 379 380
		} else {
			await this.openFile(resource);
		}
J
Joao Moreno 已提交
381 382
	}

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

386 387 388 389
		try {
			stat = await new Promise<Stats>((c, e) => lstat(resource.resourceUri.fsPath, (err, stat) => err ? e(err) : c(stat)));
		} catch (err) {
			// noop
390
		}
391

392 393 394 395
		let left: Uri | undefined;
		let right: Uri | undefined;

		if (stat && stat.isDirectory()) {
396 397 398 399
			const repository = this.model.getRepositoryForSubmodule(resource.resourceUri);

			if (repository) {
				right = toGitUri(resource.resourceUri, resource.resourceGroupType === ResourceGroupType.Index ? 'index' : 'wt', { submoduleOf: repository.root });
400 401
			}
		} else {
J
Joao Moreno 已提交
402
			if (resource.type !== Status.DELETED_BY_THEM) {
J
Joao Moreno 已提交
403
				left = this.getLeftResource(resource);
J
Joao Moreno 已提交
404 405
			}

J
Joao Moreno 已提交
406
			right = this.getRightResource(resource);
407
		}
408

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

J
Joao Moreno 已提交
411 412 413 414 415
		if (!right) {
			// TODO
			console.error('oh no');
			return;
		}
J
Joao Moreno 已提交
416

J
Joao Moreno 已提交
417
		const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
418 419
			preserveFocus,
			preview,
420
			viewColumn: ViewColumn.Active
J
Joao Moreno 已提交
421 422
		};

J
Joao Moreno 已提交
423 424
		const activeTextEditor = window.activeTextEditor;

425 426 427
		// 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 已提交
428 429 430
			opts.selection = activeTextEditor.selection;
		}

431
		if (!left) {
J
Joao Moreno 已提交
432
			await commands.executeCommand<void>('vscode.open', right, opts, title);
J
Joao Moreno 已提交
433 434 435 436
		} else {
			await commands.executeCommand<void>('vscode.diff', left, right, title, opts);
		}
	}
J
Joao Moreno 已提交
437

J
Joao Moreno 已提交
438
	private getLeftResource(resource: Resource): Uri | undefined {
J
Joao Moreno 已提交
439 440 441
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
D
Darrien Singleton 已提交
442
			case Status.INDEX_ADDED:
J
Joao Moreno 已提交
443
				return toGitUri(resource.original, 'HEAD');
J
Joao Moreno 已提交
444 445

			case Status.MODIFIED:
446
			case Status.UNTRACKED:
J
Joao Moreno 已提交
447
				return toGitUri(resource.resourceUri, '~');
448 449

			case Status.DELETED_BY_THEM:
J
Joao Moreno 已提交
450
				return toGitUri(resource.resourceUri, '');
J
Joao Moreno 已提交
451
		}
452
		return undefined;
J
Joao Moreno 已提交
453
	}
J
Joao Moreno 已提交
454

J
Joao Moreno 已提交
455
	private getRightResource(resource: Resource): Uri | undefined {
J
Joao Moreno 已提交
456 457 458 459 460
		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_ADDED:
			case Status.INDEX_COPIED:
			case Status.INDEX_RENAMED:
J
Joao Moreno 已提交
461
				return toGitUri(resource.resourceUri, '');
J
Joao Moreno 已提交
462 463 464

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

J
Joao Moreno 已提交
467
			case Status.DELETED_BY_US:
J
Joao Moreno 已提交
468
				return toGitUri(resource.resourceUri, '~3');
J
Joao Moreno 已提交
469 470

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

J
Joao Moreno 已提交
473 474 475
			case Status.MODIFIED:
			case Status.UNTRACKED:
			case Status.IGNORED:
476
			case Status.INTENT_TO_ADD:
J
Joao Moreno 已提交
477 478 479 480 481 482
				const repository = this.model.getRepository(resource.resourceUri);

				if (!repository) {
					return;
				}

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

J
Joao Moreno 已提交
486 487
				if (indexStatus && indexStatus.renameResourceUri) {
					return indexStatus.renameResourceUri;
J
Joao Moreno 已提交
488 489
				}

J
Joao Moreno 已提交
490
				return resource.resourceUri;
J
Joao Moreno 已提交
491

492
			case Status.BOTH_ADDED:
J
Joao Moreno 已提交
493
			case Status.BOTH_MODIFIED:
J
Joao Moreno 已提交
494
				return resource.resourceUri;
J
Joao Moreno 已提交
495
		}
496
		return undefined;
J
Joao Moreno 已提交
497 498 499
	}

	private getTitle(resource: Resource): string {
J
Joao Moreno 已提交
500
		const basename = path.basename(resource.resourceUri.fsPath);
J
Joao Moreno 已提交
501 502 503 504

		switch (resource.type) {
			case Status.INDEX_MODIFIED:
			case Status.INDEX_RENAMED:
D
Darrien Singleton 已提交
505
			case Status.INDEX_ADDED:
E
Eric Amodio 已提交
506
				return localize('git.title.index', '{0} (Index)', basename);
J
Joao Moreno 已提交
507 508

			case Status.MODIFIED:
M
Marc Kassay 已提交
509 510
			case Status.BOTH_ADDED:
			case Status.BOTH_MODIFIED:
E
Eric Amodio 已提交
511
				return localize('git.title.workingTree', '{0} (Working Tree)', basename);
J
Joao Moreno 已提交
512 513

			case Status.DELETED_BY_US:
E
Eric Amodio 已提交
514
				return localize('git.title.theirs', '{0} (Theirs)', basename);
J
Joao Moreno 已提交
515 516

			case Status.DELETED_BY_THEM:
E
Eric Amodio 已提交
517
				return localize('git.title.ours', '{0} (Ours)', basename);
D
Darrien Singleton 已提交
518 519

			case Status.UNTRACKED:
E
Eric Amodio 已提交
520
				return localize('git.title.untracked', '{0} (Untracked)', basename);
D
Darrien Singleton 已提交
521

E
Eric Amodio 已提交
522 523
			default:
				return '';
J
Joao Moreno 已提交
524 525 526
		}
	}

J
Joao Moreno 已提交
527
	@command('git.clone')
528
	async clone(url?: string, parentPath?: string): Promise<void> {
529
		if (!url) {
J
João Moreno 已提交
530
			const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider, url?: string })>();
J
João Moreno 已提交
531 532 533
			quickpick.ignoreFocusOut = true;

			const providers = this.model.getRemoteProviders()
J
João Moreno 已提交
534
				.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('clonefrom', "Clone from {0}", provider.name), alwaysShow: true, provider }));
J
João Moreno 已提交
535 536

			quickpick.placeholder = providers.length === 0
J
João Moreno 已提交
537
				? localize('provide url', "Provide repository URL")
538
				: localize('provide url or pick', "Provide repository URL or pick a repository source.");
J
João Moreno 已提交
539

540
			const updatePicks = (value?: string) => {
J
João Moreno 已提交
541
				if (value) {
542 543 544
					quickpick.items = [{
						label: localize('repourl', "Clone from URL"),
						description: value,
J
João Moreno 已提交
545 546
						alwaysShow: true,
						url: value
547 548
					},
					...providers];
J
João Moreno 已提交
549 550 551
				} else {
					quickpick.items = providers;
				}
552 553 554 555
			};

			quickpick.onDidChangeValue(updatePicks);
			updatePicks();
J
João Moreno 已提交
556 557 558 559

			const result = await getQuickPickResult(quickpick);

			if (result) {
J
João Moreno 已提交
560 561 562
				if (result.url) {
					url = result.url;
				} else if (result.provider) {
563
					const quickpick = new RemoteSourceProviderQuickPick(result.provider);
J
João Moreno 已提交
564
					const remote = await quickpick.pick();
J
João Moreno 已提交
565 566 567 568 569 570 571 572

					if (remote) {
						if (typeof remote.url === 'string') {
							url = remote.url;
						} else if (remote.url.length > 0) {
							url = await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: localize('pick url', "Choose a URL to clone from.") });
						}
					}
J
João Moreno 已提交
573 574
				}
			}
575
		}
J
Joao Moreno 已提交
576 577

		if (!url) {
K
kieferrm 已提交
578
			/* __GDPR__
K
kieferrm 已提交
579 580 581 582
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
583 584
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
			return;
J
Joao Moreno 已提交
585 586
		}

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

589 590 591 592 593 594 595 596 597 598 599 600
		if (!parentPath) {
			const config = workspace.getConfiguration('git');
			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 已提交
601

602 603 604 605 606 607 608 609 610
			if (!uris || uris.length === 0) {
				/* __GDPR__
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
				return;
			}
J
Joao Moreno 已提交
611

612 613
			const uri = uris[0];
			parentPath = uri.fsPath;
J
Joao Moreno 已提交
614 615
		}

616
		try {
617 618 619 620 621
			const opts = {
				location: ProgressLocation.Notification,
				title: localize('cloning', "Cloning git repository '{0}'...", url),
				cancellable: true
			};
M
Maryam Archie 已提交
622

623 624
			const repositoryPath = await window.withProgress(
				opts,
625
				(progress, token) => this.git.clone(url!, parentPath!, progress, token)
626
			);
627

J
Joao Moreno 已提交
628 629 630 631
			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];
632 633 634

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

			const result = await window.showInformationMessage(message, ...choices);
640 641

			const openFolder = result === open;
K
kieferrm 已提交
642
			/* __GDPR__
K
kieferrm 已提交
643 644 645 646 647
				"clone" : {
					"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
					"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
				}
			*/
648
			this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: openFolder ? 1 : 0 });
649 650 651

			const uri = Uri.file(repositoryPath);

652
			if (openFolder) {
653 654 655
				commands.executeCommand('vscode.openFolder', uri);
			} else if (result === addToWorkspace) {
				workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
656 657
			} else if (result === openNewWindow) {
				commands.executeCommand('vscode.openFolder', uri, true);
658
			}
659 660
		} catch (err) {
			if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
K
kieferrm 已提交
661
				/* __GDPR__
K
kieferrm 已提交
662 663 664 665
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
666
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
J
Joao Moreno 已提交
667 668
			} else if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
				return;
669
			} else {
K
kieferrm 已提交
670
				/* __GDPR__
K
kieferrm 已提交
671 672 673 674
					"clone" : {
						"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
675
				this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
676
			}
J
Joao Moreno 已提交
677

678
			throw err;
J
Joao Moreno 已提交
679 680 681
		}
	}

J
Joao Moreno 已提交
682
	@command('git.init')
J
Joao Moreno 已提交
683
	async init(skipFolderPrompt = false): Promise<void> {
J
Joao Moreno 已提交
684
		let repositoryPath: string | undefined = undefined;
J
Joao Moreno 已提交
685
		let askToOpen = true;
J
Joao Moreno 已提交
686

J
Joao Moreno 已提交
687
		if (workspace.workspaceFolders) {
J
Joao Moreno 已提交
688 689
			if (skipFolderPrompt && workspace.workspaceFolders.length === 1) {
				repositoryPath = workspace.workspaceFolders[0].uri.fsPath;
J
Joao Moreno 已提交
690
				askToOpen = false;
J
Joao Moreno 已提交
691 692 693 694 695 696 697 698 699 700 701 702 703 704 705
			} else {
				const placeHolder = localize('init', "Pick workspace folder to initialize git repo in");
				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
				];
				const item = await window.showQuickPick(items, { placeHolder, ignoreFocusOut: true });

				if (!item) {
					return;
				} else if (item.folder) {
					repositoryPath = item.folder.uri.fsPath;
					askToOpen = false;
				}
J
Joao Moreno 已提交
706 707
			}
		}
J
Joao Moreno 已提交
708

J
Joao Moreno 已提交
709
		if (!repositoryPath) {
J
Joao Moreno 已提交
710 711 712 713 714 715 716 717 718 719 720 721
			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 已提交
722

J
Joao Moreno 已提交
723
			if (!result || result.length === 0) {
J
Joao Moreno 已提交
724 725
				return;
			}
J
Joao Moreno 已提交
726 727 728 729 730 731 732 733 734 735 736 737

			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 已提交
738
			repositoryPath = uri.fsPath;
J
Joao Moreno 已提交
739 740 741 742

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

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

J
Joao Moreno 已提交
747 748 749 750
		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 已提交
751

J
Joao Moreno 已提交
752 753 754 755
		if (!askToOpen) {
			return;
		}

J
Joao Moreno 已提交
756 757
		const addToWorkspace = localize('add', "Add to Workspace");
		if (workspace.workspaceFolders) {
J
Joao Moreno 已提交
758
			message = localize('proposeopen2 init', "Would you like to open the initialized repository, or add it to the current workspace?");
J
Joao Moreno 已提交
759 760
			choices.push(addToWorkspace);
		}
I
Ivan Sučić 已提交
761 762

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

J
Joao Moreno 已提交
765 766 767 768
		if (result === open) {
			commands.executeCommand('vscode.openFolder', uri);
		} else if (result === addToWorkspace) {
			workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
769 770
		} else if (result === openNewWindow) {
			commands.executeCommand('vscode.openFolder', uri, true);
J
Joao Moreno 已提交
771 772
		} else {
			await this.model.openRepository(repositoryPath);
I
Ivan Sučić 已提交
773
		}
J
Joao Moreno 已提交
774 775
	}

J
Joao Moreno 已提交
776 777
	@command('git.openRepository', { repository: false })
	async openRepository(path?: string): Promise<void> {
778
		if (!path) {
J
Joao Moreno 已提交
779 780 781 782 783 784
			const result = await window.showOpenDialog({
				canSelectFiles: false,
				canSelectFolders: true,
				canSelectMany: false,
				defaultUri: Uri.file(os.homedir()),
				openLabel: localize('open repo', "Open Repository")
785 786
			});

J
Joao Moreno 已提交
787
			if (!result || result.length === 0) {
788 789 790
				return;
			}

J
Joao Moreno 已提交
791
			path = result[0].fsPath;
792
		}
J
Joao Moreno 已提交
793 794

		await this.model.openRepository(path);
795 796
	}

J
Joao Moreno 已提交
797 798 799 800 801
	@command('git.close', { repository: true })
	async close(repository: Repository): Promise<void> {
		this.model.close(repository);
	}

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

806
		let uris: Uri[] | undefined;
807 808

		if (arg instanceof Uri) {
J
Joao Moreno 已提交
809
			if (isGitUri(arg)) {
810
				uris = [Uri.file(fromGitUri(arg).path)];
811
			} else if (arg.scheme === 'file') {
812
				uris = [arg];
813 814 815 816 817 818
			}
		} else {
			let resource = arg;

			if (!(resource instanceof Resource)) {
				// can happen when called from a keybinding
819
				resource = this.getSCMResource();
820 821 822
			}

			if (resource) {
J
Joao Moreno 已提交
823 824 825 826 827
				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];
828
			}
J
Joao Moreno 已提交
829 830
		}

831
		if (!uris) {
J
Joao Moreno 已提交
832
			return;
J
Joao Moreno 已提交
833 834
		}

835
		const activeTextEditor = window.activeTextEditor;
J
Joao Moreno 已提交
836

837 838
		for (const uri of uris) {
			const opts: TextDocumentShowOptions = {
J
Joao Moreno 已提交
839
				preserveFocus,
J
Joao Moreno 已提交
840
				preview: false,
841
				viewColumn: ViewColumn.Active
842 843
			};

M
Micah Smith 已提交
844 845 846 847
			let document;
			try {
				document = await workspace.openTextDocument(uri);
			} catch (error) {
J
João Moreno 已提交
848
				await commands.executeCommand('vscode.open', uri, opts);
M
Micah Smith 已提交
849 850
				continue;
			}
E
Eric Gang 已提交
851

852 853 854
			// 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 已提交
855
				// preserve not only selection but also visible range
B
Benjamin Pasero 已提交
856
				opts.selection = activeTextEditor.selection;
E
Eric Gang 已提交
857 858 859 860
				const previousVisibleRanges = activeTextEditor.visibleRanges;
				const editor = await window.showTextDocument(document, opts);
				editor.revealRange(previousVisibleRanges[0]);
			} else {
J
João Moreno 已提交
861
				await commands.executeCommand('vscode.open', uri, opts);
B
Benjamin Pasero 已提交
862
			}
J
Joao Moreno 已提交
863
		}
J
Joao Moreno 已提交
864 865
	}

J
Joao Moreno 已提交
866 867 868 869 870
	@command('git.openFile2')
	async openFile2(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
		this.openFile(arg, ...resourceStates);
	}

J
Joao Moreno 已提交
871 872
	@command('git.openHEADFile')
	async openHEADFile(arg?: Resource | Uri): Promise<void> {
D
Duroktar 已提交
873
		let resource: Resource | undefined = undefined;
J
Joao Moreno 已提交
874
		const preview = !(arg instanceof Resource);
D
Duroktar 已提交
875 876 877 878

		if (arg instanceof Resource) {
			resource = arg;
		} else if (arg instanceof Uri) {
879
			resource = this.getSCMResource(arg);
D
Duroktar 已提交
880
		} else {
881
			resource = this.getSCMResource();
D
Duroktar 已提交
882 883 884 885 886 887
		}

		if (!resource) {
			return;
		}

J
Joao Moreno 已提交
888
		const HEAD = this.getLeftResource(resource);
889 890
		const basename = path.basename(resource.resourceUri.fsPath);
		const title = `${basename} (HEAD)`;
D
Duroktar 已提交
891

J
Joao Moreno 已提交
892 893 894
		if (!HEAD) {
			window.showWarningMessage(localize('HEAD not available', "HEAD version of '{0}' is not available.", path.basename(resource.resourceUri.fsPath)));
			return;
D
Duroktar 已提交
895
		}
J
Joao Moreno 已提交
896

J
Joao Moreno 已提交
897 898 899 900
		const opts: TextDocumentShowOptions = {
			preview
		};

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

J
Joao Moreno 已提交
904 905
	@command('git.openChange')
	async openChange(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
906
		const preserveFocus = arg instanceof Resource;
J
Joao Moreno 已提交
907 908
		const preview = !(arg instanceof Resource);

J
Joao 已提交
909
		const preserveSelection = arg instanceof Uri || !arg;
910
		let resources: Resource[] | undefined = undefined;
911

912
		if (arg instanceof Uri) {
913
			const resource = this.getSCMResource(arg);
914 915 916
			if (resource !== undefined) {
				resources = [resource];
			}
917
		} else {
918
			let resource: Resource | undefined = undefined;
J
Joao Moreno 已提交
919

920 921 922
			if (arg instanceof Resource) {
				resource = arg;
			} else {
923
				resource = this.getSCMResource();
924 925 926 927 928
			}

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

931
		if (!resources) {
J
Joao Moreno 已提交
932
			return;
J
Joao Moreno 已提交
933
		}
J
Joao Moreno 已提交
934

935
		for (const resource of resources) {
J
Joao 已提交
936
			await this._openResource(resource, preview, preserveFocus, preserveSelection);
937
		}
J
Joao Moreno 已提交
938 939
	}

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

944 945
		resourceStates = resourceStates.filter(s => !!s);

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

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

951 952 953 954 955 956 957
			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

958
		const selection = resourceStates.filter(s => s instanceof Resource) as Resource[];
J
Joao Moreno 已提交
959
		const { resolved, unresolved, deletionConflicts } = await categorizeResourceByResolution(selection);
960 961 962 963 964

		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));
965

966 967 968 969 970 971 972 973
			const yes = localize('yes', "Yes");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

J
Joao Moreno 已提交
974 975 976 977 978 979 980 981 982 983 984 985 986 987
		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 已提交
988
		const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree);
989 990
		const untracked = selection.filter(s => s.resourceGroupType === ResourceGroupType.Untracked);
		const scmResources = [...workingTree, ...untracked, ...resolved, ...unresolved];
991

J
Joao Moreno 已提交
992
		this.outputChannel.appendLine(`git.stage.scmResources ${scmResources.length}`);
993
		if (!scmResources.length) {
J
Joao Moreno 已提交
994 995
			return;
		}
J
Joao Moreno 已提交
996

997
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
998
		await this.runByRepository(resources, async (repository, resources) => repository.add(resources));
J
Joao Moreno 已提交
999 1000
	}

J
Joao Moreno 已提交
1001 1002
	@command('git.stageAll', { repository: true })
	async stageAll(repository: Repository): Promise<void> {
P
Pascal Fong Kye 已提交
1003 1004
		const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates];
		const uris = resources.map(r => r.resourceUri);
J
Joao Moreno 已提交
1005

P
Pascal Fong Kye 已提交
1006 1007 1008 1009
		if (uris.length > 0) {
			const config = workspace.getConfiguration('git', Uri.file(repository.root));
			const untrackedChanges = config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
			await repository.add(uris, untrackedChanges === 'mixed' ? undefined : { update: true });
J
Joao Moreno 已提交
1010
		}
J
Joao Moreno 已提交
1011 1012
	}

J
Joao Moreno 已提交
1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047
	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');
			}
		}
	}

1048 1049 1050 1051 1052 1053 1054 1055 1056
	@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);
	}

1057 1058 1059 1060 1061 1062 1063 1064 1065
	@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);
	}

1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102
	@command('git.stageAllMerge', { repository: true })
	async stageAllMerge(repository: Repository): Promise<void> {
		const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[];
		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;
		}

		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?", 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));

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

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

		const uris = resources.map(r => r.resourceUri);

		if (uris.length > 0) {
			await repository.add(uris);
		}
	}

J
Joao Moreno 已提交
1103
	@command('git.stageChange')
J
Joao Moreno 已提交
1104 1105 1106 1107 1108 1109 1110 1111
	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 已提交
1112 1113
	}

J
Joao Moreno 已提交
1114
	@command('git.stageSelectedRanges', { diff: true })
J
Joao Moreno 已提交
1115
	async stageSelectedChanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
1116 1117 1118 1119 1120 1121 1122
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
J
Joao Moreno 已提交
1123 1124 1125 1126
		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 已提交
1127

J
Joao Moreno 已提交
1128
		if (!selectedChanges.length) {
J
Joao Moreno 已提交
1129 1130 1131
			return;
		}

J
Joao Moreno 已提交
1132 1133
		await this._stageChanges(textEditor, selectedChanges);
	}
J
Joao Moreno 已提交
1134

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

J
Joao Moreno 已提交
1139
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
1140 1141 1142
			return;
		}

J
Joao Moreno 已提交
1143
		const originalUri = toGitUri(modifiedUri, '~');
J
Joao Moreno 已提交
1144
		const originalDocument = await workspace.openTextDocument(originalUri);
J
Joao Moreno 已提交
1145
		const result = applyLineChanges(originalDocument, modifiedDocument, changes);
J
Joao Moreno 已提交
1146

J
Joao Moreno 已提交
1147
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
1148
	}
J
Joao Moreno 已提交
1149

J
Joao Moreno 已提交
1150
	@command('git.revertChange')
J
Joao Moreno 已提交
1151 1152 1153 1154
	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 已提交
1155 1156 1157
			return;
		}

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

J
Joao Moreno 已提交
1161
	@command('git.revertSelectedRanges', { diff: true })
J
Joao Moreno 已提交
1162
	async revertSelectedRanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
1163 1164 1165 1166 1167 1168 1169
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
J
Joao Moreno 已提交
1170 1171
		const selections = textEditor.selections;
		const selectedChanges = changes.filter(change => {
J
Joao Moreno 已提交
1172
			const modifiedRange = getModifiedRange(modifiedDocument, change);
J
Joao Moreno 已提交
1173 1174 1175 1176
			return selections.every(selection => !selection.intersection(modifiedRange));
		});

		if (selectedChanges.length === changes.length) {
J
Joao Moreno 已提交
1177 1178 1179
			return;
		}

J
Joao Moreno 已提交
1180 1181
		await this._revertChanges(textEditor, selectedChanges);
	}
J
Joao Moreno 已提交
1182

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

J
Joao Moreno 已提交
1187
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
1188 1189 1190
			return;
		}

J
Joao Moreno 已提交
1191 1192
		const originalUri = toGitUri(modifiedUri, '~');
		const originalDocument = await workspace.openTextDocument(originalUri);
1193 1194
		const selectionsBeforeRevert = textEditor.selections;
		const visibleRangesBeforeRevert = textEditor.visibleRanges;
J
Joao Moreno 已提交
1195
		const result = applyLineChanges(originalDocument, modifiedDocument, changes);
1196

J
Joao Moreno 已提交
1197 1198 1199
		const edit = new WorkspaceEdit();
		edit.replace(modifiedUri, new Range(new Position(0, 0), modifiedDocument.lineAt(modifiedDocument.lineCount - 1).range.end), result);
		workspace.applyEdit(edit);
1200

J
Joao Moreno 已提交
1201 1202
		await modifiedDocument.save();

1203 1204
		textEditor.selections = selectionsBeforeRevert;
		textEditor.revealRange(visibleRangesBeforeRevert[0]);
J
Joao Moreno 已提交
1205 1206
	}

J
Joao Moreno 已提交
1207 1208
	@command('git.unstage')
	async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
1209 1210
		resourceStates = resourceStates.filter(s => !!s);

J
Joao Moreno 已提交
1211
		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
1212
			const resource = this.getSCMResource();
1213 1214 1215 1216 1217 1218 1219 1220

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

1224
		if (!scmResources.length) {
J
Joao Moreno 已提交
1225 1226 1227
			return;
		}

1228
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
1229
		await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
J
Joao Moreno 已提交
1230 1231
	}

J
Joao Moreno 已提交
1232 1233
	@command('git.unstageAll', { repository: true })
	async unstageAll(repository: Repository): Promise<void> {
1234
		await repository.revert([]);
J
Joao Moreno 已提交
1235
	}
J
Joao Moreno 已提交
1236

J
Joao Moreno 已提交
1237 1238
	@command('git.unstageSelectedRanges', { diff: true })
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
1239 1240 1241 1242 1243 1244 1245 1246 1247
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

J
Joao Moreno 已提交
1248
		if (!isGitUri(modifiedUri)) {
1249 1250 1251 1252 1253 1254
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
J
Joao Moreno 已提交
1255 1256 1257
			return;
		}

J
Joao Moreno 已提交
1258
		const originalUri = toGitUri(modifiedUri, 'HEAD');
J
Joao Moreno 已提交
1259
		const originalDocument = await workspace.openTextDocument(originalUri);
1260 1261 1262 1263
		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 已提交
1264 1265 1266 1267 1268

		if (!selectedDiffs.length) {
			return;
		}

1269 1270
		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
J
Joao Moreno 已提交
1271

J
Joao Moreno 已提交
1272
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
1273 1274
	}

J
Joao Moreno 已提交
1275 1276
	@command('git.clean')
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
1277 1278
		resourceStates = resourceStates.filter(s => !!s);

J
Joao Moreno 已提交
1279
		if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
1280
			const resource = this.getSCMResource();
1281 1282 1283 1284 1285 1286 1287 1288

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

1289 1290
		const scmResources = resourceStates.filter(s => s instanceof Resource
			&& (s.resourceGroupType === ResourceGroupType.WorkingTree || s.resourceGroupType === ResourceGroupType.Untracked)) as Resource[];
1291

J
Joao Moreno 已提交
1292
		if (!scmResources.length) {
J
Joao Moreno 已提交
1293 1294
			return;
		}
J
Joao Moreno 已提交
1295

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

J
Joao Moreno 已提交
1300
		if (scmResources.length === 1) {
J
Joao Moreno 已提交
1301
			if (untrackedCount > 0) {
J
Joao Moreno 已提交
1302
				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 已提交
1303 1304
				yes = localize('delete file', "Delete file");
			} else {
1305 1306 1307 1308 1309 1310
				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 已提交
1311 1312
			}
		} else {
1313 1314 1315 1316 1317 1318
			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 已提交
1319 1320

			if (untrackedCount > 0) {
J
Joao Moreno 已提交
1321
				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 已提交
1322 1323
			}
		}
1324

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

J
Joao Moreno 已提交
1327 1328 1329 1330
		if (pick !== yes) {
			return;
		}

J
Joao Moreno 已提交
1331
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
1332
		await this.runByRepository(resources, async (repository, resources) => repository.clean(resources));
J
Joao Moreno 已提交
1333
	}
J
Joao Moreno 已提交
1334

J
Joao Moreno 已提交
1335 1336
	@command('git.cleanAll', { repository: true })
	async cleanAll(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1337
		let resources = repository.workingTreeGroup.resourceStates;
J
Joao Moreno 已提交
1338 1339 1340 1341 1342

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

J
Joao Moreno 已提交
1343 1344
		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 已提交
1345

J
Joao Moreno 已提交
1346
		if (untrackedResources.length === 0) {
1347
			await this._cleanTrackedChanges(repository, resources);
1348 1349
		} else if (resources.length === 1) {
			await this._cleanUntrackedChange(repository, resources[0]);
J
Joao Moreno 已提交
1350
		} else if (trackedResources.length === 0) {
1351
			await this._cleanUntrackedChanges(repository, resources);
J
Joao Moreno 已提交
1352 1353 1354 1355
		} 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 已提交
1356

J
Joao Moreno 已提交
1357 1358 1359 1360 1361
			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 已提交
1362

J
Joao Moreno 已提交
1363
			const yesAll = localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
1364 1365 1366 1367 1368 1369 1370 1371
			const pick = await window.showWarningMessage(message, { modal: true }, yesTracked, yesAll);

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

J
Joao 已提交
1372
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
1373
		}
J
Joao Moreno 已提交
1374 1375
	}

1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387
	@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);
	}

1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403
	@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);
		}
	}

1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419
	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));
	}

1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443
	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 已提交
1444
	private async smartCommit(
J
Joao Moreno 已提交
1445
		repository: Repository,
1446
		getCommitMessage: () => Promise<string | undefined>,
J
Joao Moreno 已提交
1447 1448
		opts?: CommitOptions
	): Promise<boolean> {
1449
		const config = workspace.getConfiguration('git', Uri.file(repository.root));
1450
		let promptToSaveFilesBeforeCommit = config.get<'always' | 'staged' | 'never'>('promptToSaveFilesBeforeCommit');
J
Joao Moreno 已提交
1451

1452 1453 1454 1455 1456 1457 1458
		// migration
		if (promptToSaveFilesBeforeCommit as any === true) {
			promptToSaveFilesBeforeCommit = 'always';
		} else if (promptToSaveFilesBeforeCommit as any === false) {
			promptToSaveFilesBeforeCommit = 'never';
		}

J
Joao Moreno 已提交
1459
		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
J
Joao Moreno 已提交
1460 1461 1462
		const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
J
Joao Moreno 已提交
1463

1464 1465 1466 1467
		if (promptToSaveFilesBeforeCommit !== 'never') {
			let documents = workspace.textDocuments
				.filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath));

J
Joao Moreno 已提交
1468
			if (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) {
1469
				documents = documents
J
Joao Moreno 已提交
1470
					.filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath)));
1471
			}
1472

1473 1474
			if (documents.length > 0) {
				const message = documents.length === 1
J
Joao Moreno 已提交
1475
					? 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 已提交
1476
					: localize('unsaved files', "There are {0} unsaved files.\n\nWould you like to save them before committing?", documents.length);
J
Joao Moreno 已提交
1477 1478 1479 1480 1481
				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) {
1482
					await Promise.all(documents.map(d => d.save()));
J
Joao Moreno 已提交
1483
					await repository.add(documents.map(d => d.uri));
J
Joao Moreno 已提交
1484 1485 1486
				} else if (pick !== commit) {
					return false; // do not commit on cancel
				}
1487 1488 1489
			}
		}

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

1494 1495 1496 1497
			if (!suggestSmartCommit) {
				return false;
			}

J
Joao Moreno 已提交
1498 1499
			// 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?");
1500 1501
			const yes = localize('yes', "Yes");
			const always = localize('always', "Always");
1502 1503
			const never = localize('never', "Never");
			const pick = await window.showWarningMessage(message, { modal: true }, yes, always, never);
1504 1505

			if (pick === always) {
J
Joao Moreno 已提交
1506
				config.update('enableSmartCommit', true, true);
1507 1508 1509
			} else if (pick === never) {
				config.update('suggestSmartCommit', false, true);
				return false;
J
Joao Moreno 已提交
1510 1511
			} else if (pick !== yes) {
				return false; // do not commit on cancel
1512 1513 1514
			}
		}

J
Joao Moreno 已提交
1515
		if (!opts) {
1516
			opts = { all: noStagedChanges };
J
Joao Moreno 已提交
1517 1518
		} else if (!opts.all && noStagedChanges) {
			opts = { ...opts, all: true };
J
Joao Moreno 已提交
1519 1520
		}

1521 1522 1523
		// enable signing of commits if configurated
		opts.signCommit = enableCommitSigning;

1524 1525 1526 1527
		if (config.get<boolean>('alwaysSignOff')) {
			opts.signoff = true;
		}

J
Joao Moreno 已提交
1528 1529
		const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges');

J
Joao Moreno 已提交
1530
		if (
1531
			(
J
Joao Moreno 已提交
1532 1533 1534 1535
				// no changes
				(noStagedChanges && noUnstagedChanges)
				// or no staged changes and not `all`
				|| (!opts.all && noStagedChanges)
J
Joao Moreno 已提交
1536 1537
				// no staged changes and no tracked unstaged changes
				|| (noStagedChanges && smartCommitChanges === 'tracked' && repository.workingTreeGroup.resourceStates.every(r => r.type === Status.UNTRACKED))
J
Joao Moreno 已提交
1538
			)
1539
			&& !opts.empty
J
Joao Moreno 已提交
1540
		) {
J
Joao Moreno 已提交
1541 1542 1543 1544
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
1545
		const message = await getCommitMessage();
J
Joao Moreno 已提交
1546 1547 1548 1549 1550

		if (!message) {
			return false;
		}

J
Joao Moreno 已提交
1551
		if (opts.all && smartCommitChanges === 'tracked') {
J
Joao Moreno 已提交
1552
			opts.all = 'tracked';
1553 1554
		}

1555
		if (opts.all && config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges') !== 'mixed') {
1556
			opts.all = 'tracked';
J
Joao Moreno 已提交
1557 1558
		}

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

J
Joao Moreno 已提交
1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571
		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 已提交
1572 1573 1574
		return true;
	}

J
Joao Moreno 已提交
1575
	private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
J
Joao Moreno 已提交
1576
		const message = repository.inputBox.value;
J
Joao Moreno 已提交
1577
		const getCommitMessage = async () => {
1578
			let _message: string | undefined = message;
1579

1580 1581
			if (!_message) {
				let value: string | undefined = undefined;
J
Joao Moreno 已提交
1582

1583 1584 1585
				if (opts && opts.amend && repository.HEAD && repository.HEAD.commit) {
					value = (await repository.getCommit(repository.HEAD.commit)).message;
				}
1586

1587 1588
				const branchName = repository.headShortName;
				let placeHolder: string;
1589

1590 1591 1592 1593 1594
				if (branchName) {
					placeHolder = localize('commitMessageWithHeadLabel2', "Message (commit on '{0}')", branchName);
				} else {
					placeHolder = localize('commit message', "Commit message");
				}
J
Joao Moreno 已提交
1595

1596 1597
				_message = await window.showInputBox({
					value,
1598
					placeHolder,
1599 1600 1601
					prompt: localize('provide commit message', "Please provide a commit message"),
					ignoreFocusOut: true
				});
A
💄  
al 已提交
1602
			}
J
Joao Moreno 已提交
1603

1604
			return _message;
J
Joao Moreno 已提交
1605 1606
		};

J
Joao Moreno 已提交
1607
		const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
J
Joao Moreno 已提交
1608 1609

		if (message && didCommit) {
J
Joao Moreno 已提交
1610
			repository.inputBox.value = await repository.getInputTemplate();
J
Joao Moreno 已提交
1611
		}
J
Joao Moreno 已提交
1612 1613
	}

J
Joao Moreno 已提交
1614
	@command('git.commit', { repository: true })
J
Joao Moreno 已提交
1615 1616
	async commit(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository);
J
Joao Moreno 已提交
1617 1618
	}

J
Joao Moreno 已提交
1619
	@command('git.commitStaged', { repository: true })
J
Joao Moreno 已提交
1620 1621
	async commitStaged(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false });
J
Joao Moreno 已提交
1622 1623
	}

J
Joao Moreno 已提交
1624
	@command('git.commitStagedSigned', { repository: true })
J
Joao Moreno 已提交
1625 1626
	async commitStagedSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, signoff: true });
J
Joao Moreno 已提交
1627 1628
	}

J
Joao Moreno 已提交
1629
	@command('git.commitStagedAmend', { repository: true })
J
Joao Moreno 已提交
1630
	async commitStagedAmend(repository: Repository): Promise<void> {
1631
		await this.commitWithAnyInput(repository, { all: false, amend: true });
K
Krzysztof Cieślak 已提交
1632 1633
	}

J
Joao Moreno 已提交
1634
	@command('git.commitAll', { repository: true })
J
Joao Moreno 已提交
1635 1636
	async commitAll(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true });
J
Joao Moreno 已提交
1637 1638
	}

J
Joao Moreno 已提交
1639
	@command('git.commitAllSigned', { repository: true })
J
Joao Moreno 已提交
1640 1641
	async commitAllSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, signoff: true });
J
Joao Moreno 已提交
1642 1643
	}

J
Joao Moreno 已提交
1644
	@command('git.commitAllAmend', { repository: true })
J
Joao Moreno 已提交
1645 1646
	async commitAllAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, amend: true });
K
Krzysztof Cieślak 已提交
1647 1648
	}

J
Joao Moreno 已提交
1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670
	@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 已提交
1671 1672 1673 1674 1675
	@command('git.restoreCommitTemplate', { repository: true })
	async restoreCommitTemplate(repository: Repository): Promise<void> {
		repository.inputBox.value = await repository.getCommitTemplate();
	}

J
Joao Moreno 已提交
1676
	@command('git.undoCommit', { repository: true })
J
Joao Moreno 已提交
1677 1678
	async undoCommit(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
1679 1680

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

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

1687 1688
		if (commit.parents.length > 1) {
			const yes = localize('undo commit', "Undo merge commit");
J
Joao Moreno 已提交
1689
			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);
1690 1691 1692 1693 1694 1695

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

J
Joao Moreno 已提交
1696
		if (commit.parents.length > 0) {
1697 1698 1699 1700 1701
			await repository.reset('HEAD~');
		} else {
			await repository.deleteRef('HEAD');
			await this.unstageAll(repository);
		}
J
Joao Moreno 已提交
1702

J
Joao Moreno 已提交
1703
		repository.inputBox.value = commit.message;
J
Joao Moreno 已提交
1704 1705
	}

J
Joao Moreno 已提交
1706
	@command('git.checkout', { repository: true })
1707
	async checkout(repository: Repository, treeish: string): Promise<boolean> {
J
Joao Moreno 已提交
1708
		if (typeof treeish === 'string') {
1709 1710
			await repository.checkout(treeish);
			return true;
J
Joao Moreno 已提交
1711
		}
J
Joao Moreno 已提交
1712

J
Joao Moreno 已提交
1713 1714 1715
		const createBranch = new CreateBranchItem(this);
		const createBranchFrom = new CreateBranchFromItem(this);
		const picks = [createBranch, createBranchFrom, ...createCheckoutItems(repository)];
1716
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
1717 1718 1719 1720 1721 1722 1723 1724

		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 已提交
1725 1726

		if (!choice) {
1727
			return false;
J
Joao Moreno 已提交
1728 1729
		}

1730 1731
		if (choice === createBranch) {
			await this._branch(repository, quickpick.value);
J
Joao Moreno 已提交
1732 1733
		} else if (choice === createBranchFrom) {
			await this._branch(repository, quickpick.value, true);
1734 1735 1736 1737
		} else {
			await (choice as CheckoutItem).run(repository);
		}

1738
		return true;
J
Joao Moreno 已提交
1739 1740
	}

J
Joao Moreno 已提交
1741
	@command('git.branch', { repository: true })
J
Joao Moreno 已提交
1742
	async branch(repository: Repository): Promise<void> {
1743 1744
		await this._branch(repository);
	}
J
Joao Moreno 已提交
1745

J
Joao Moreno 已提交
1746 1747 1748 1749 1750
	@command('git.branchFrom', { repository: true })
	async branchFrom(repository: Repository): Promise<void> {
		await this._branch(repository, undefined, true);
	}

1751
	private async promptForBranchName(defaultName?: string, initialValue?: string): Promise<string> {
1752 1753 1754 1755
		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 已提交
1756
			name.trim().replace(/^-+/, '').replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, branchWhitespaceChar)
1757 1758
			: name;

J
Joao Moreno 已提交
1759
		const rawBranchName = defaultName || await window.showInputBox({
1760
			placeHolder: localize('branch name', "Branch name"),
1761
			prompt: localize('provide branch name', "Please provide a new branch name"),
1762
			value: initialValue,
1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773
			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 已提交
1774 1775 1776 1777 1778
		return sanitize(rawBranchName || '');
	}

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

		if (!branchName) {
1781 1782
			return;
		}
J
Joao Moreno 已提交
1783

J
Joao Moreno 已提交
1784
		let target = 'HEAD';
J
Joao Moreno 已提交
1785

J
Joao Moreno 已提交
1786 1787 1788 1789 1790 1791 1792 1793 1794 1795
		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 已提交
1796
		}
J
Joao Moreno 已提交
1797

J
Joao Moreno 已提交
1798
		await repository.branch(branchName, true, target);
J
Joao Moreno 已提交
1799 1800
	}

J
Joao Moreno 已提交
1801
	@command('git.deleteBranch', { repository: true })
J
Joao Moreno 已提交
1802
	async deleteBranch(repository: Repository, name: string, force?: boolean): Promise<void> {
1803 1804
		let run: (force?: boolean) => Promise<void>;
		if (typeof name === 'string') {
J
Joao Moreno 已提交
1805
			run = force => repository.deleteBranch(name, force);
1806
		} else {
J
Joao Moreno 已提交
1807 1808
			const currentHead = repository.HEAD && repository.HEAD.name;
			const heads = repository.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead)
1809
				.map(ref => new BranchDeleteItem(ref));
M
Maik Riechert 已提交
1810

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

M
Maik Riechert 已提交
1814
			if (!choice || !choice.branchName) {
1815 1816
				return;
			}
M
Maik Riechert 已提交
1817
			name = choice.branchName;
J
Joao Moreno 已提交
1818
			run = force => choice.run(repository, force);
M
Maik Riechert 已提交
1819 1820
		}

1821 1822 1823 1824 1825 1826 1827 1828 1829
		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");
1830
			const pick = await window.showWarningMessage(message, { modal: true }, yes);
1831 1832 1833 1834 1835

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

1838 1839
	@command('git.renameBranch', { repository: true })
	async renameBranch(repository: Repository): Promise<void> {
1840 1841
		const currentBranchName = repository.HEAD && repository.HEAD.name;
		const branchName = await this.promptForBranchName(undefined, currentBranchName);
J
Justin Horner 已提交
1842

J
Joao Moreno 已提交
1843
		if (!branchName) {
J
Justin Horner 已提交
1844 1845 1846 1847
			return;
		}

		try {
J
Joao Moreno 已提交
1848
			await repository.renameBranch(branchName);
J
Justin Horner 已提交
1849 1850 1851
		} catch (err) {
			switch (err.gitErrorCode) {
				case GitErrorCodes.InvalidBranchName:
1852 1853
					window.showErrorMessage(localize('invalid branch name', 'Invalid branch name'));
					return;
J
Justin Horner 已提交
1854
				case GitErrorCodes.BranchAlreadyExists:
J
Joao Moreno 已提交
1855
					window.showErrorMessage(localize('branch already exists', "A branch named '{0}' already exists", branchName));
1856 1857 1858
					return;
				default:
					throw err;
J
Justin Horner 已提交
1859 1860 1861 1862
			}
		}
	}

J
Joao Moreno 已提交
1863
	@command('git.merge', { repository: true })
J
Joao Moreno 已提交
1864
	async merge(repository: Repository): Promise<void> {
1865 1866 1867 1868
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string>('checkoutType') || 'all';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1869
		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
1870 1871
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1872

J
Joao Moreno 已提交
1873
		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
1874 1875
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1876 1877

		const picks = [...heads, ...remoteHeads];
1878 1879
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
1880 1881 1882 1883 1884

		if (!choice) {
			return;
		}

1885
		await choice.run(repository);
1886 1887
	}

J
Joao Moreno 已提交
1888
	@command('git.createTag', { repository: true })
J
Joao Moreno 已提交
1889
	async createTag(repository: Repository): Promise<void> {
1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901
		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 已提交
1902
			prompt: localize('provide tag message', "Please provide a message to annotate the tag"),
1903 1904 1905 1906 1907
			ignoreFocusOut: true
		});

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

X
Xhulio Hasani 已提交
1911 1912
	@command('git.deleteTag', { repository: true })
	async deleteTag(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1913 1914 1915 1916
		const picks = repository.refs.filter(ref => ref.type === RefType.Tag)
			.map(ref => new TagItem(ref));

		if (picks.length === 0) {
1917
			window.showWarningMessage(localize('no tags', "This repository has no tags."));
J
Joao Moreno 已提交
1918
			return;
1919
		}
J
Joao Moreno 已提交
1920

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

1924
		if (!choice) {
X
Xhulio Hasani 已提交
1925 1926
			return;
		}
J
Joao Moreno 已提交
1927 1928

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

J
Joao Moreno 已提交
1931 1932 1933 1934 1935 1936 1937
	@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 已提交
1938
		await repository.fetchDefault();
J
Joao Moreno 已提交
1939 1940
	}

R
Ryan Scott 已提交
1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951
	@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 已提交
1952 1953 1954 1955 1956 1957 1958 1959 1960 1961
	@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 已提交
1962
	@command('git.pullFrom', { repository: true })
J
Joao Moreno 已提交
1963 1964
	async pullFrom(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1965 1966 1967 1968 1969 1970

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

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

D
Dozed12 已提交
1975
		if (!remotePick) {
1976 1977 1978
			return;
		}

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

D
Dozed12 已提交
1985
		if (!branchPick) {
1986 1987 1988
			return;
		}

F
Francisco Moreira 已提交
1989 1990
		const remoteCharCnt = remotePick.label.length;

1991
		await repository.pullFrom(false, remotePick.label, branchPick.label.slice(remoteCharCnt + 1));
1992 1993
	}

J
Joao Moreno 已提交
1994
	@command('git.pull', { repository: true })
J
Joao Moreno 已提交
1995 1996
	async pull(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1997 1998 1999 2000 2001 2002

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

J
Joao Moreno 已提交
2003
		await repository.pull(repository.HEAD);
J
Joao Moreno 已提交
2004 2005
	}

J
Joao Moreno 已提交
2006
	@command('git.pullRebase', { repository: true })
J
Joao Moreno 已提交
2007 2008
	async pullRebase(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
2009 2010 2011 2012 2013 2014

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

J
Joao Moreno 已提交
2015
		await repository.pullWithRebase(repository.HEAD);
J
Joao Moreno 已提交
2016 2017
	}

J
Joao Moreno 已提交
2018
	private async _push(repository: Repository, pushOptions: PushOptions) {
J
Joao Moreno 已提交
2019
		const remotes = repository.remotes;
2020

J
Joao Moreno 已提交
2021
		if (remotes.length === 0) {
J
João Moreno 已提交
2022 2023
			if (pushOptions.silent) {
				return;
J
Joao Moreno 已提交
2024
			}
J
João Moreno 已提交
2025 2026 2027 2028 2029 2030 2031 2032

			const addRemote = localize('addremote', 'Add Remote');
			const result = await window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."), addRemote);

			if (result === addRemote) {
				await this.addRemote(repository);
			}

J
Joao Moreno 已提交
2033 2034
			return;
		}
2035

J
Joao Moreno 已提交
2036 2037 2038 2039 2040 2041
		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."));
2042 2043
				return;
			}
J
Joao Moreno 已提交
2044

J
Joao Moreno 已提交
2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058
			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 已提交
2059 2060
		}

2061 2062
		if (pushOptions.pushType === PushType.PushFollowTags) {
			await repository.pushFollowTags(undefined, forcePushMode);
2063 2064 2065
			return;
		}

2066
		if (!repository.HEAD || !repository.HEAD.name) {
J
Joao Moreno 已提交
2067 2068 2069
			if (!pushOptions.silent) {
				window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			}
2070 2071 2072
			return;
		}

2073 2074 2075 2076 2077 2078 2079
		if (pushOptions.pushType === PushType.Push) {
			try {
				await repository.push(repository.HEAD, forcePushMode);
			} catch (err) {
				if (err.gitErrorCode !== GitErrorCodes.NoUpstreamBranch) {
					throw err;
				}
2080

J
Joao Moreno 已提交
2081 2082 2083 2084
				if (pushOptions.silent) {
					return;
				}

2085 2086 2087 2088 2089 2090 2091 2092 2093 2094
				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 {
2095
			const branchName = repository.HEAD.name;
J
Joao Moreno 已提交
2096 2097
			const addRemote = new AddRemoteItem(this);
			const picks = [...remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl })), addRemote];
2098
			const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
J
Joao Moreno 已提交
2099
			const choice = await window.showQuickPick(picks, { placeHolder });
2100

J
Joao Moreno 已提交
2101
			if (!choice) {
2102
				return;
2103
			}
2104

J
Joao Moreno 已提交
2105 2106 2107 2108 2109 2110 2111 2112 2113
			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);
			}
2114
		}
J
Joao Moreno 已提交
2115 2116
	}

2117 2118
	@command('git.push', { repository: true })
	async push(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
2119
		await this._push(repository, { pushType: PushType.Push });
2120
	}
2121

2122 2123
	@command('git.pushForce', { repository: true })
	async pushForce(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
2124
		await this._push(repository, { pushType: PushType.Push, forcePush: true });
2125
	}
2126

2127
	@command('git.pushWithTags', { repository: true })
2128 2129
	async pushFollowTags(repository: Repository): Promise<void> {
		await this._push(repository, { pushType: PushType.PushFollowTags });
2130
	}
2131

2132
	@command('git.pushWithTagsForce', { repository: true })
2133 2134
	async pushFollowTagsForce(repository: Repository): Promise<void> {
		await this._push(repository, { pushType: PushType.PushFollowTags, forcePush: true });
2135 2136
	}

J
Joao Moreno 已提交
2137
	@command('git.pushTo', { repository: true })
J
Joao Moreno 已提交
2138
	async pushTo(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
2139
		await this._push(repository, { pushType: PushType.PushTo });
2140
	}
J
Joao Moreno 已提交
2141

2142 2143
	@command('git.pushToForce', { repository: true })
	async pushToForce(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
2144
		await this._push(repository, { pushType: PushType.PushTo, forcePush: true });
J
Joao Moreno 已提交
2145 2146
	}

O
Omkar Manjrekar 已提交
2147
	@command('git.addRemote', { repository: true })
2148
	async addRemote(repository: Repository): Promise<string | undefined> {
J
João Moreno 已提交
2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170
		const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider, url?: string })>();
		quickpick.ignoreFocusOut = true;

		const providers = this.model.getRemoteProviders()
			.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('addfrom', "Add remote from {0}", provider.name), alwaysShow: true, provider }));

		quickpick.placeholder = providers.length === 0
			? localize('provide url', "Provide repository URL")
			: localize('provide url or pick', "Provide repository URL or pick a repository source.");

		const updatePicks = (value?: string) => {
			if (value) {
				quickpick.items = [{
					label: localize('addFrom', "Add remote from URL"),
					description: value,
					alwaysShow: true,
					url: value
				},
				...providers];
			} else {
				quickpick.items = providers;
			}
O
Omkar Manjrekar 已提交
2171
		};
O
Omkar Manjrekar 已提交
2172

J
João Moreno 已提交
2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199
		quickpick.onDidChangeValue(updatePicks);
		updatePicks();

		const result = await getQuickPickResult(quickpick);
		let url: string | undefined;

		if (result) {
			if (result.url) {
				url = result.url;
			} else if (result.provider) {
				const quickpick = new RemoteSourceProviderQuickPick(result.provider);
				const remote = await quickpick.pick();

				if (remote) {
					if (typeof remote.url === 'string') {
						url = remote.url;
					} else if (remote.url.length > 0) {
						url = await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: localize('pick url', "Choose a URL to clone from.") });
					}
				}
			}
		}

		if (!url) {
			return;
		}

O
Omkar Manjrekar 已提交
2200
		const resultName = await window.showInputBox({
O
Omkar Manjrekar 已提交
2201 2202 2203 2204
			placeHolder: localize('remote name', "Remote name"),
			prompt: localize('provide remote name', "Please provide a remote name"),
			ignoreFocusOut: true,
			validateInput: (name: string) => {
J
João Moreno 已提交
2205 2206 2207 2208
				if (!sanitizeRemoteName(name)) {
					return localize('remote name format invalid', "Remote name format invalid");
				} else if (repository.remotes.find(r => r.name === name)) {
					return localize('remote already exists', "Remote '{0}' already exists.", name);
O
Omkar Manjrekar 已提交
2209
				}
J
João Moreno 已提交
2210 2211

				return null;
O
Omkar Manjrekar 已提交
2212
			}
O
Omkar Manjrekar 已提交
2213
		});
O
Omkar Manjrekar 已提交
2214

J
João Moreno 已提交
2215
		const name = sanitizeRemoteName(resultName || '');
O
Omkar Manjrekar 已提交
2216

O
Omkar Manjrekar 已提交
2217 2218 2219 2220
		if (!name) {
			return;
		}

O
Omkar Manjrekar 已提交
2221
		await repository.addRemote(name, url);
2222
		return name;
O
Omkar Manjrekar 已提交
2223 2224
	}

O
Omkar Manjrekar 已提交
2225 2226
	@command('git.removeRemote', { repository: true })
	async removeRemote(repository: Repository): Promise<void> {
O
Omkar Manjrekar 已提交
2227 2228 2229
		const remotes = repository.remotes;

		if (remotes.length === 0) {
2230
			window.showErrorMessage(localize('no remotes added', "Your repository has no remotes."));
O
Omkar Manjrekar 已提交
2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241
			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 已提交
2242

O
Omkar Manjrekar 已提交
2243
		await repository.removeRemote(remoteName);
O
Omkar Manjrekar 已提交
2244
	}
O
Omkar Manjrekar 已提交
2245

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

2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259
		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 已提交
2260 2261 2262
			return;
		}

J
Joao Moreno 已提交
2263 2264 2265 2266
		const remoteName = HEAD.remote || HEAD.upstream.remote;
		const remote = repository.remotes.find(r => r.name === remoteName);
		const isReadonly = remote && remote.isReadOnly;

J
Joao Moreno 已提交
2267
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
2268
		const shouldPrompt = !isReadonly && config.get<boolean>('confirmSync') === true;
J
Joao Moreno 已提交
2269 2270

		if (shouldPrompt) {
J
Joao Moreno 已提交
2271
			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 已提交
2272
			const yes = localize('ok', "OK");
B
Benjamin Pasero 已提交
2273
			const neverAgain = localize('never again', "OK, Don't Show Again");
J
Joao Moreno 已提交
2274 2275 2276 2277 2278 2279 2280 2281 2282
			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 已提交
2283
		if (rebase) {
J
Joao Moreno 已提交
2284
			await repository.syncRebase(HEAD);
J
Joao Moreno 已提交
2285
		} else {
J
Joao Moreno 已提交
2286
			await repository.sync(HEAD);
J
Joao Moreno 已提交
2287 2288 2289 2290
		}
	}

	@command('git.sync', { repository: true })
2291 2292 2293 2294 2295 2296 2297 2298 2299 2300
	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 已提交
2301 2302
	}

J
Joao 已提交
2303 2304 2305 2306 2307 2308 2309 2310 2311
	@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 已提交
2312
			await repository.sync(HEAD);
J
Joao 已提交
2313 2314 2315
		}));
	}

2316
	@command('git.syncRebase', { repository: true })
2317 2318 2319 2320 2321 2322 2323 2324 2325 2326
	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 已提交
2327 2328
	}

J
Joao Moreno 已提交
2329
	@command('git.publish', { repository: true })
J
Joao Moreno 已提交
2330
	async publish(repository: Repository): Promise<void> {
J
João Moreno 已提交
2331
		const branchName = repository.HEAD && repository.HEAD.name || '';
J
Joao Moreno 已提交
2332
		const remotes = repository.remotes;
J
Joao Moreno 已提交
2333 2334

		if (remotes.length === 0) {
J
João Moreno 已提交
2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359
			const providers = this.model.getRemoteProviders().filter(p => !!p.publishRepository);

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

			let provider: RemoteSourceProvider;

			if (providers.length === 1) {
				provider = providers[0];
			} else {
				const picks = providers
					.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('publish to', "Publish to {0}", provider.name), alwaysShow: true, provider }));
				const placeHolder = localize('pick provider', "Pick a provider to publish the branch '{0}' to:", branchName);
				const choice = await window.showQuickPick(picks, { placeHolder });

				if (!choice) {
					return;
				}

				provider = choice.provider;
			}

			await provider.publishRepository!(new ApiRepository(repository));
J
Joao Moreno 已提交
2360 2361 2362
			return;
		}

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

2367
		const addRemote = new AddRemoteItem(this);
J
Joao Moreno 已提交
2368
		const picks = [...repository.remotes.map(r => ({ label: r.name, description: r.pushUrl })), addRemote];
2369 2370
		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 已提交
2371 2372 2373 2374 2375

		if (!choice) {
			return;
		}

2376 2377 2378 2379
		if (choice === addRemote) {
			const newRemote = await this.addRemote(repository);

			if (newRemote) {
J
Joao Moreno 已提交
2380
				await repository.pushTo(newRemote, branchName, true);
2381
			}
J
Joao Moreno 已提交
2382 2383
		} else {
			await repository.pushTo(choice.label, branchName, true);
2384
		}
J
Joao Moreno 已提交
2385 2386
	}

2387 2388
	@command('git.ignore')
	async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
2389 2390
		resourceStates = resourceStates.filter(s => !!s);

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

2394
			if (!resource) {
J
Joao Moreno 已提交
2395 2396 2397
				return;
			}

2398
			resourceStates = [resource];
J
Joao Moreno 已提交
2399 2400
		}

2401
		const resources = resourceStates
J
Joao Moreno 已提交
2402 2403 2404
			.filter(s => s instanceof Resource)
			.map(r => r.resourceUri);

2405
		if (!resources.length) {
N
NKumar2 已提交
2406 2407 2408
			return;
		}

2409
		await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
N
NKumar2 已提交
2410 2411
	}

J
Joao Moreno 已提交
2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424
	@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 已提交
2425
	private async _stash(repository: Repository, includeUntracked = false): Promise<void> {
2426 2427
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0
			&& (!includeUntracked || repository.untrackedGroup.resourceStates.length === 0);
2428
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
2429

2430
		if (noUnstagedChanges && noStagedChanges) {
K
Krzysztof Cieślak 已提交
2431 2432 2433
			window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
			return;
		}
J
Joao Moreno 已提交
2434

2435
		const message = await this.getStashMessage();
J
Joao Moreno 已提交
2436 2437 2438 2439 2440

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

J
Joao Moreno 已提交
2441
		await repository.createStash(message, includeUntracked);
K
Krzysztof Cieślak 已提交
2442 2443
	}

2444 2445 2446 2447 2448 2449 2450
	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 已提交
2451 2452 2453 2454 2455 2456 2457 2458 2459 2460
	@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 已提交
2461
	@command('git.stashPop', { repository: true })
J
Joao Moreno 已提交
2462
	async stashPop(repository: Repository): Promise<void> {
2463
		const placeHolder = localize('pick stash to pop', "Pick a stash to pop");
J
Joao Moreno 已提交
2464
		const stash = await this.pickStash(repository, placeHolder);
2465

J
Joao Moreno 已提交
2466
		if (!stash) {
2467 2468 2469
			return;
		}

J
Joao Moreno 已提交
2470
		await repository.popStash(stash.index);
2471 2472 2473 2474
	}

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

J
Joao Moreno 已提交
2477 2478
		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
K
Krzysztof Cieślak 已提交
2479 2480 2481
			return;
		}

2482 2483 2484 2485 2486 2487
		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 已提交
2488
		const stash = await this.pickStash(repository, placeHolder);
K
Krzysztof Cieślak 已提交
2489

J
Joao Moreno 已提交
2490
		if (!stash) {
K
Krzysztof Cieślak 已提交
2491 2492
			return;
		}
J
Joao Moreno 已提交
2493

J
Joao Moreno 已提交
2494
		await repository.applyStash(stash.index);
K
Krzysztof Cieślak 已提交
2495 2496
	}

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

J
Joao Moreno 已提交
2501 2502
		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
2503 2504 2505 2506 2507 2508
			return;
		}

		await repository.applyStash();
	}

2509 2510
	@command('git.stashDrop', { repository: true })
	async stashDrop(repository: Repository): Promise<void> {
2511 2512 2513 2514 2515 2516 2517 2518 2519 2520
		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 已提交
2521
	private async pickStash(repository: Repository, placeHolder: string): Promise<Stash | undefined> {
2522 2523
		const stashes = await repository.getStashes();

J
Joao Moreno 已提交
2524 2525
		if (stashes.length === 0) {
			window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
K
Krzysztof Cieślak 已提交
2526 2527 2528
			return;
		}

J
Joao Moreno 已提交
2529 2530 2531
		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 已提交
2532
	}
K
Krzysztof Cieślak 已提交
2533

2534 2535
	@command('git.timeline.openDiff', { repository: false })
	async timelineOpenDiff(item: TimelineItem, uri: Uri | undefined, _source: string) {
E
Eric Amodio 已提交
2536
		if (uri === undefined || uri === null || !GitTimelineItem.is(item)) {
2537 2538 2539
			return undefined;
		}

E
Eric Amodio 已提交
2540 2541
		const basename = path.basename(uri.fsPath);

2542
		let title;
2543
		if ((item.previousRef === 'HEAD' || item.previousRef === '~') && item.ref === '') {
E
Eric Amodio 已提交
2544
			title = localize('git.title.workingTree', '{0} (Working Tree)', basename);
2545
		}
2546
		else if (item.previousRef === 'HEAD' && item.ref === '~') {
E
Eric Amodio 已提交
2547
			title = localize('git.title.index', '{0} (Index)', basename);
2548
		} else {
2549
			title = localize('git.title.diffRefs', '{0} ({1}) ⟷ {0} ({2})', basename, item.shortPreviousRef, item.shortRef);
2550 2551
		}

E
Eric Amodio 已提交
2552 2553 2554 2555 2556 2557 2558
		const options: TextDocumentShowOptions = {
			preserveFocus: true,
			preview: true,
			viewColumn: ViewColumn.Active
		};

		return commands.executeCommand('vscode.diff', toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title, options);
2559 2560 2561 2562 2563 2564
	}

	@command('git.timeline.copyCommitId', { repository: false })
	async timelineCopyCommitId(item: TimelineItem, _uri: Uri | undefined, _source: string) {
		if (!GitTimelineItem.is(item)) {
			return;
2565 2566
		}

2567
		env.clipboard.writeText(item.ref);
2568 2569
	}

2570 2571 2572 2573 2574 2575 2576 2577 2578
	@command('git.timeline.copyCommitMessage', { repository: false })
	async timelineCopyCommitMessage(item: TimelineItem, _uri: Uri | undefined, _source: string) {
		if (!GitTimelineItem.is(item)) {
			return;
		}

		env.clipboard.writeText(item.message);
	}

2579 2580 2581 2582
	@command('git.rebaseAbort', { repository: true })
	async rebaseAbort(repository: Repository): Promise<void> {
		await repository.rebaseAbort();
	}
2583

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

J
Joao Moreno 已提交
2588
			if (!options.repository) {
J
Joao Moreno 已提交
2589 2590
				result = Promise.resolve(method.apply(this, args));
			} else {
J
Joao Moreno 已提交
2591 2592
				// try to guess the repository based on the first argument
				const repository = this.model.getRepository(args[0]);
J
Joao Moreno 已提交
2593 2594 2595 2596 2597 2598 2599 2600 2601
				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();
				}
2602

J
Joao Moreno 已提交
2603
				result = repositoryPromise.then(repository => {
J
Joao Moreno 已提交
2604
					if (!repository) {
J
Joao 已提交
2605
						return Promise.resolve();
J
Joao Moreno 已提交
2606 2607
					}

J
Joao Moreno 已提交
2608
					return Promise.resolve(method.apply(this, [repository, ...args]));
J
Joao Moreno 已提交
2609
				});
J
Joao Moreno 已提交
2610 2611
			}

K
kieferrm 已提交
2612
			/* __GDPR__
K
kieferrm 已提交
2613 2614 2615 2616
				"git.command" : {
					"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
J
Joao Moreno 已提交
2617 2618
			this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

J
Joao Moreno 已提交
2619
			return result.catch(async err => {
J
Joao Moreno 已提交
2620
				const options: MessageOptions = {
2621
					modal: true
J
Joao Moreno 已提交
2622 2623
				};

J
Joao Moreno 已提交
2624
				let message: string;
2625
				let type: 'error' | 'warning' = 'error';
J
Joao Moreno 已提交
2626

J
Joao Moreno 已提交
2627 2628 2629 2630 2631
				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 已提交
2632
				switch (err.gitErrorCode) {
2633
					case GitErrorCodes.DirtyWorkTree:
J
Joao Moreno 已提交
2634 2635
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
2636
					case GitErrorCodes.PushRejected:
J
Joao Moreno 已提交
2637
						message = localize('cant push', "Can't push refs to remote. Try running 'Pull' first to integrate your changes.");
2638
						break;
2639 2640 2641 2642 2643
					case GitErrorCodes.Conflict:
						message = localize('merge conflicts', "There are merge conflicts. Resolve them before committing.");
						type = 'warning';
						options.modal = false;
						break;
J
Joao Moreno 已提交
2644 2645 2646 2647 2648
					case GitErrorCodes.StashConflict:
						message = localize('stash merge conflicts', "There were merge conflicts while applying the stash.");
						type = 'warning';
						options.modal = false;
						break;
2649 2650 2651 2652 2653 2654 2655 2656
					case GitErrorCodes.AuthenticationFailed:
						const regex = /Authentication failed for '(.*)'/i;
						const match = regex.exec(err.stderr || String(err));

						message = match
							? localize('auth failed specific', "Failed to authenticate to git remote:\n\n{0}", match[1])
							: localize('auth failed', "Failed to authenticate to git remote.");
						break;
2657 2658
					case GitErrorCodes.NoUserNameConfigured:
					case GitErrorCodes.NoUserEmailConfigured:
J
Joao Moreno 已提交
2659 2660
						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')));
2661
						break;
J
Joao Moreno 已提交
2662
					default:
2663 2664 2665
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
J
Joao Moreno 已提交
2666
							.split(/[\r\n]/)
J
João Moreno 已提交
2667
							.filter((line: string) => !!line)
2668 2669 2670 2671 2672
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
2673 2674 2675 2676 2677 2678 2679 2680 2681

						break;
				}

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

J
Joao Moreno 已提交
2682 2683 2684 2685 2686 2687 2688
				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 已提交
2689

J
Joao Moreno 已提交
2690 2691 2692
					if (resultFn) {
						resultFn();
					}
J
Joao Moreno 已提交
2693 2694 2695
				}
			});
		};
2696 2697

		// patch this object, so people can call methods directly
2698
		(this as any)[key] = result;
2699 2700

		return result;
J
Joao Moreno 已提交
2701 2702
	}

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

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

J
Joao Moreno 已提交
2708 2709 2710
		for (const r of this.model.repositories.map(r => r.root)) {
			this.outputChannel.appendLine(`repo root ${r}`);
		}
J
Joao Moreno 已提交
2711

J
Joao Moreno 已提交
2712
		if (!uri) {
2713
			return undefined;
J
Joao Moreno 已提交
2714 2715
		}

J
Joao Moreno 已提交
2716
		if (isGitUri(uri)) {
J
Joao Moreno 已提交
2717 2718
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
2719 2720 2721 2722
		}

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

J
Joao Moreno 已提交
2725
			if (!repository) {
2726 2727
				return undefined;
			}
J
Joao Moreno 已提交
2728

J
Joao Moreno 已提交
2729 2730
			return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
				|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
J
Joao Moreno 已提交
2731
		}
2732
		return undefined;
J
Joao Moreno 已提交
2733 2734
	}

J
Joao Moreno 已提交
2735
	private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;
J
Joao Moreno 已提交
2736 2737 2738 2739 2740 2741
	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) => {
2742
			let repository = this.model.getRepository(resource);
J
Joao Moreno 已提交
2743 2744 2745 2746 2747 2748

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

2749
			// Could it be a submodule?
2750
			if (pathEquals(resource.fsPath, repository.root)) {
2751 2752 2753
				repository = this.model.getRepositoryForSubmodule(resource) || repository;
			}

J
Joao Moreno 已提交
2754
			const tuple = result.filter(p => p.repository === repository)[0];
J
Joao Moreno 已提交
2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770

			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 已提交
2771 2772 2773
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
João Moreno 已提交
2774
}