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

'use strict';

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

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

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

	protected get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); }
	protected get treeish(): string | undefined { return this.ref.name; }
	get label(): string { return this.ref.name || this.shortCommit; }
	get description(): string { return this.shortCommit; }

J
Joao Moreno 已提交
29
	constructor(protected ref: Ref) { }
J
Joao Moreno 已提交
30

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

		if (!ref) {
			return;
		}

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

class CheckoutTagItem extends CheckoutItem {

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

class CheckoutRemoteHeadItem extends CheckoutItem {

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

	protected get treeish(): string | undefined {
		if (!this.ref.name) {
			return;
		}

		const match = /^[^/]+\/(.*)$/.exec(this.ref.name);
		return match ? match[1] : this.ref.name;
	}
}

M
Maik Riechert 已提交
65 66
class BranchDeleteItem implements QuickPickItem {

67 68 69
	private get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); }
	get branchName(): string | undefined { return this.ref.name; }
	get label(): string { return this.branchName || ''; }
M
Maik Riechert 已提交
70 71
	get description(): string { return this.shortCommit; }

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

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

82 83 84 85 86 87
class MergeItem implements QuickPickItem {

	get label(): string { return this.ref.name || ''; }
	get description(): string { return this.ref.name || ''; }

	constructor(protected ref: Ref) { }
J
Joao Moreno 已提交
88

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

180 181 182
		// 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 已提交
183 184 185
			opts.selection = activeTextEditor.selection;
		}

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

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

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

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

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

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

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

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

				if (!repository) {
					return;
				}

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

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

J
Joao Moreno 已提交
238
				return resource.resourceUri;
J
Joao Moreno 已提交
239

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

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

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

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

		return '';
	}

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

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

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

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

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

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

J
Joao Moreno 已提交
304

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

309 310 311 312 313 314
			const repositoryPath = await clonePromise;

			const open = localize('openrepo', "Open Repository");
			const result = await window.showInformationMessage(localize('proposeopen', "Would you like to open the cloned repository?"), open);

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

J
Joao Moreno 已提交
345
	@command('git.init')
J
Joao Moreno 已提交
346
	async init(): Promise<void> {
J
Joao Moreno 已提交
347 348 349 350 351 352 353 354 355 356 357
		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 已提交
358 359
		});

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

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

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

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

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

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

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

389
		let uris: Uri[] | undefined;
390 391 392

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

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

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

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

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

423 424 425
			// 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) {
B
Benjamin Pasero 已提交
426 427 428
				opts.selection = activeTextEditor.selection;
			}

429 430
			const document = await workspace.openTextDocument(uri);
			await window.showTextDocument(document, opts);
J
Joao Moreno 已提交
431
		}
J
Joao Moreno 已提交
432 433
	}

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

		if (arg instanceof Resource) {
			resource = arg;
		} else if (arg instanceof Uri) {
441
			resource = this.getSCMResource(arg);
D
Duroktar 已提交
442
		} else {
443
			resource = this.getSCMResource();
D
Duroktar 已提交
444 445 446 447 448 449
		}

		if (!resource) {
			return;
		}

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

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

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

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

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

474 475 476
			if (arg instanceof Resource) {
				resource = arg;
			} else {
477
				resource = this.getSCMResource();
478 479 480 481 482
			}

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

485
		if (!resources) {
J
Joao Moreno 已提交
486
			return;
J
Joao Moreno 已提交
487
		}
J
Joao Moreno 已提交
488

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

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

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

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

523 524 525 526 527 528 529 530
			const yes = localize('yes', "Yes");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

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

534
		if (!scmResources.length) {
J
Joao Moreno 已提交
535 536
			return;
		}
J
Joao Moreno 已提交
537

538
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
539
		await this.runByRepository(resources, async (repository, resources) => repository.add(resources));
J
Joao Moreno 已提交
540 541
	}

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

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

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

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

J
Joao Moreno 已提交
560
		await repository.add([]);
J
Joao Moreno 已提交
561 562
	}

J
Joao Moreno 已提交
563
	@command('git.stageChange')
J
Joao Moreno 已提交
564 565 566 567 568 569 570 571
	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 已提交
572 573
	}

J
Joao Moreno 已提交
574
	@command('git.stageSelectedRanges', { diff: true })
J
Joao Moreno 已提交
575
	async stageSelectedChanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
576 577 578 579 580 581 582
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
J
Joao Moreno 已提交
583 584 585 586
		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 已提交
587

J
Joao Moreno 已提交
588
		if (!selectedChanges.length) {
J
Joao Moreno 已提交
589 590 591
			return;
		}

J
Joao Moreno 已提交
592 593
		await this._stageChanges(textEditor, selectedChanges);
	}
J
Joao Moreno 已提交
594

J
Joao Moreno 已提交
595 596 597 598 599
	private async _stageChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
		const modifiedDocument = textEditor.document;
		const modifiedUri = modifiedDocument.uri;

		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
600 601 602
			return;
		}

J
Joao Moreno 已提交
603 604 605
		const originalUri = toGitUri(modifiedUri, '~');
		const originalDocument = await workspace.openTextDocument(originalUri);
		const result = applyLineChanges(originalDocument, modifiedDocument, changes);
606

J
Joao Moreno 已提交
607
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
608
	}
J
Joao Moreno 已提交
609

J
Joao Moreno 已提交
610
	@command('git.revertChange')
J
Joao Moreno 已提交
611 612 613 614 615 616 617 618
	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) {
			return;
		}

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

J
Joao Moreno 已提交
621
	@command('git.revertSelectedRanges', { diff: true })
J
Joao Moreno 已提交
622
	async revertSelectedRanges(changes: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
623 624 625 626 627 628 629
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

		const modifiedDocument = textEditor.document;
J
Joao Moreno 已提交
630 631 632 633 634
		const selections = textEditor.selections;
		const selectedChanges = changes.filter(change => {
			const modifiedRange = change.modifiedEndLineNumber === 0
				? new Range(modifiedDocument.lineAt(change.modifiedStartLineNumber - 1).range.end, modifiedDocument.lineAt(change.modifiedStartLineNumber).range.start)
				: new Range(modifiedDocument.lineAt(change.modifiedStartLineNumber - 1).range.start, modifiedDocument.lineAt(change.modifiedEndLineNumber - 1).range.end);
J
Joao Moreno 已提交
635

J
Joao Moreno 已提交
636 637 638 639
			return selections.every(selection => !selection.intersection(modifiedRange));
		});

		if (selectedChanges.length === changes.length) {
J
Joao Moreno 已提交
640 641 642
			return;
		}

J
Joao Moreno 已提交
643 644
		await this._revertChanges(textEditor, selectedChanges);
	}
J
Joao Moreno 已提交
645

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

J
Joao Moreno 已提交
650
		if (modifiedUri.scheme !== 'file') {
J
Joao Moreno 已提交
651 652 653
			return;
		}

J
Joao Moreno 已提交
654 655
		const originalUri = toGitUri(modifiedUri, '~');
		const originalDocument = await workspace.openTextDocument(originalUri);
J
Joao Moreno 已提交
656 657 658 659 660 661 662 663 664
		const basename = path.basename(modifiedUri.fsPath);
		const message = localize('confirm revert', "Are you sure you want to revert the selected changes in {0}?", basename);
		const yes = localize('revert', "Revert Changes");
		const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

J
Joao Moreno 已提交
665
		const result = applyLineChanges(originalDocument, modifiedDocument, changes);
J
Joao Moreno 已提交
666 667 668
		const edit = new WorkspaceEdit();
		edit.replace(modifiedUri, new Range(new Position(0, 0), modifiedDocument.lineAt(modifiedDocument.lineCount - 1).range.end), result);
		workspace.applyEdit(edit);
J
Joao Moreno 已提交
669
		await modifiedDocument.save();
J
Joao Moreno 已提交
670 671
	}

J
Joao Moreno 已提交
672 673
	@command('git.unstage')
	async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
674
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
675
			const resource = this.getSCMResource();
676 677 678 679 680 681 682 683

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

687
		if (!scmResources.length) {
J
Joao Moreno 已提交
688 689 690
			return;
		}

691
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
692
		await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
J
Joao Moreno 已提交
693 694
	}

J
Joao Moreno 已提交
695 696
	@command('git.unstageAll', { repository: true })
	async unstageAll(repository: Repository): Promise<void> {
697
		await repository.revert([]);
J
Joao Moreno 已提交
698
	}
J
Joao Moreno 已提交
699

J
Joao Moreno 已提交
700 701
	@command('git.unstageSelectedRanges', { diff: true })
	async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
J
Joao Moreno 已提交
702 703 704 705 706 707 708 709 710
		const textEditor = window.activeTextEditor;

		if (!textEditor) {
			return;
		}

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

711 712 713 714 715 716 717
		if (modifiedUri.scheme !== 'git') {
			return;
		}

		const { ref } = fromGitUri(modifiedUri);

		if (ref !== '') {
J
Joao Moreno 已提交
718 719 720
			return;
		}

J
Joao Moreno 已提交
721
		const originalUri = toGitUri(modifiedUri, 'HEAD');
J
Joao Moreno 已提交
722
		const originalDocument = await workspace.openTextDocument(originalUri);
723 724 725 726
		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 已提交
727 728 729 730 731

		if (!selectedDiffs.length) {
			return;
		}

732 733
		const invertedDiffs = selectedDiffs.map(invertLineChange);
		const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
J
Joao Moreno 已提交
734

J
Joao Moreno 已提交
735
		await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
J
Joao Moreno 已提交
736 737
	}

J
Joao Moreno 已提交
738 739
	@command('git.clean')
	async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
J
Joao Moreno 已提交
740
		if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
741
			const resource = this.getSCMResource();
742 743 744 745 746 747 748 749

			if (!resource) {
				return;
			}

			resourceStates = [resource];
		}

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

J
Joao Moreno 已提交
753
		if (!scmResources.length) {
J
Joao Moreno 已提交
754 755
			return;
		}
J
Joao Moreno 已提交
756

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

J
Joao Moreno 已提交
761
		if (scmResources.length === 1) {
J
Joao Moreno 已提交
762
			if (untrackedCount > 0) {
J
Joao Moreno 已提交
763
				message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
764 765
				yes = localize('delete file', "Delete file");
			} else {
J
Joao Moreno 已提交
766
				message = localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(scmResources[0].resourceUri.fsPath));
J
Joao Moreno 已提交
767 768
			}
		} else {
J
Joao Moreno 已提交
769
			message = localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", scmResources.length);
J
Joao Moreno 已提交
770 771 772 773 774

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

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

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

J
Joao Moreno 已提交
782
		const resources = scmResources.map(r => r.resourceUri);
J
Joao Moreno 已提交
783
		await this.runByRepository(resources, async (repository, resources) => repository.clean(resources));
J
Joao Moreno 已提交
784
	}
J
Joao Moreno 已提交
785

J
Joao Moreno 已提交
786 787
	@command('git.cleanAll', { repository: true })
	async cleanAll(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
788
		let resources = repository.workingTreeGroup.resourceStates;
J
Joao Moreno 已提交
789 790 791 792 793

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

J
Joao Moreno 已提交
794 795
		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 已提交
796

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

J
Joao Moreno 已提交
806
			if (pick !== yes) {
J
Joao Moreno 已提交
807 808 809
				return;
			}

J
Joao 已提交
810
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
811 812 813 814 815 816 817 818
			return;
		} else if (resources.length === 1) {
			const message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(resources[0].resourceUri.fsPath));
			const yes = localize('delete file', "Delete file");
			const pick = await window.showWarningMessage(message, { modal: true }, yes);

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

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

J
Joao Moreno 已提交
827 828 829
			if (pick !== yes) {
				return;
			}
J
Joao Moreno 已提交
830

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

J
Joao Moreno 已提交
833 834 835 836
		} 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 已提交
837

J
Joao Moreno 已提交
838 839 840 841 842
			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 已提交
843

J
Joao Moreno 已提交
844
			const yesAll = localize('discardAll', "Discard All {0} Files", resources.length);
J
Joao Moreno 已提交
845 846 847 848 849 850 851 852
			const pick = await window.showWarningMessage(message, { modal: true }, yesTracked, yesAll);

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

J
Joao 已提交
853
			await repository.clean(resources.map(r => r.resourceUri));
J
Joao Moreno 已提交
854
		}
J
Joao Moreno 已提交
855 856
	}

J
Joao Moreno 已提交
857
	private async smartCommit(
J
Joao Moreno 已提交
858
		repository: Repository,
859
		getCommitMessage: () => Promise<string | undefined>,
J
Joao Moreno 已提交
860 861
		opts?: CommitOptions
	): Promise<boolean> {
862 863
		const config = workspace.getConfiguration('git');
		const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
864
		const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
J
Joao Moreno 已提交
865 866
		const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
		const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
867 868

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

J
Joao Moreno 已提交
871 872
			// 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?");
873 874 875 876 877
			const yes = localize('yes', "Yes");
			const always = localize('always', "Always");
			const pick = await window.showWarningMessage(message, { modal: true }, yes, always);

			if (pick === always) {
J
Joao Moreno 已提交
878 879 880
				config.update('enableSmartCommit', true, true);
			} else if (pick !== yes) {
				return false; // do not commit on cancel
881 882 883
			}
		}

J
Joao Moreno 已提交
884
		if (!opts) {
885
			opts = { all: noStagedChanges };
J
Joao Moreno 已提交
886 887
		}

888 889 890
		// enable signing of commits if configurated
		opts.signCommit = enableCommitSigning;

J
Joao Moreno 已提交
891 892
		if (
			// no changes
893
			(noStagedChanges && noUnstagedChanges)
J
Joao Moreno 已提交
894
			// or no staged changes and not `all`
895
			|| (!opts.all && noStagedChanges)
J
Joao Moreno 已提交
896
		) {
J
Joao Moreno 已提交
897 898 899 900
			window.showInformationMessage(localize('no changes', "There are no changes to commit."));
			return false;
		}

J
Joao Moreno 已提交
901
		const message = await getCommitMessage();
J
Joao Moreno 已提交
902 903 904 905 906

		if (!message) {
			return false;
		}

J
Joao Moreno 已提交
907
		await repository.commit(message, opts);
J
Joao Moreno 已提交
908 909 910 911

		return true;
	}

J
Joao Moreno 已提交
912
	private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
J
Joao Moreno 已提交
913
		const message = repository.inputBox.value;
J
Joao Moreno 已提交
914
		const getCommitMessage = async () => {
J
Joao Moreno 已提交
915 916 917 918 919 920
			if (message) {
				return message;
			}

			return await window.showInputBox({
				placeHolder: localize('commit message', "Commit message"),
J
Joao Moreno 已提交
921 922
				prompt: localize('provide commit message', "Please provide a commit message"),
				ignoreFocusOut: true
J
Joao Moreno 已提交
923
			});
J
Joao Moreno 已提交
924 925
		};

J
Joao Moreno 已提交
926
		const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
J
Joao Moreno 已提交
927 928

		if (message && didCommit) {
J
Joao Moreno 已提交
929
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
930
		}
J
Joao Moreno 已提交
931 932
	}

J
Joao Moreno 已提交
933
	@command('git.commit', { repository: true })
J
Joao Moreno 已提交
934 935
	async commit(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository);
J
Joao Moreno 已提交
936 937
	}

J
Joao Moreno 已提交
938
	@command('git.commitWithInput', { repository: true })
J
Joao Moreno 已提交
939
	async commitWithInput(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
940
		if (!repository.inputBox.value) {
J
Joao Moreno 已提交
941 942 943
			return;
		}

J
Joao Moreno 已提交
944
		const didCommit = await this.smartCommit(repository, async () => repository.inputBox.value);
J
Joao Moreno 已提交
945 946

		if (didCommit) {
J
Joao Moreno 已提交
947
			repository.inputBox.value = await repository.getCommitTemplate();
J
Joao Moreno 已提交
948
		}
J
Joao Moreno 已提交
949 950
	}

J
Joao Moreno 已提交
951
	@command('git.commitStaged', { repository: true })
J
Joao Moreno 已提交
952 953
	async commitStaged(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false });
J
Joao Moreno 已提交
954 955
	}

J
Joao Moreno 已提交
956
	@command('git.commitStagedSigned', { repository: true })
J
Joao Moreno 已提交
957 958
	async commitStagedSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, signoff: true });
J
Joao Moreno 已提交
959 960
	}

J
Joao Moreno 已提交
961
	@command('git.commitStagedAmend', { repository: true })
J
Joao Moreno 已提交
962 963
	async commitStagedAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: false, amend: true });
K
Krzysztof Cieślak 已提交
964 965
	}

J
Joao Moreno 已提交
966
	@command('git.commitAll', { repository: true })
J
Joao Moreno 已提交
967 968
	async commitAll(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true });
J
Joao Moreno 已提交
969 970
	}

J
Joao Moreno 已提交
971
	@command('git.commitAllSigned', { repository: true })
J
Joao Moreno 已提交
972 973
	async commitAllSigned(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, signoff: true });
J
Joao Moreno 已提交
974 975
	}

J
Joao Moreno 已提交
976
	@command('git.commitAllAmend', { repository: true })
J
Joao Moreno 已提交
977 978
	async commitAllAmend(repository: Repository): Promise<void> {
		await this.commitWithAnyInput(repository, { all: true, amend: true });
K
Krzysztof Cieślak 已提交
979 980
	}

J
Joao Moreno 已提交
981
	@command('git.undoCommit', { repository: true })
J
Joao Moreno 已提交
982 983
	async undoCommit(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
984 985 986 987 988

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

J
Joao Moreno 已提交
989 990
		const commit = await repository.getCommit('HEAD');
		await repository.reset('HEAD~');
J
Joao Moreno 已提交
991
		repository.inputBox.value = commit.message;
J
Joao Moreno 已提交
992 993
	}

J
Joao Moreno 已提交
994
	@command('git.checkout', { repository: true })
J
Joao Moreno 已提交
995
	async checkout(repository: Repository, treeish: string): Promise<void> {
J
Joao Moreno 已提交
996
		if (typeof treeish === 'string') {
J
Joao Moreno 已提交
997
			return await repository.checkout(treeish);
J
Joao Moreno 已提交
998 999
		}

J
Joao Moreno 已提交
1000
		const config = workspace.getConfiguration('git');
J
Joao Moreno 已提交
1001
		const checkoutType = config.get<string>('checkoutType') || 'all';
J
Joao Moreno 已提交
1002 1003 1004
		const includeTags = checkoutType === 'all' || checkoutType === 'tags';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1005
		const createBranch = new CreateBranchItem(this);
J
Joao Moreno 已提交
1006

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

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

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

J
Joao Moreno 已提交
1016
		const picks = [createBranch, ...heads, ...tags, ...remoteHeads];
1017
		const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
J
Joao Moreno 已提交
1018
		const choice = await window.showQuickPick(picks, { placeHolder });
J
Joao Moreno 已提交
1019 1020 1021 1022 1023

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1024
		await choice.run(repository);
J
Joao Moreno 已提交
1025 1026
	}

J
Joao Moreno 已提交
1027
	@command('git.branch', { repository: true })
J
Joao Moreno 已提交
1028
	async branch(repository: Repository): Promise<void> {
J
Joao Moreno 已提交
1029
		const result = await window.showInputBox({
J
Joao Moreno 已提交
1030
			placeHolder: localize('branch name', "Branch name"),
J
Joao Moreno 已提交
1031 1032
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true
J
Joao Moreno 已提交
1033
		});
J
Joao Moreno 已提交
1034

J
Joao Moreno 已提交
1035 1036 1037
		if (!result) {
			return;
		}
J
Joao Moreno 已提交
1038

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

J
Joao Moreno 已提交
1043
	@command('git.deleteBranch', { repository: true })
J
Joao Moreno 已提交
1044
	async deleteBranch(repository: Repository, name: string, force?: boolean): Promise<void> {
1045 1046
		let run: (force?: boolean) => Promise<void>;
		if (typeof name === 'string') {
J
Joao Moreno 已提交
1047
			run = force => repository.deleteBranch(name, force);
1048
		} else {
J
Joao Moreno 已提交
1049 1050
			const currentHead = repository.HEAD && repository.HEAD.name;
			const heads = repository.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead)
1051
				.map(ref => new BranchDeleteItem(ref));
M
Maik Riechert 已提交
1052

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

M
Maik Riechert 已提交
1056
			if (!choice || !choice.branchName) {
1057 1058
				return;
			}
M
Maik Riechert 已提交
1059
			name = choice.branchName;
J
Joao Moreno 已提交
1060
			run = force => choice.run(repository, force);
M
Maik Riechert 已提交
1061 1062
		}

1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077
		try {
			await run(force);
		} catch (err) {
			if (err.gitErrorCode !== GitErrorCodes.BranchNotFullyMerged) {
				throw err;
			}

			const message = localize('confirm force delete branch', "The branch '{0}' is not fully merged. Delete anyway?", name);
			const yes = localize('delete branch', "Delete Branch");
			const pick = await window.showWarningMessage(message, yes);

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

J
Joao Moreno 已提交
1080
	@command('git.merge', { repository: true })
J
Joao Moreno 已提交
1081
	async merge(repository: Repository): Promise<void> {
1082 1083 1084 1085
		const config = workspace.getConfiguration('git');
		const checkoutType = config.get<string>('checkoutType') || 'all';
		const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';

J
Joao Moreno 已提交
1086
		const heads = repository.refs.filter(ref => ref.type === RefType.Head)
J
Joao Moreno 已提交
1087 1088
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1089

J
Joao Moreno 已提交
1090
		const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
J
Joao Moreno 已提交
1091 1092
			.filter(ref => ref.name || ref.commit)
			.map(ref => new MergeItem(ref as Branch));
1093 1094

		const picks = [...heads, ...remoteHeads];
1095 1096
		const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
		const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
1097 1098 1099 1100 1101

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1102
		try {
J
Joao Moreno 已提交
1103
			await choice.run(repository);
J
Joao Moreno 已提交
1104 1105 1106 1107 1108 1109 1110 1111
		} catch (err) {
			if (err.gitErrorCode !== GitErrorCodes.Conflict) {
				throw err;
			}

			const message = localize('merge conflicts', "There are merge conflicts. Resolve them before committing.");
			await window.showWarningMessage(message);
		}
1112 1113
	}

J
Joao Moreno 已提交
1114
	@command('git.createTag', { repository: true })
J
Joao Moreno 已提交
1115
	async createTag(repository: Repository): Promise<void> {
1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127
		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 已提交
1128
			prompt: localize('provide tag message', "Please provide a message to annotate the tag"),
1129 1130 1131 1132 1133
			ignoreFocusOut: true
		});

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

J
Joao Moreno 已提交
1137
	@command('git.pullFrom', { repository: true })
J
Joao Moreno 已提交
1138 1139
	async pullFrom(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163

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

		const picks = remotes.map(r => ({ label: r.name, description: r.url }));
		const placeHolder = localize('pick remote pull repo', "Pick a remote to pull the branch from");
		const pick = await window.showQuickPick(picks, { placeHolder });

		if (!pick) {
			return;
		}

		const branchName = await window.showInputBox({
			placeHolder: localize('branch name', "Branch name"),
			prompt: localize('provide branch name', "Please provide a branch name"),
			ignoreFocusOut: true
		});

		if (!branchName) {
			return;
		}

J
Joao Moreno 已提交
1164
		repository.pull(false, pick.label, branchName);
1165 1166
	}

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

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

J
Joao Moreno 已提交
1176
		await repository.pull();
J
Joao Moreno 已提交
1177 1178
	}

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

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

J
Joao Moreno 已提交
1188
		await repository.pullWithRebase();
J
Joao Moreno 已提交
1189 1190
	}

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

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

J
Joao Moreno 已提交
1200
		await repository.push();
J
Joao Moreno 已提交
1201 1202
	}

J
Joao Moreno 已提交
1203
	@command('git.pushWithTags', { repository: true })
J
Joao Moreno 已提交
1204 1205
	async pushWithTags(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
1206 1207 1208 1209 1210 1211

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

J
Joao Moreno 已提交
1212
		await repository.pushTags();
1213 1214 1215 1216

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

J
Joao Moreno 已提交
1217
	@command('git.pushTo', { repository: true })
J
Joao Moreno 已提交
1218 1219
	async pushTo(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1220 1221 1222 1223 1224 1225

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

J
Joao Moreno 已提交
1226
		if (!repository.HEAD || !repository.HEAD.name) {
J
Joao Moreno 已提交
1227 1228 1229 1230
			window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
			return;
		}

J
Joao Moreno 已提交
1231
		const branchName = repository.HEAD.name;
J
Joao Moreno 已提交
1232 1233 1234 1235 1236 1237 1238 1239
		const picks = remotes.map(r => ({ label: r.name, description: r.url }));
		const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
		const pick = await window.showQuickPick(picks, { placeHolder });

		if (!pick) {
			return;
		}

J
Joao Moreno 已提交
1240
		repository.pushTo(pick.label, branchName);
J
Joao Moreno 已提交
1241 1242
	}

J
Joao Moreno 已提交
1243
	@command('git.sync', { repository: true })
J
Joao Moreno 已提交
1244 1245
	async sync(repository: Repository): Promise<void> {
		const HEAD = repository.HEAD;
J
Joao Moreno 已提交
1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266

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

		const config = workspace.getConfiguration('git');
		const shouldPrompt = config.get<boolean>('confirmSync') === true;

		if (shouldPrompt) {
			const message = localize('sync is unpredictable', "This action will push and pull commits to and from '{0}'.", HEAD.upstream);
			const yes = localize('ok', "OK");
			const neverAgain = localize('never again', "OK, Never Show Again");
			const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);

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

J
Joao Moreno 已提交
1267
		await repository.sync();
J
Joao Moreno 已提交
1268 1269
	}

J
Joao 已提交
1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282
	@command('git._syncAll')
	async syncAll(): Promise<void> {
		await Promise.all(this.model.repositories.map(async repository => {
			const HEAD = repository.HEAD;

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

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

J
Joao Moreno 已提交
1283
	@command('git.publish', { repository: true })
J
Joao Moreno 已提交
1284 1285
	async publish(repository: Repository): Promise<void> {
		const remotes = repository.remotes;
J
Joao Moreno 已提交
1286 1287 1288 1289 1290 1291

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

J
Joao Moreno 已提交
1292
		const branchName = repository.HEAD && repository.HEAD.name || '';
M
Markus Wolf 已提交
1293 1294 1295 1296 1297 1298
		const selectRemote = async () => {
			const picks = repository.remotes.map(r => r.name);
			const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
			return await window.showQuickPick(picks, { placeHolder });
		};
		const choice = remotes.length === 1 ? remotes[0].name : await selectRemote();
J
Joao Moreno 已提交
1299 1300 1301 1302 1303

		if (!choice) {
			return;
		}

J
Joao Moreno 已提交
1304
		await repository.pushTo(choice, branchName, true);
J
Joao Moreno 已提交
1305 1306
	}

J
Joao Moreno 已提交
1307
	@command('git.showOutput')
J
Joao Moreno 已提交
1308 1309 1310 1311
	showOutput(): void {
		this.outputChannel.show();
	}

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

1317
			if (!resource) {
J
Joao Moreno 已提交
1318 1319 1320
				return;
			}

1321
			resourceStates = [resource];
J
Joao Moreno 已提交
1322 1323
		}

1324
		const resources = resourceStates
J
Joao Moreno 已提交
1325 1326 1327
			.filter(s => s instanceof Resource)
			.map(r => r.resourceUri);

1328
		if (!resources.length) {
N
NKumar2 已提交
1329 1330 1331
			return;
		}

1332
		await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
N
NKumar2 已提交
1333 1334
	}

J
Joao Moreno 已提交
1335
	@command('git.stash', { repository: true })
J
Joao Moreno 已提交
1336 1337
	async stash(repository: Repository): Promise<void> {
		if (repository.workingTreeGroup.resourceStates.length === 0) {
K
Krzysztof Cieślak 已提交
1338 1339 1340
			window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
			return;
		}
J
Joao Moreno 已提交
1341 1342 1343 1344 1345 1346 1347 1348 1349 1350

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

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

J
Joao Moreno 已提交
1351
		await repository.createStash(message);
K
Krzysztof Cieślak 已提交
1352 1353
	}

J
Joao Moreno 已提交
1354
	@command('git.stashPop', { repository: true })
J
Joao Moreno 已提交
1355 1356
	async stashPop(repository: Repository): Promise<void> {
		const stashes = await repository.getStashes();
J
Joao Moreno 已提交
1357 1358

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1359 1360 1361 1362
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

J
Joao Moreno 已提交
1363 1364
		const picks = stashes.map(r => ({ label: `#${r.index}:  ${r.description}`, description: '', details: '', id: r.index }));
		const placeHolder = localize('pick stash to pop', "Pick a stash to pop");
K
Krzysztof Cieślak 已提交
1365 1366 1367 1368 1369
		const choice = await window.showQuickPick(picks, { placeHolder });

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

J
Joao Moreno 已提交
1371
		await repository.popStash(choice.id);
K
Krzysztof Cieślak 已提交
1372 1373
	}

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

		if (stashes.length === 0) {
K
Krzysztof Cieślak 已提交
1379 1380 1381 1382
			window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
			return;
		}

J
Joao Moreno 已提交
1383
		await repository.popStash();
J
Joao Moreno 已提交
1384
	}
K
Krzysztof Cieślak 已提交
1385

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

J
Joao Moreno 已提交
1390
			if (!options.repository) {
J
Joao Moreno 已提交
1391 1392
				result = Promise.resolve(method.apply(this, args));
			} else {
J
Joao Moreno 已提交
1393 1394
				// try to guess the repository based on the first argument
				const repository = this.model.getRepository(args[0]);
J
Joao Moreno 已提交
1395 1396 1397 1398 1399 1400 1401 1402 1403
				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();
				}
1404

J
Joao Moreno 已提交
1405
				result = repositoryPromise.then(repository => {
J
Joao Moreno 已提交
1406
					if (!repository) {
J
Joao 已提交
1407
						return Promise.resolve();
J
Joao Moreno 已提交
1408 1409
					}

J
Joao Moreno 已提交
1410
					return Promise.resolve(method.apply(this, [repository, ...args]));
J
Joao Moreno 已提交
1411
				});
J
Joao Moreno 已提交
1412 1413
			}

K
kieferrm 已提交
1414
			/* __GDPR__
K
kieferrm 已提交
1415 1416 1417 1418
				"git.command" : {
					"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
				}
			*/
J
Joao Moreno 已提交
1419 1420
			this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });

J
Joao Moreno 已提交
1421 1422 1423 1424
			return result.catch(async err => {
				let message: string;

				switch (err.gitErrorCode) {
1425
					case GitErrorCodes.DirtyWorkTree:
J
Joao Moreno 已提交
1426 1427
						message = localize('clean repo', "Please clean your repository working tree before checkout.");
						break;
1428 1429 1430
					case GitErrorCodes.PushRejected:
						message = localize('cant push', "Can't push refs to remote. Run 'Pull' first to integrate your changes.");
						break;
J
Joao Moreno 已提交
1431
					default:
1432 1433 1434
						const hint = (err.stderr || err.message || String(err))
							.replace(/^error: /mi, '')
							.replace(/^> husky.*$/mi, '')
J
Joao Moreno 已提交
1435
							.split(/[\r\n]/)
1436 1437 1438 1439 1440 1441
							.filter(line => !!line)
						[0];

						message = hint
							? localize('git error details', "Git: {0}", hint)
							: localize('git error', "Git error");
J
Joao Moreno 已提交
1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459

						break;
				}

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

				const outputChannel = this.outputChannel as OutputChannel;
				const openOutputChannelChoice = localize('open git log', "Open Git Log");
				const choice = await window.showErrorMessage(message, openOutputChannelChoice);

				if (choice === openOutputChannelChoice) {
					outputChannel.show();
				}
			});
		};
1460 1461 1462 1463 1464

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

		return result;
J
Joao Moreno 已提交
1465 1466
	}

1467
	private getSCMResource(uri?: Uri): Resource | undefined {
1468
		uri = uri ? uri : window.activeTextEditor && window.activeTextEditor.document.uri;
J
Joao Moreno 已提交
1469 1470

		if (!uri) {
1471
			return undefined;
J
Joao Moreno 已提交
1472 1473 1474
		}

		if (uri.scheme === 'git') {
J
Joao Moreno 已提交
1475 1476
			const { path } = fromGitUri(uri);
			uri = Uri.file(path);
J
Joao Moreno 已提交
1477 1478 1479 1480
		}

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

J
Joao Moreno 已提交
1483
			if (!repository) {
1484 1485
				return undefined;
			}
J
Joao Moreno 已提交
1486

J
Joao Moreno 已提交
1487 1488
			return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
				|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
J
Joao Moreno 已提交
1489 1490 1491
		}
	}

J
Joao Moreno 已提交
1492
	private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;
J
Joao Moreno 已提交
1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505
	private runByRepository<T>(resources: Uri[], fn: (repository: Repository, resources: Uri[]) => Promise<T>): Promise<T[]>;
	private async runByRepository<T>(arg: Uri | Uri[], fn: (repository: Repository, resources: any) => Promise<T>): Promise<T[]> {
		const resources = arg instanceof Uri ? [arg] : arg;
		const isSingleResource = arg instanceof Uri;

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

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

J
Joao Moreno 已提交
1506
			const tuple = result.filter(p => p.repository === repository)[0];
J
Joao Moreno 已提交
1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522

			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 已提交
1523 1524 1525
	dispose(): void {
		this.disposables.forEach(d => d.dispose());
	}
J
Joao Moreno 已提交
1526
}