Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/commands.ts
3316 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as os from 'os';
7
import * as path from 'path';
8
import { Command, commands, Disposable, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge, QuickPickItemKind, TextDocument, LogOutputChannel, l10n, Memento, UIKind, QuickInputButton, ThemeIcon, SourceControlHistoryItem, SourceControl, InputBoxValidationMessage, Tab, TabInputNotebook, QuickInputButtonLocation, languages } from 'vscode';
9
import TelemetryReporter from '@vscode/extension-telemetry';
10
import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator';
11
import { ForcePushMode, GitErrorCodes, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote, Branch, Ref } from './api/git';
12
import { Git, Stash, Worktree } from './git';
13
import { Model } from './model';
14
import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository';
15
import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges, compareLineChanges } from './staging';
16
import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri';
17
import { DiagnosticSeverityConfig, dispose, fromNow, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, toDiagnosticSeverity, truncate } from './util';
18
import { GitTimelineItem } from './timelineProvider';
19
import { ApiRepository } from './api/api1';
20
import { getRemoteSourceActions, pickRemoteSource } from './remoteSource';
21
import { RemoteSourceAction } from './typings/git-base';
22
23
abstract class CheckoutCommandItem implements QuickPickItem {
24
abstract get label(): string;
25
get description(): string { return ''; }
26
get alwaysShow(): boolean { return true; }
27
}
28
29
class CreateBranchItem extends CheckoutCommandItem {
30
get label(): string { return l10n.t('{0} Create new branch...', '$(plus)'); }
31
}
32
33
class CreateBranchFromItem extends CheckoutCommandItem {
34
get label(): string { return l10n.t('{0} Create new branch from...', '$(plus)'); }
35
}
36
37
class CheckoutDetachedItem extends CheckoutCommandItem {
38
get label(): string { return l10n.t('{0} Checkout detached...', '$(debug-disconnect)'); }
39
}
40
41
class RefItemSeparator implements QuickPickItem {
42
get kind(): QuickPickItemKind { return QuickPickItemKind.Separator; }
43
44
get label(): string {
45
switch (this.refType) {
46
case RefType.Head:
47
return l10n.t('branches');
48
case RefType.RemoteHead:
49
return l10n.t('remote branches');
50
case RefType.Tag:
51
return l10n.t('tags');
52
default:
53
return '';
54
}
55
}
56
57
constructor(private readonly refType: RefType) { }
58
}
59
60
class WorktreeItem implements QuickPickItem {
61
62
get label(): string {
63
return `$(list-tree) ${this.worktree.name}`;
64
}
65
66
get description(): string {
67
return this.worktree.path;
68
}
69
70
constructor(readonly worktree: Worktree) { }
71
}
72
73
class RefItem implements QuickPickItem {
74
75
get label(): string {
76
switch (this.ref.type) {
77
case RefType.Head:
78
return `$(git-branch) ${this.ref.name ?? this.shortCommit}`;
79
case RefType.RemoteHead:
80
return `$(cloud) ${this.ref.name ?? this.shortCommit}`;
81
case RefType.Tag:
82
return `$(tag) ${this.ref.name ?? this.shortCommit}`;
83
default:
84
return '';
85
}
86
}
87
88
get description(): string {
89
if (this.ref.commitDetails?.commitDate) {
90
return fromNow(this.ref.commitDetails.commitDate, true, true);
91
}
92
93
switch (this.ref.type) {
94
case RefType.Head:
95
return this.shortCommit;
96
case RefType.RemoteHead:
97
return l10n.t('Remote branch at {0}', this.shortCommit);
98
case RefType.Tag:
99
return l10n.t('Tag at {0}', this.shortCommit);
100
default:
101
return '';
102
}
103
}
104
105
get detail(): string | undefined {
106
if (this.ref.commitDetails?.authorName && this.ref.commitDetails?.message) {
107
return `${this.ref.commitDetails.authorName}$(circle-small-filled)${this.shortCommit}$(circle-small-filled)${this.ref.commitDetails.message}`;
108
}
109
110
return undefined;
111
}
112
113
get refId(): string {
114
switch (this.ref.type) {
115
case RefType.Head:
116
return `refs/heads/${this.ref.name}`;
117
case RefType.RemoteHead:
118
return `refs/remotes/${this.ref.remote}/${this.ref.name}`;
119
case RefType.Tag:
120
return `refs/tags/${this.ref.name}`;
121
}
122
}
123
get refName(): string | undefined { return this.ref.name; }
124
get refRemote(): string | undefined { return this.ref.remote; }
125
get shortCommit(): string { return (this.ref.commit || '').substring(0, this.shortCommitLength); }
126
127
private _buttons?: QuickInputButton[];
128
get buttons(): QuickInputButton[] | undefined { return this._buttons; }
129
set buttons(newButtons: QuickInputButton[] | undefined) { this._buttons = newButtons; }
130
131
constructor(protected readonly ref: Ref, private readonly shortCommitLength: number) { }
132
}
133
134
class BranchItem extends RefItem {
135
override get description(): string {
136
const description: string[] = [];
137
138
if (typeof this.ref.behind === 'number' && typeof this.ref.ahead === 'number') {
139
description.push(`${this.ref.behind}↓ ${this.ref.ahead}↑`);
140
}
141
if (this.ref.commitDetails?.commitDate) {
142
description.push(fromNow(this.ref.commitDetails.commitDate, true, true));
143
}
144
145
return description.length > 0 ? description.join('$(circle-small-filled)') : this.shortCommit;
146
}
147
148
constructor(override readonly ref: Branch, shortCommitLength: number) {
149
super(ref, shortCommitLength);
150
}
151
}
152
153
class CheckoutItem extends BranchItem {
154
async run(repository: Repository, opts?: { detached?: boolean }): Promise<void> {
155
if (!this.ref.name) {
156
return;
157
}
158
159
const config = workspace.getConfiguration('git', Uri.file(repository.root));
160
const pullBeforeCheckout = config.get<boolean>('pullBeforeCheckout', false) === true;
161
162
const treeish = opts?.detached ? this.ref.commit ?? this.ref.name : this.ref.name;
163
await repository.checkout(treeish, { ...opts, pullBeforeCheckout });
164
}
165
}
166
167
class CheckoutProtectedItem extends CheckoutItem {
168
169
override get label(): string {
170
return `$(lock) ${this.ref.name ?? this.shortCommit}`;
171
}
172
}
173
174
class CheckoutRemoteHeadItem extends RefItem {
175
176
async run(repository: Repository, opts?: { detached?: boolean }): Promise<void> {
177
if (!this.ref.name) {
178
return;
179
}
180
181
if (opts?.detached) {
182
await repository.checkout(this.ref.commit ?? this.ref.name, opts);
183
return;
184
}
185
186
const branches = await repository.findTrackingBranches(this.ref.name);
187
188
if (branches.length > 0) {
189
await repository.checkout(branches[0].name!, opts);
190
} else {
191
await repository.checkoutTracking(this.ref.name, opts);
192
}
193
}
194
}
195
196
class CheckoutTagItem extends RefItem {
197
198
async run(repository: Repository, opts?: { detached?: boolean }): Promise<void> {
199
if (!this.ref.name) {
200
return;
201
}
202
203
await repository.checkout(this.ref.name, opts);
204
}
205
}
206
207
class BranchDeleteItem extends BranchItem {
208
209
async run(repository: Repository, force?: boolean): Promise<void> {
210
if (this.ref.type === RefType.Head && this.refName) {
211
await repository.deleteBranch(this.refName, force);
212
} else if (this.ref.type === RefType.RemoteHead && this.refRemote && this.refName) {
213
const refName = this.refName.substring(this.refRemote.length + 1);
214
await repository.deleteRemoteRef(this.refRemote, refName, { force });
215
}
216
}
217
}
218
219
class TagDeleteItem extends RefItem {
220
221
async run(repository: Repository): Promise<void> {
222
if (this.ref.name) {
223
await repository.deleteTag(this.ref.name);
224
}
225
}
226
}
227
228
class RemoteTagDeleteItem extends RefItem {
229
230
override get description(): string {
231
return l10n.t('Remote tag at {0}', this.shortCommit);
232
}
233
234
async run(repository: Repository, remote: string): Promise<void> {
235
if (this.ref.name) {
236
await repository.deleteRemoteRef(remote, this.ref.name);
237
}
238
}
239
}
240
241
class WorktreeDeleteItem extends WorktreeItem {
242
async run(mainRepository: Repository): Promise<void> {
243
if (!this.worktree.path) {
244
return;
245
}
246
247
try {
248
await mainRepository.deleteWorktree(this.worktree.path);
249
} catch (err) {
250
if (err.gitErrorCode === GitErrorCodes.WorktreeContainsChanges) {
251
const forceDelete = l10n.t('Force Delete');
252
const message = l10n.t('The worktree contains modified or untracked files. Do you want to force delete?');
253
const choice = await window.showWarningMessage(message, { modal: true }, forceDelete);
254
255
if (choice === forceDelete) {
256
await mainRepository.deleteWorktree(this.worktree.path, { force: true });
257
}
258
}
259
}
260
}
261
}
262
263
264
class MergeItem extends BranchItem {
265
266
async run(repository: Repository): Promise<void> {
267
if (this.ref.name || this.ref.commit) {
268
await repository.merge(this.ref.name ?? this.ref.commit!);
269
}
270
}
271
}
272
273
class RebaseItem extends BranchItem {
274
275
async run(repository: Repository): Promise<void> {
276
if (this.ref?.name) {
277
await repository.rebase(this.ref.name);
278
}
279
}
280
}
281
282
class RebaseUpstreamItem extends RebaseItem {
283
284
override get description(): string {
285
return '(upstream)';
286
}
287
}
288
289
class HEADItem implements QuickPickItem {
290
291
constructor(private repository: Repository, private readonly shortCommitLength: number) { }
292
293
get label(): string { return 'HEAD'; }
294
get description(): string { return (this.repository.HEAD?.commit ?? '').substring(0, this.shortCommitLength); }
295
get alwaysShow(): boolean { return true; }
296
get refName(): string { return 'HEAD'; }
297
}
298
299
class AddRemoteItem implements QuickPickItem {
300
301
constructor(private cc: CommandCenter) { }
302
303
get label(): string { return '$(plus) ' + l10n.t('Add a new remote...'); }
304
get description(): string { return ''; }
305
306
get alwaysShow(): boolean { return true; }
307
308
async run(repository: Repository): Promise<void> {
309
await this.cc.addRemote(repository);
310
}
311
}
312
313
class RemoteItem implements QuickPickItem {
314
get label() { return `$(cloud) ${this.remote.name}`; }
315
get description(): string | undefined { return this.remote.fetchUrl; }
316
get remoteName(): string { return this.remote.name; }
317
318
constructor(private readonly repository: Repository, private readonly remote: Remote) { }
319
320
async run(): Promise<void> {
321
await this.repository.fetch({ remote: this.remote.name });
322
}
323
}
324
325
class FetchAllRemotesItem implements QuickPickItem {
326
get label(): string { return l10n.t('{0} Fetch all remotes', '$(cloud-download)'); }
327
328
constructor(private readonly repository: Repository) { }
329
330
async run(): Promise<void> {
331
await this.repository.fetch({ all: true });
332
}
333
}
334
335
class RepositoryItem implements QuickPickItem {
336
get label(): string { return `$(repo) ${getRepositoryLabel(this.path)}`; }
337
338
get description(): string { return this.path; }
339
340
constructor(public readonly path: string) { }
341
}
342
343
class StashItem implements QuickPickItem {
344
get label(): string { return `#${this.stash.index}: ${this.stash.description}`; }
345
346
get description(): string | undefined { return this.stash.branchName; }
347
348
constructor(readonly stash: Stash) { }
349
}
350
351
interface ScmCommandOptions {
352
repository?: boolean;
353
repositoryFilter?: ('repository' | 'submodule' | 'worktree')[];
354
}
355
356
interface ScmCommand {
357
commandId: string;
358
key: string;
359
method: Function;
360
options: ScmCommandOptions;
361
}
362
363
const Commands: ScmCommand[] = [];
364
365
function command(commandId: string, options: ScmCommandOptions = {}): Function {
366
return (value: any, context: ClassMethodDecoratorContext) => {
367
if (context.kind !== 'method') {
368
throw new Error('not supported');
369
}
370
const key = context.name.toString();
371
Commands.push({ commandId, key, method: value, options });
372
};
373
}
374
375
// const ImageMimetypes = [
376
// 'image/png',
377
// 'image/gif',
378
// 'image/jpeg',
379
// 'image/webp',
380
// 'image/tiff',
381
// 'image/bmp'
382
// ];
383
384
async function categorizeResourceByResolution(resources: Resource[]): Promise<{ merge: Resource[]; resolved: Resource[]; unresolved: Resource[]; deletionConflicts: Resource[] }> {
385
const selection = resources.filter(s => s instanceof Resource) as Resource[];
386
const merge = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge);
387
const isBothAddedOrModified = (s: Resource) => s.type === Status.BOTH_MODIFIED || s.type === Status.BOTH_ADDED;
388
const isAnyDeleted = (s: Resource) => s.type === Status.DELETED_BY_THEM || s.type === Status.DELETED_BY_US;
389
const possibleUnresolved = merge.filter(isBothAddedOrModified);
390
const promises = possibleUnresolved.map(s => grep(s.resourceUri.fsPath, /^<{7}\s|^={7}$|^>{7}\s/));
391
const unresolvedBothModified = await Promise.all<boolean>(promises);
392
const resolved = possibleUnresolved.filter((_s, i) => !unresolvedBothModified[i]);
393
const deletionConflicts = merge.filter(s => isAnyDeleted(s));
394
const unresolved = [
395
...merge.filter(s => !isBothAddedOrModified(s) && !isAnyDeleted(s)),
396
...possibleUnresolved.filter((_s, i) => unresolvedBothModified[i])
397
];
398
399
return { merge, resolved, unresolved, deletionConflicts };
400
}
401
402
async function createCheckoutItems(repository: Repository, detached = false): Promise<QuickPickItem[]> {
403
const config = workspace.getConfiguration('git');
404
const checkoutTypeConfig = config.get<string | string[]>('checkoutType');
405
const showRefDetails = config.get<boolean>('showReferenceDetails') === true;
406
407
let checkoutTypes: string[];
408
409
if (checkoutTypeConfig === 'all' || !checkoutTypeConfig || checkoutTypeConfig.length === 0) {
410
checkoutTypes = ['local', 'remote', 'tags'];
411
} else if (typeof checkoutTypeConfig === 'string') {
412
checkoutTypes = [checkoutTypeConfig];
413
} else {
414
checkoutTypes = checkoutTypeConfig;
415
}
416
417
if (detached) {
418
// Remove tags when in detached mode
419
checkoutTypes = checkoutTypes.filter(t => t !== 'tags');
420
}
421
422
const refs = await repository.getRefs({ includeCommitDetails: showRefDetails });
423
const refProcessors = checkoutTypes.map(type => getCheckoutRefProcessor(repository, type))
424
.filter(p => !!p) as RefProcessor[];
425
426
const buttons = await getRemoteRefItemButtons(repository);
427
const itemsProcessor = new CheckoutItemsProcessor(repository, refProcessors, buttons, detached);
428
429
return itemsProcessor.processRefs(refs);
430
}
431
432
type RemoteSourceActionButton = {
433
iconPath: ThemeIcon;
434
tooltip: string;
435
actual: RemoteSourceAction;
436
};
437
438
async function getRemoteRefItemButtons(repository: Repository) {
439
// Compute actions for all known remotes
440
const remoteUrlsToActions = new Map<string, RemoteSourceActionButton[]>();
441
442
const getButtons = async (remoteUrl: string) => (await getRemoteSourceActions(remoteUrl)).map((action) => ({ iconPath: new ThemeIcon(action.icon), tooltip: action.label, actual: action }));
443
444
for (const remote of repository.remotes) {
445
if (remote.fetchUrl) {
446
const actions = remoteUrlsToActions.get(remote.fetchUrl) ?? [];
447
actions.push(...await getButtons(remote.fetchUrl));
448
remoteUrlsToActions.set(remote.fetchUrl, actions);
449
}
450
if (remote.pushUrl && remote.pushUrl !== remote.fetchUrl) {
451
const actions = remoteUrlsToActions.get(remote.pushUrl) ?? [];
452
actions.push(...await getButtons(remote.pushUrl));
453
remoteUrlsToActions.set(remote.pushUrl, actions);
454
}
455
}
456
457
return remoteUrlsToActions;
458
}
459
460
class RefProcessor {
461
protected readonly refs: Ref[] = [];
462
463
constructor(protected readonly type: RefType, protected readonly ctor: { new(ref: Ref, shortCommitLength: number): QuickPickItem } = RefItem) { }
464
465
processRef(ref: Ref): boolean {
466
if (!ref.name && !ref.commit) {
467
return false;
468
}
469
if (ref.type !== this.type) {
470
return false;
471
}
472
473
this.refs.push(ref);
474
return true;
475
}
476
477
getItems(shortCommitLength: number): QuickPickItem[] {
478
const items = this.refs.map(r => new this.ctor(r, shortCommitLength));
479
return items.length === 0 ? items : [new RefItemSeparator(this.type), ...items];
480
}
481
}
482
483
class RefItemsProcessor {
484
protected readonly shortCommitLength: number;
485
486
constructor(
487
protected readonly repository: Repository,
488
protected readonly processors: RefProcessor[],
489
protected readonly options: {
490
skipCurrentBranch?: boolean;
491
skipCurrentBranchRemote?: boolean;
492
} = {}
493
) {
494
const config = workspace.getConfiguration('git', Uri.file(repository.root));
495
this.shortCommitLength = config.get<number>('commitShortHashLength', 7);
496
}
497
498
processRefs(refs: Ref[]): QuickPickItem[] {
499
const refsToSkip = this.getRefsToSkip();
500
501
for (const ref of refs) {
502
if (ref.name && refsToSkip.includes(ref.name)) {
503
continue;
504
}
505
for (const processor of this.processors) {
506
if (processor.processRef(ref)) {
507
break;
508
}
509
}
510
}
511
512
const result: QuickPickItem[] = [];
513
for (const processor of this.processors) {
514
result.push(...processor.getItems(this.shortCommitLength));
515
}
516
517
return result;
518
}
519
520
protected getRefsToSkip(): string[] {
521
const refsToSkip = ['origin/HEAD'];
522
523
if (this.options.skipCurrentBranch && this.repository.HEAD?.name) {
524
refsToSkip.push(this.repository.HEAD.name);
525
}
526
527
if (this.options.skipCurrentBranchRemote && this.repository.HEAD?.upstream) {
528
refsToSkip.push(`${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`);
529
}
530
531
return refsToSkip;
532
}
533
}
534
535
class CheckoutRefProcessor extends RefProcessor {
536
537
constructor(private readonly repository: Repository) {
538
super(RefType.Head);
539
}
540
541
override getItems(shortCommitLength: number): QuickPickItem[] {
542
const items = this.refs.map(ref => {
543
return this.repository.isBranchProtected(ref) ?
544
new CheckoutProtectedItem(ref, shortCommitLength) :
545
new CheckoutItem(ref, shortCommitLength);
546
});
547
548
return items.length === 0 ? items : [new RefItemSeparator(this.type), ...items];
549
}
550
}
551
552
class CheckoutItemsProcessor extends RefItemsProcessor {
553
554
private defaultButtons: RemoteSourceActionButton[] | undefined;
555
556
constructor(
557
repository: Repository,
558
processors: RefProcessor[],
559
private readonly buttons: Map<string, RemoteSourceActionButton[]>,
560
private readonly detached = false) {
561
super(repository, processors);
562
563
// Default button(s)
564
const remote = repository.remotes.find(r => r.pushUrl === repository.HEAD?.remote || r.fetchUrl === repository.HEAD?.remote) ?? repository.remotes[0];
565
const remoteUrl = remote?.pushUrl ?? remote?.fetchUrl;
566
if (remoteUrl) {
567
this.defaultButtons = buttons.get(remoteUrl);
568
}
569
}
570
571
override processRefs(refs: Ref[]): QuickPickItem[] {
572
for (const ref of refs) {
573
if (!this.detached && ref.name === 'origin/HEAD') {
574
continue;
575
}
576
577
for (const processor of this.processors) {
578
if (processor.processRef(ref)) {
579
break;
580
}
581
}
582
}
583
584
const result: QuickPickItem[] = [];
585
for (const processor of this.processors) {
586
for (const item of processor.getItems(this.shortCommitLength)) {
587
if (!(item instanceof RefItem)) {
588
result.push(item);
589
continue;
590
}
591
592
// Button(s)
593
if (item.refRemote) {
594
const matchingRemote = this.repository.remotes.find((remote) => remote.name === item.refRemote);
595
const buttons = [];
596
if (matchingRemote?.pushUrl) {
597
buttons.push(...this.buttons.get(matchingRemote.pushUrl) ?? []);
598
}
599
if (matchingRemote?.fetchUrl && matchingRemote.fetchUrl !== matchingRemote.pushUrl) {
600
buttons.push(...this.buttons.get(matchingRemote.fetchUrl) ?? []);
601
}
602
if (buttons.length) {
603
item.buttons = buttons;
604
}
605
} else {
606
item.buttons = this.defaultButtons;
607
}
608
609
result.push(item);
610
}
611
}
612
613
return result;
614
}
615
}
616
617
function getCheckoutRefProcessor(repository: Repository, type: string): RefProcessor | undefined {
618
switch (type) {
619
case 'local':
620
return new CheckoutRefProcessor(repository);
621
case 'remote':
622
return new RefProcessor(RefType.RemoteHead, CheckoutRemoteHeadItem);
623
case 'tags':
624
return new RefProcessor(RefType.Tag, CheckoutTagItem);
625
default:
626
return undefined;
627
}
628
}
629
630
function getRepositoryLabel(repositoryRoot: string): string {
631
const workspaceFolder = workspace.getWorkspaceFolder(Uri.file(repositoryRoot));
632
return workspaceFolder?.uri.toString() === repositoryRoot ? workspaceFolder.name : path.basename(repositoryRoot);
633
}
634
635
function compareRepositoryLabel(repositoryRoot1: string, repositoryRoot2: string): number {
636
return getRepositoryLabel(repositoryRoot1).localeCompare(getRepositoryLabel(repositoryRoot2));
637
}
638
639
function sanitizeBranchName(name: string, whitespaceChar: string): string {
640
return name ? name.trim().replace(/^-+/, '').replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, whitespaceChar) : name;
641
}
642
643
function sanitizeRemoteName(name: string) {
644
name = name.trim();
645
return name && name.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, '-');
646
}
647
648
enum PushType {
649
Push,
650
PushTo,
651
PushFollowTags,
652
PushTags
653
}
654
655
interface PushOptions {
656
pushType: PushType;
657
forcePush?: boolean;
658
silent?: boolean;
659
660
pushTo?: {
661
remote?: string;
662
refspec?: string;
663
setUpstream?: boolean;
664
};
665
}
666
667
class CommandErrorOutputTextDocumentContentProvider implements TextDocumentContentProvider {
668
669
private items = new Map<string, string>();
670
671
set(uri: Uri, contents: string): void {
672
this.items.set(uri.path, contents);
673
}
674
675
delete(uri: Uri): void {
676
this.items.delete(uri.path);
677
}
678
679
provideTextDocumentContent(uri: Uri): string | undefined {
680
return this.items.get(uri.path);
681
}
682
}
683
684
async function evaluateDiagnosticsCommitHook(repository: Repository, options: CommitOptions): Promise<boolean> {
685
const config = workspace.getConfiguration('git', Uri.file(repository.root));
686
const enabled = config.get<boolean>('diagnosticsCommitHook.enabled', false) === true;
687
const sourceSeverity = config.get<Record<string, DiagnosticSeverityConfig>>('diagnosticsCommitHook.sources', { '*': 'error' });
688
689
if (!enabled) {
690
return true;
691
}
692
693
const resources: Uri[] = [];
694
if (repository.indexGroup.resourceStates.length > 0) {
695
// Staged files
696
resources.push(...repository.indexGroup.resourceStates.map(r => r.resourceUri));
697
} else if (options.all === 'tracked') {
698
// Tracked files
699
resources.push(...repository.workingTreeGroup.resourceStates
700
.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED)
701
.map(r => r.resourceUri));
702
} else {
703
// All files
704
resources.push(...repository.workingTreeGroup.resourceStates.map(r => r.resourceUri));
705
resources.push(...repository.untrackedGroup.resourceStates.map(r => r.resourceUri));
706
}
707
708
const diagnostics: Map<Uri, number> = new Map();
709
710
for (const resource of resources) {
711
const unresolvedDiagnostics = languages.getDiagnostics(resource)
712
.filter(d => {
713
// No source or ignored source
714
if (!d.source || (Object.keys(sourceSeverity).includes(d.source) && sourceSeverity[d.source] === 'none')) {
715
return false;
716
}
717
718
// Source severity
719
if (Object.keys(sourceSeverity).includes(d.source) &&
720
d.severity <= toDiagnosticSeverity(sourceSeverity[d.source])) {
721
return true;
722
}
723
724
// Wildcard severity
725
if (Object.keys(sourceSeverity).includes('*') &&
726
d.severity <= toDiagnosticSeverity(sourceSeverity['*'])) {
727
return true;
728
}
729
730
return false;
731
});
732
733
if (unresolvedDiagnostics.length > 0) {
734
diagnostics.set(resource, unresolvedDiagnostics.length);
735
}
736
}
737
738
if (diagnostics.size === 0) {
739
return true;
740
}
741
742
// Show dialog
743
const commit = l10n.t('Commit Anyway');
744
const view = l10n.t('View Problems');
745
746
const message = diagnostics.size === 1
747
? l10n.t('The following file has unresolved diagnostics: \'{0}\'.\n\nHow would you like to proceed?', path.basename(diagnostics.keys().next().value!.fsPath))
748
: l10n.t('There are {0} files that have unresolved diagnostics.\n\nHow would you like to proceed?', diagnostics.size);
749
750
const choice = await window.showWarningMessage(message, { modal: true }, commit, view);
751
752
// Commit Anyway
753
if (choice === commit) {
754
return true;
755
}
756
757
// View Problems
758
if (choice === view) {
759
commands.executeCommand('workbench.panel.markers.view.focus');
760
}
761
762
return false;
763
}
764
765
export class CommandCenter {
766
767
private disposables: Disposable[];
768
private commandErrors = new CommandErrorOutputTextDocumentContentProvider();
769
770
private static readonly WORKTREE_ROOT_KEY = 'worktreeRoot';
771
772
constructor(
773
private git: Git,
774
private model: Model,
775
private globalState: Memento,
776
private logger: LogOutputChannel,
777
private telemetryReporter: TelemetryReporter
778
) {
779
this.disposables = Commands.map(({ commandId, key, method, options }) => {
780
const command = this.createCommand(commandId, key, method, options);
781
return commands.registerCommand(commandId, command);
782
});
783
784
this.disposables.push(workspace.registerTextDocumentContentProvider('git-output', this.commandErrors));
785
}
786
787
@command('git.showOutput')
788
showOutput(): void {
789
this.logger.show();
790
}
791
792
@command('git.refresh', { repository: true })
793
async refresh(repository: Repository): Promise<void> {
794
await repository.refresh();
795
}
796
797
@command('git.openResource')
798
async openResource(resource: Resource): Promise<void> {
799
const repository = this.model.getRepository(resource.resourceUri);
800
801
if (!repository) {
802
return;
803
}
804
805
await resource.open();
806
}
807
808
@command('git.openAllChanges', { repository: true })
809
async openChanges(repository: Repository): Promise<void> {
810
for (const resource of [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates]) {
811
if (
812
resource.type === Status.DELETED || resource.type === Status.DELETED_BY_THEM ||
813
resource.type === Status.DELETED_BY_US || resource.type === Status.BOTH_DELETED
814
) {
815
continue;
816
}
817
818
void commands.executeCommand(
819
'vscode.open',
820
resource.resourceUri,
821
{ background: true, preview: false, }
822
);
823
}
824
}
825
826
@command('git.openMergeEditor')
827
async openMergeEditor(uri: unknown) {
828
if (uri === undefined) {
829
// fallback to active editor...
830
if (window.tabGroups.activeTabGroup.activeTab?.input instanceof TabInputText) {
831
uri = window.tabGroups.activeTabGroup.activeTab.input.uri;
832
}
833
}
834
if (!(uri instanceof Uri)) {
835
return;
836
}
837
const repo = this.model.getRepository(uri);
838
if (!repo) {
839
return;
840
}
841
842
const isRebasing = Boolean(repo.rebaseCommit);
843
844
type InputData = { uri: Uri; title?: string; detail?: string; description?: string };
845
const mergeUris = toMergeUris(uri);
846
847
let isStashConflict = false;
848
try {
849
// Look at the conflict markers to check if this is a stash conflict
850
const document = await workspace.openTextDocument(uri);
851
const firstConflictInfo = findFirstConflictMarker(document);
852
isStashConflict = firstConflictInfo?.incomingChangeLabel === 'Stashed changes';
853
} catch (error) {
854
console.error(error);
855
}
856
857
const current: InputData = { uri: mergeUris.ours, title: l10n.t('Current') };
858
const incoming: InputData = { uri: mergeUris.theirs, title: l10n.t('Incoming') };
859
860
if (isStashConflict) {
861
incoming.title = l10n.t('Stashed Changes');
862
}
863
864
try {
865
const [head, rebaseOrMergeHead, diffBetween] = await Promise.all([
866
repo.getCommit('HEAD'),
867
isRebasing ? repo.getCommit('REBASE_HEAD') : repo.getCommit('MERGE_HEAD'),
868
await repo.diffBetween(isRebasing ? 'REBASE_HEAD' : 'MERGE_HEAD', 'HEAD')
869
]);
870
const diffFile = diffBetween?.find(diff => diff.uri.fsPath === uri.fsPath);
871
872
// ours (current branch and commit)
873
current.detail = head.refNames.map(s => s.replace(/^HEAD ->/, '')).join(', ');
874
current.description = '$(git-commit) ' + head.hash.substring(0, 7);
875
current.uri = toGitUri(uri, head.hash);
876
877
// theirs
878
incoming.detail = rebaseOrMergeHead.refNames.join(', ');
879
incoming.description = '$(git-commit) ' + rebaseOrMergeHead.hash.substring(0, 7);
880
if (diffFile) {
881
incoming.uri = toGitUri(diffFile.originalUri, rebaseOrMergeHead.hash);
882
} else {
883
incoming.uri = toGitUri(uri, rebaseOrMergeHead.hash);
884
}
885
886
} catch (error) {
887
// not so bad, can continue with just uris
888
console.error('FAILED to read HEAD, MERGE_HEAD commits');
889
console.error(error);
890
}
891
892
const options = {
893
base: mergeUris.base,
894
input1: isRebasing ? current : incoming,
895
input2: isRebasing ? incoming : current,
896
output: uri
897
};
898
899
await commands.executeCommand(
900
'_open.mergeEditor',
901
options
902
);
903
904
function findFirstConflictMarker(doc: TextDocument): { currentChangeLabel: string; incomingChangeLabel: string } | undefined {
905
const conflictMarkerStart = '<<<<<<<';
906
const conflictMarkerEnd = '>>>>>>>';
907
let inConflict = false;
908
let currentChangeLabel: string = '';
909
let incomingChangeLabel: string = '';
910
let hasConflict = false;
911
912
for (let lineIdx = 0; lineIdx < doc.lineCount; lineIdx++) {
913
const lineStr = doc.lineAt(lineIdx).text;
914
if (!inConflict) {
915
if (lineStr.startsWith(conflictMarkerStart)) {
916
currentChangeLabel = lineStr.substring(conflictMarkerStart.length).trim();
917
inConflict = true;
918
hasConflict = true;
919
}
920
} else {
921
if (lineStr.startsWith(conflictMarkerEnd)) {
922
incomingChangeLabel = lineStr.substring(conflictMarkerStart.length).trim();
923
inConflict = false;
924
break;
925
}
926
}
927
}
928
if (hasConflict) {
929
return {
930
currentChangeLabel,
931
incomingChangeLabel
932
};
933
}
934
return undefined;
935
}
936
}
937
938
async cloneRepository(url?: string, parentPath?: string, options: { recursive?: boolean; ref?: string } = {}): Promise<void> {
939
if (!url || typeof url !== 'string') {
940
url = await pickRemoteSource({
941
providerLabel: provider => l10n.t('Clone from {0}', provider.name),
942
urlLabel: l10n.t('Clone from URL')
943
});
944
}
945
946
if (!url) {
947
/* __GDPR__
948
"clone" : {
949
"owner": "lszomoru",
950
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
951
}
952
*/
953
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
954
return;
955
}
956
957
url = url.trim().replace(/^git\s+clone\s+/, '');
958
959
if (!parentPath) {
960
const config = workspace.getConfiguration('git');
961
let defaultCloneDirectory = config.get<string>('defaultCloneDirectory') || os.homedir();
962
defaultCloneDirectory = defaultCloneDirectory.replace(/^~/, os.homedir());
963
964
const uris = await window.showOpenDialog({
965
canSelectFiles: false,
966
canSelectFolders: true,
967
canSelectMany: false,
968
defaultUri: Uri.file(defaultCloneDirectory),
969
title: l10n.t('Choose a folder to clone {0} into', url),
970
openLabel: l10n.t('Select as Repository Destination')
971
});
972
973
if (!uris || uris.length === 0) {
974
/* __GDPR__
975
"clone" : {
976
"owner": "lszomoru",
977
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
978
}
979
*/
980
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
981
return;
982
}
983
984
const uri = uris[0];
985
parentPath = uri.fsPath;
986
}
987
988
try {
989
const opts = {
990
location: ProgressLocation.Notification,
991
title: l10n.t('Cloning git repository "{0}"...', url),
992
cancellable: true
993
};
994
995
const repositoryPath = await window.withProgress(
996
opts,
997
(progress, token) => this.git.clone(url!, { parentPath: parentPath!, progress, recursive: options.recursive, ref: options.ref }, token)
998
);
999
1000
const config = workspace.getConfiguration('git');
1001
const openAfterClone = config.get<'always' | 'alwaysNewWindow' | 'whenNoFolderOpen' | 'prompt'>('openAfterClone');
1002
1003
enum PostCloneAction { Open, OpenNewWindow, AddToWorkspace }
1004
let action: PostCloneAction | undefined = undefined;
1005
1006
if (openAfterClone === 'always') {
1007
action = PostCloneAction.Open;
1008
} else if (openAfterClone === 'alwaysNewWindow') {
1009
action = PostCloneAction.OpenNewWindow;
1010
} else if (openAfterClone === 'whenNoFolderOpen' && !workspace.workspaceFolders) {
1011
action = PostCloneAction.Open;
1012
}
1013
1014
if (action === undefined) {
1015
let message = l10n.t('Would you like to open the cloned repository?');
1016
const open = l10n.t('Open');
1017
const openNewWindow = l10n.t('Open in New Window');
1018
const choices = [open, openNewWindow];
1019
1020
const addToWorkspace = l10n.t('Add to Workspace');
1021
if (workspace.workspaceFolders) {
1022
message = l10n.t('Would you like to open the cloned repository, or add it to the current workspace?');
1023
choices.push(addToWorkspace);
1024
}
1025
1026
const result = await window.showInformationMessage(message, { modal: true }, ...choices);
1027
1028
action = result === open ? PostCloneAction.Open
1029
: result === openNewWindow ? PostCloneAction.OpenNewWindow
1030
: result === addToWorkspace ? PostCloneAction.AddToWorkspace : undefined;
1031
}
1032
1033
/* __GDPR__
1034
"clone" : {
1035
"owner": "lszomoru",
1036
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" },
1037
"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Indicates whether the folder is opened following the clone operation" }
1038
}
1039
*/
1040
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: action === PostCloneAction.Open || action === PostCloneAction.OpenNewWindow ? 1 : 0 });
1041
1042
const uri = Uri.file(repositoryPath);
1043
1044
if (action === PostCloneAction.Open) {
1045
commands.executeCommand('vscode.openFolder', uri, { forceReuseWindow: true });
1046
} else if (action === PostCloneAction.AddToWorkspace) {
1047
workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
1048
} else if (action === PostCloneAction.OpenNewWindow) {
1049
commands.executeCommand('vscode.openFolder', uri, { forceNewWindow: true });
1050
}
1051
} catch (err) {
1052
if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
1053
/* __GDPR__
1054
"clone" : {
1055
"owner": "lszomoru",
1056
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
1057
}
1058
*/
1059
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
1060
} else if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
1061
return;
1062
} else {
1063
/* __GDPR__
1064
"clone" : {
1065
"owner": "lszomoru",
1066
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
1067
}
1068
*/
1069
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
1070
}
1071
1072
throw err;
1073
}
1074
}
1075
1076
private getRepositoriesWithRemote(repositories: Repository[]) {
1077
return repositories.reduce<(QuickPickItem & { repository: Repository })[]>((items, repository) => {
1078
const remote = repository.remotes.find((r) => r.name === repository.HEAD?.upstream?.remote);
1079
if (remote?.pushUrl) {
1080
items.push({ repository: repository, label: remote.pushUrl });
1081
}
1082
return items;
1083
}, []);
1084
}
1085
1086
@command('git.continueInLocalClone')
1087
async continueInLocalClone(): Promise<Uri | void> {
1088
if (this.model.repositories.length === 0) { return; }
1089
1090
// Pick a single repository to continue working on in a local clone if there's more than one
1091
let items = this.getRepositoriesWithRemote(this.model.repositories);
1092
1093
// We have a repository but there is no remote URL (e.g. git init)
1094
if (items.length === 0) {
1095
const pick = this.model.repositories.length === 1
1096
? { repository: this.model.repositories[0] }
1097
: await window.showQuickPick(this.model.repositories.map((i) => ({ repository: i, label: i.root })), { canPickMany: false, placeHolder: l10n.t('Choose which repository to publish') });
1098
if (!pick) { return; }
1099
1100
await this.publish(pick.repository);
1101
1102
items = this.getRepositoriesWithRemote([pick.repository]);
1103
if (items.length === 0) {
1104
return;
1105
}
1106
}
1107
1108
let selection = items[0];
1109
if (items.length > 1) {
1110
const pick = await window.showQuickPick(items, { canPickMany: false, placeHolder: l10n.t('Choose which repository to clone') });
1111
if (pick === undefined) { return; }
1112
selection = pick;
1113
}
1114
1115
const uri = selection.label;
1116
const ref = selection.repository.HEAD?.upstream?.name;
1117
1118
if (uri !== undefined) {
1119
let target = `${env.uriScheme}://vscode.git/clone?url=${encodeURIComponent(uri)}`;
1120
const isWeb = env.uiKind === UIKind.Web;
1121
const isRemote = env.remoteName !== undefined;
1122
1123
if (isWeb || isRemote) {
1124
if (ref !== undefined) {
1125
target += `&ref=${encodeURIComponent(ref)}`;
1126
}
1127
1128
if (isWeb) {
1129
// Launch desktop client if currently in web
1130
return Uri.parse(target);
1131
}
1132
1133
if (isRemote) {
1134
// If already in desktop client but in a remote window, we need to force a new window
1135
// so that the git extension can access the local filesystem for cloning
1136
target += `&windowId=_blank`;
1137
return Uri.parse(target);
1138
}
1139
}
1140
1141
// Otherwise, directly clone
1142
void this.clone(uri, undefined, { ref: ref });
1143
}
1144
}
1145
1146
@command('git.clone')
1147
async clone(url?: string, parentPath?: string, options?: { ref?: string }): Promise<void> {
1148
await this.cloneRepository(url, parentPath, options);
1149
}
1150
1151
@command('git.cloneRecursive')
1152
async cloneRecursive(url?: string, parentPath?: string): Promise<void> {
1153
await this.cloneRepository(url, parentPath, { recursive: true });
1154
}
1155
1156
@command('git.init')
1157
async init(skipFolderPrompt = false): Promise<void> {
1158
let repositoryPath: string | undefined = undefined;
1159
let askToOpen = true;
1160
1161
if (workspace.workspaceFolders) {
1162
if (skipFolderPrompt && workspace.workspaceFolders.length === 1) {
1163
repositoryPath = workspace.workspaceFolders[0].uri.fsPath;
1164
askToOpen = false;
1165
} else {
1166
const placeHolder = l10n.t('Pick workspace folder to initialize git repo in');
1167
const pick = { label: l10n.t('Choose Folder...') };
1168
const items: { label: string; folder?: WorkspaceFolder }[] = [
1169
...workspace.workspaceFolders.map(folder => ({ label: folder.name, description: folder.uri.fsPath, folder })),
1170
pick
1171
];
1172
const item = await window.showQuickPick(items, { placeHolder, ignoreFocusOut: true });
1173
1174
if (!item) {
1175
return;
1176
} else if (item.folder) {
1177
repositoryPath = item.folder.uri.fsPath;
1178
askToOpen = false;
1179
}
1180
}
1181
}
1182
1183
if (!repositoryPath) {
1184
const homeUri = Uri.file(os.homedir());
1185
const defaultUri = workspace.workspaceFolders && workspace.workspaceFolders.length > 0
1186
? Uri.file(workspace.workspaceFolders[0].uri.fsPath)
1187
: homeUri;
1188
1189
const result = await window.showOpenDialog({
1190
canSelectFiles: false,
1191
canSelectFolders: true,
1192
canSelectMany: false,
1193
defaultUri,
1194
openLabel: l10n.t('Initialize Repository')
1195
});
1196
1197
if (!result || result.length === 0) {
1198
return;
1199
}
1200
1201
const uri = result[0];
1202
1203
if (homeUri.toString().startsWith(uri.toString())) {
1204
const yes = l10n.t('Initialize Repository');
1205
const answer = await window.showWarningMessage(l10n.t('This will create a Git repository in "{0}". Are you sure you want to continue?', uri.fsPath), yes);
1206
1207
if (answer !== yes) {
1208
return;
1209
}
1210
}
1211
1212
repositoryPath = uri.fsPath;
1213
1214
if (workspace.workspaceFolders && workspace.workspaceFolders.some(w => w.uri.toString() === uri.toString())) {
1215
askToOpen = false;
1216
}
1217
}
1218
1219
const config = workspace.getConfiguration('git');
1220
const defaultBranchName = config.get<string>('defaultBranchName', 'main');
1221
const branchWhitespaceChar = config.get<string>('branchWhitespaceChar', '-');
1222
1223
await this.git.init(repositoryPath, { defaultBranch: sanitizeBranchName(defaultBranchName, branchWhitespaceChar) });
1224
1225
let message = l10n.t('Would you like to open the initialized repository?');
1226
const open = l10n.t('Open');
1227
const openNewWindow = l10n.t('Open in New Window');
1228
const choices = [open, openNewWindow];
1229
1230
if (!askToOpen) {
1231
await this.model.openRepository(repositoryPath);
1232
return;
1233
}
1234
1235
const addToWorkspace = l10n.t('Add to Workspace');
1236
if (workspace.workspaceFolders) {
1237
message = l10n.t('Would you like to open the initialized repository, or add it to the current workspace?');
1238
choices.push(addToWorkspace);
1239
}
1240
1241
const result = await window.showInformationMessage(message, ...choices);
1242
const uri = Uri.file(repositoryPath);
1243
1244
if (result === open) {
1245
commands.executeCommand('vscode.openFolder', uri);
1246
} else if (result === addToWorkspace) {
1247
workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
1248
} else if (result === openNewWindow) {
1249
commands.executeCommand('vscode.openFolder', uri, true);
1250
} else {
1251
await this.model.openRepository(repositoryPath);
1252
}
1253
}
1254
1255
@command('git.openRepository', { repository: false })
1256
async openRepository(path?: string): Promise<void> {
1257
if (!path) {
1258
const result = await window.showOpenDialog({
1259
canSelectFiles: false,
1260
canSelectFolders: true,
1261
canSelectMany: false,
1262
defaultUri: Uri.file(os.homedir()),
1263
openLabel: l10n.t('Open Repository')
1264
});
1265
1266
if (!result || result.length === 0) {
1267
return;
1268
}
1269
1270
path = result[0].fsPath;
1271
}
1272
1273
await this.model.openRepository(path, true);
1274
}
1275
1276
@command('git.reopenClosedRepositories', { repository: false })
1277
async reopenClosedRepositories(): Promise<void> {
1278
if (this.model.closedRepositories.length === 0) {
1279
return;
1280
}
1281
1282
const closedRepositories: string[] = [];
1283
1284
const title = l10n.t('Reopen Closed Repositories');
1285
const placeHolder = l10n.t('Pick a repository to reopen');
1286
1287
const allRepositoriesLabel = l10n.t('All Repositories');
1288
const allRepositoriesQuickPickItem: QuickPickItem = { label: allRepositoriesLabel };
1289
const repositoriesQuickPickItems: QuickPickItem[] = this.model.closedRepositories
1290
.sort(compareRepositoryLabel).map(r => new RepositoryItem(r));
1291
1292
const items = this.model.closedRepositories.length === 1 ? [...repositoriesQuickPickItems] :
1293
[...repositoriesQuickPickItems, { label: '', kind: QuickPickItemKind.Separator }, allRepositoriesQuickPickItem];
1294
1295
const repositoryItem = await window.showQuickPick(items, { title, placeHolder });
1296
if (!repositoryItem) {
1297
return;
1298
}
1299
1300
if (repositoryItem === allRepositoriesQuickPickItem) {
1301
// All Repositories
1302
closedRepositories.push(...this.model.closedRepositories.values());
1303
} else {
1304
// One Repository
1305
closedRepositories.push((repositoryItem as RepositoryItem).path);
1306
}
1307
1308
for (const repository of closedRepositories) {
1309
await this.model.openRepository(repository, true);
1310
}
1311
}
1312
1313
@command('git.close', { repository: true })
1314
async close(repository: Repository, ...args: SourceControl[]): Promise<void> {
1315
const otherRepositories = args
1316
.map(sourceControl => this.model.getRepository(sourceControl))
1317
.filter(isDefined);
1318
1319
for (const r of [repository, ...otherRepositories]) {
1320
this.model.close(r);
1321
}
1322
}
1323
1324
@command('git.closeOtherRepositories', { repository: true })
1325
async closeOtherRepositories(repository: Repository, ...args: SourceControl[]): Promise<void> {
1326
const otherRepositories = args
1327
.map(sourceControl => this.model.getRepository(sourceControl))
1328
.filter(isDefined);
1329
1330
const selectedRepositories = [repository, ...otherRepositories];
1331
for (const r of this.model.repositories) {
1332
if (selectedRepositories.includes(r)) {
1333
continue;
1334
}
1335
this.model.close(r);
1336
}
1337
}
1338
1339
@command('git.openFile')
1340
async openFile(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
1341
const preserveFocus = arg instanceof Resource;
1342
1343
let uris: Uri[] | undefined;
1344
1345
if (arg instanceof Uri) {
1346
if (isGitUri(arg)) {
1347
uris = [Uri.file(fromGitUri(arg).path)];
1348
} else if (arg.scheme === 'file') {
1349
uris = [arg];
1350
}
1351
} else {
1352
let resource = arg;
1353
1354
if (!(resource instanceof Resource)) {
1355
// can happen when called from a keybinding
1356
resource = this.getSCMResource();
1357
}
1358
1359
if (resource) {
1360
uris = ([resource, ...resourceStates] as Resource[])
1361
.filter(r => r.type !== Status.DELETED && r.type !== Status.INDEX_DELETED)
1362
.map(r => r.resourceUri);
1363
} else if (window.activeTextEditor) {
1364
uris = [window.activeTextEditor.document.uri];
1365
}
1366
}
1367
1368
if (!uris) {
1369
return;
1370
}
1371
1372
const activeTextEditor = window.activeTextEditor;
1373
// Must extract these now because opening a new document will change the activeTextEditor reference
1374
const previousVisibleRanges = activeTextEditor?.visibleRanges;
1375
const previousURI = activeTextEditor?.document.uri;
1376
const previousSelection = activeTextEditor?.selection;
1377
1378
for (const uri of uris) {
1379
const opts: TextDocumentShowOptions = {
1380
preserveFocus,
1381
preview: false,
1382
viewColumn: ViewColumn.Active
1383
};
1384
1385
await commands.executeCommand('vscode.open', uri, {
1386
...opts,
1387
override: arg instanceof Resource && arg.type === Status.BOTH_MODIFIED ? false : undefined
1388
});
1389
1390
const document = window.activeTextEditor?.document;
1391
1392
// If the document doesn't match what we opened then don't attempt to select the range
1393
// Additionally if there was no previous document we don't have information to select a range
1394
if (document?.uri.toString() !== uri.toString() || !activeTextEditor || !previousURI || !previousSelection) {
1395
continue;
1396
}
1397
1398
// Check if active text editor has same path as other editor. we cannot compare via
1399
// URI.toString() here because the schemas can be different. Instead we just go by path.
1400
if (previousURI.path === uri.path && document) {
1401
// preserve not only selection but also visible range
1402
opts.selection = previousSelection;
1403
const editor = await window.showTextDocument(document, opts);
1404
// This should always be defined but just in case
1405
if (previousVisibleRanges && previousVisibleRanges.length > 0) {
1406
let rangeToReveal = previousVisibleRanges[0];
1407
if (previousSelection && previousVisibleRanges.length > 1) {
1408
// In case of multiple visible ranges, find the one that intersects with the selection
1409
rangeToReveal = previousVisibleRanges.find(r => r.intersection(previousSelection)) ?? rangeToReveal;
1410
}
1411
editor.revealRange(rangeToReveal);
1412
}
1413
}
1414
}
1415
}
1416
1417
@command('git.openFile2')
1418
async openFile2(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
1419
this.openFile(arg, ...resourceStates);
1420
}
1421
1422
@command('git.openHEADFile')
1423
async openHEADFile(arg?: Resource | Uri): Promise<void> {
1424
let resource: Resource | undefined = undefined;
1425
const preview = !(arg instanceof Resource);
1426
1427
if (arg instanceof Resource) {
1428
resource = arg;
1429
} else if (arg instanceof Uri) {
1430
resource = this.getSCMResource(arg);
1431
} else {
1432
resource = this.getSCMResource();
1433
}
1434
1435
if (!resource) {
1436
return;
1437
}
1438
1439
const HEAD = resource.leftUri;
1440
const basename = path.basename(resource.resourceUri.fsPath);
1441
const title = `${basename} (HEAD)`;
1442
1443
if (!HEAD) {
1444
window.showWarningMessage(l10n.t('HEAD version of "{0}" is not available.', path.basename(resource.resourceUri.fsPath)));
1445
return;
1446
}
1447
1448
const opts: TextDocumentShowOptions = {
1449
preview
1450
};
1451
1452
return await commands.executeCommand<void>('vscode.open', HEAD, opts, title);
1453
}
1454
1455
@command('git.openChange')
1456
async openChange(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
1457
let resources: Resource[] | undefined = undefined;
1458
1459
if (arg instanceof Uri) {
1460
const resource = this.getSCMResource(arg);
1461
if (resource !== undefined) {
1462
resources = [resource];
1463
}
1464
} else {
1465
let resource: Resource | undefined = undefined;
1466
1467
if (arg instanceof Resource) {
1468
resource = arg;
1469
} else {
1470
resource = this.getSCMResource();
1471
}
1472
1473
if (resource) {
1474
resources = [...resourceStates as Resource[], resource];
1475
}
1476
}
1477
1478
if (!resources) {
1479
return;
1480
}
1481
1482
for (const resource of resources) {
1483
await resource.openChange();
1484
}
1485
}
1486
1487
@command('git.compareWithWorkspace')
1488
async compareWithWorkspace(resource?: Resource): Promise<void> {
1489
if (!resource) {
1490
return;
1491
}
1492
1493
await resource.compareWithWorkspace();
1494
}
1495
1496
@command('git.rename', { repository: true })
1497
async rename(repository: Repository, fromUri: Uri | undefined): Promise<void> {
1498
fromUri = fromUri ?? window.activeTextEditor?.document.uri;
1499
1500
if (!fromUri) {
1501
return;
1502
}
1503
1504
const from = relativePath(repository.root, fromUri.fsPath);
1505
let to = await window.showInputBox({
1506
value: from,
1507
valueSelection: [from.length - path.basename(from).length, from.length]
1508
});
1509
1510
to = to?.trim();
1511
1512
if (!to) {
1513
return;
1514
}
1515
1516
await repository.move(from, to);
1517
1518
// Close active editor and open the renamed file
1519
await commands.executeCommand('workbench.action.closeActiveEditor');
1520
await commands.executeCommand('vscode.open', Uri.file(path.join(repository.root, to)), { viewColumn: ViewColumn.Active });
1521
}
1522
1523
@command('git.stage')
1524
async stage(...resourceStates: SourceControlResourceState[]): Promise<void> {
1525
this.logger.debug(`[CommandCenter][stage] git.stage ${resourceStates.length} `);
1526
1527
resourceStates = resourceStates.filter(s => !!s);
1528
1529
if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
1530
const resource = this.getSCMResource();
1531
1532
this.logger.debug(`[CommandCenter][stage] git.stage.getSCMResource ${resource ? resource.resourceUri.toString() : null} `);
1533
1534
if (!resource) {
1535
return;
1536
}
1537
1538
resourceStates = [resource];
1539
}
1540
1541
const selection = resourceStates.filter(s => s instanceof Resource) as Resource[];
1542
const { resolved, unresolved, deletionConflicts } = await categorizeResourceByResolution(selection);
1543
1544
if (unresolved.length > 0) {
1545
const message = unresolved.length > 1
1546
? l10n.t('Are you sure you want to stage {0} files with merge conflicts?', unresolved.length)
1547
: l10n.t('Are you sure you want to stage {0} with merge conflicts?', path.basename(unresolved[0].resourceUri.fsPath));
1548
1549
const yes = l10n.t('Yes');
1550
const pick = await window.showWarningMessage(message, { modal: true }, yes);
1551
1552
if (pick !== yes) {
1553
return;
1554
}
1555
}
1556
1557
try {
1558
await this.runByRepository(deletionConflicts.map(r => r.resourceUri), async (repository, resources) => {
1559
for (const resource of resources) {
1560
await this._stageDeletionConflict(repository, resource);
1561
}
1562
});
1563
} catch (err) {
1564
if (/Cancelled/.test(err.message)) {
1565
return;
1566
}
1567
1568
throw err;
1569
}
1570
1571
const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree);
1572
const untracked = selection.filter(s => s.resourceGroupType === ResourceGroupType.Untracked);
1573
const scmResources = [...workingTree, ...untracked, ...resolved, ...unresolved];
1574
1575
this.logger.debug(`[CommandCenter][stage] git.stage.scmResources ${scmResources.length} `);
1576
if (!scmResources.length) {
1577
return;
1578
}
1579
1580
const resources = scmResources.map(r => r.resourceUri);
1581
await this.runByRepository(resources, async (repository, resources) => repository.add(resources));
1582
}
1583
1584
@command('git.stageAll', { repository: true })
1585
async stageAll(repository: Repository): Promise<void> {
1586
const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates];
1587
const uris = resources.map(r => r.resourceUri);
1588
1589
if (uris.length > 0) {
1590
const config = workspace.getConfiguration('git', Uri.file(repository.root));
1591
const untrackedChanges = config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
1592
await repository.add(uris, untrackedChanges === 'mixed' ? undefined : { update: true });
1593
}
1594
}
1595
1596
private async _stageDeletionConflict(repository: Repository, uri: Uri): Promise<void> {
1597
const uriString = uri.toString();
1598
const resource = repository.mergeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
1599
1600
if (!resource) {
1601
return;
1602
}
1603
1604
if (resource.type === Status.DELETED_BY_THEM) {
1605
const keepIt = l10n.t('Keep Our Version');
1606
const deleteIt = l10n.t('Delete File');
1607
const result = await window.showInformationMessage(l10n.t('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);
1608
1609
if (result === keepIt) {
1610
await repository.add([uri]);
1611
} else if (result === deleteIt) {
1612
await repository.rm([uri]);
1613
} else {
1614
throw new Error('Cancelled');
1615
}
1616
} else if (resource.type === Status.DELETED_BY_US) {
1617
const keepIt = l10n.t('Keep Their Version');
1618
const deleteIt = l10n.t('Delete File');
1619
const result = await window.showInformationMessage(l10n.t('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);
1620
1621
if (result === keepIt) {
1622
await repository.add([uri]);
1623
} else if (result === deleteIt) {
1624
await repository.rm([uri]);
1625
} else {
1626
throw new Error('Cancelled');
1627
}
1628
}
1629
}
1630
1631
@command('git.stageAllTracked', { repository: true })
1632
async stageAllTracked(repository: Repository): Promise<void> {
1633
const resources = repository.workingTreeGroup.resourceStates
1634
.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
1635
const uris = resources.map(r => r.resourceUri);
1636
1637
await repository.add(uris);
1638
}
1639
1640
@command('git.stageAllUntracked', { repository: true })
1641
async stageAllUntracked(repository: Repository): Promise<void> {
1642
const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates]
1643
.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);
1644
const uris = resources.map(r => r.resourceUri);
1645
1646
await repository.add(uris);
1647
}
1648
1649
@command('git.stageAllMerge', { repository: true })
1650
async stageAllMerge(repository: Repository): Promise<void> {
1651
const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[];
1652
const { merge, unresolved, deletionConflicts } = await categorizeResourceByResolution(resources);
1653
1654
try {
1655
for (const deletionConflict of deletionConflicts) {
1656
await this._stageDeletionConflict(repository, deletionConflict.resourceUri);
1657
}
1658
} catch (err) {
1659
if (/Cancelled/.test(err.message)) {
1660
return;
1661
}
1662
1663
throw err;
1664
}
1665
1666
if (unresolved.length > 0) {
1667
const message = unresolved.length > 1
1668
? l10n.t('Are you sure you want to stage {0} files with merge conflicts?', merge.length)
1669
: l10n.t('Are you sure you want to stage {0} with merge conflicts?', path.basename(merge[0].resourceUri.fsPath));
1670
1671
const yes = l10n.t('Yes');
1672
const pick = await window.showWarningMessage(message, { modal: true }, yes);
1673
1674
if (pick !== yes) {
1675
return;
1676
}
1677
}
1678
1679
const uris = resources.map(r => r.resourceUri);
1680
1681
if (uris.length > 0) {
1682
await repository.add(uris);
1683
}
1684
}
1685
1686
@command('git.stageChange')
1687
async stageChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
1688
if (!uri) {
1689
return;
1690
}
1691
1692
const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];
1693
1694
if (!textEditor) {
1695
return;
1696
}
1697
1698
await this._stageChanges(textEditor, [changes[index]]);
1699
1700
const firstStagedLine = changes[index].modifiedStartLineNumber;
1701
textEditor.selections = [new Selection(firstStagedLine, 0, firstStagedLine, 0)];
1702
}
1703
1704
@command('git.diff.stageHunk')
1705
async diffStageHunk(changes: DiffEditorSelectionHunkToolbarContext | undefined): Promise<void> {
1706
if (changes) {
1707
this.diffStageHunkOrSelection(changes);
1708
} else {
1709
await this.stageHunkAtCursor();
1710
}
1711
}
1712
1713
@command('git.diff.stageSelection')
1714
async diffStageSelection(changes: DiffEditorSelectionHunkToolbarContext | undefined): Promise<void> {
1715
this.diffStageHunkOrSelection(changes);
1716
}
1717
1718
async diffStageHunkOrSelection(changes: DiffEditorSelectionHunkToolbarContext | undefined): Promise<void> {
1719
if (!changes) {
1720
return;
1721
}
1722
1723
let modifiedUri = changes.modifiedUri;
1724
let modifiedDocument: TextDocument | undefined;
1725
1726
if (!modifiedUri) {
1727
const textEditor = window.activeTextEditor;
1728
if (!textEditor) {
1729
return;
1730
}
1731
modifiedDocument = textEditor.document;
1732
modifiedUri = modifiedDocument.uri;
1733
}
1734
1735
if (modifiedUri.scheme !== 'file') {
1736
return;
1737
}
1738
1739
if (!modifiedDocument) {
1740
modifiedDocument = await workspace.openTextDocument(modifiedUri);
1741
}
1742
1743
const result = changes.originalWithModifiedChanges;
1744
await this.runByRepository(modifiedUri, async (repository, resource) =>
1745
await repository.stage(resource, result, modifiedDocument.encoding));
1746
}
1747
1748
private async stageHunkAtCursor(): Promise<void> {
1749
const textEditor = window.activeTextEditor;
1750
1751
if (!textEditor) {
1752
return;
1753
}
1754
1755
const workingTreeDiffInformation = getWorkingTreeDiffInformation(textEditor);
1756
if (!workingTreeDiffInformation) {
1757
return;
1758
}
1759
1760
const workingTreeLineChanges = toLineChanges(workingTreeDiffInformation);
1761
const modifiedDocument = textEditor.document;
1762
const cursorPosition = textEditor.selection.active;
1763
1764
// Find the hunk that contains the cursor position
1765
const hunkAtCursor = workingTreeLineChanges.find(change => {
1766
const hunkRange = getModifiedRange(modifiedDocument, change);
1767
return hunkRange.contains(cursorPosition);
1768
});
1769
1770
if (!hunkAtCursor) {
1771
window.showInformationMessage(l10n.t('No hunk found at cursor position.'));
1772
return;
1773
}
1774
1775
await this._stageChanges(textEditor, [hunkAtCursor]);
1776
}
1777
1778
@command('git.stageSelectedRanges')
1779
async stageSelectedChanges(): Promise<void> {
1780
const textEditor = window.activeTextEditor;
1781
1782
if (!textEditor) {
1783
return;
1784
}
1785
1786
const workingTreeDiffInformation = getWorkingTreeDiffInformation(textEditor);
1787
if (!workingTreeDiffInformation) {
1788
return;
1789
}
1790
1791
const workingTreeLineChanges = toLineChanges(workingTreeDiffInformation);
1792
1793
this.logger.trace(`[CommandCenter][stageSelectedChanges] diffInformation: ${JSON.stringify(workingTreeDiffInformation)}`);
1794
this.logger.trace(`[CommandCenter][stageSelectedChanges] diffInformation changes: ${JSON.stringify(workingTreeLineChanges)}`);
1795
1796
const modifiedDocument = textEditor.document;
1797
const selectedLines = toLineRanges(textEditor.selections, modifiedDocument);
1798
const selectedChanges = workingTreeLineChanges
1799
.map(change => selectedLines.reduce<LineChange | null>((result, range) => result || intersectDiffWithRange(modifiedDocument, change, range), null))
1800
.filter(d => !!d) as LineChange[];
1801
1802
this.logger.trace(`[CommandCenter][stageSelectedChanges] selectedChanges: ${JSON.stringify(selectedChanges)}`);
1803
1804
if (!selectedChanges.length) {
1805
window.showInformationMessage(l10n.t('The selection range does not contain any changes.'));
1806
return;
1807
}
1808
1809
await this._stageChanges(textEditor, selectedChanges);
1810
}
1811
1812
@command('git.stageFile')
1813
async stageFile(uri: Uri): Promise<void> {
1814
uri = uri ?? window.activeTextEditor?.document.uri;
1815
1816
if (!uri) {
1817
return;
1818
}
1819
1820
const repository = this.model.getRepository(uri);
1821
if (!repository) {
1822
return;
1823
}
1824
1825
const resources = [
1826
...repository.workingTreeGroup.resourceStates,
1827
...repository.untrackedGroup.resourceStates]
1828
.filter(r => r.multiFileDiffEditorModifiedUri?.toString() === uri.toString())
1829
.map(r => r.resourceUri);
1830
1831
if (resources.length === 0) {
1832
return;
1833
}
1834
1835
await repository.add(resources);
1836
}
1837
1838
@command('git.acceptMerge')
1839
async acceptMerge(_uri: Uri | unknown): Promise<void> {
1840
const { activeTab } = window.tabGroups.activeTabGroup;
1841
if (!activeTab) {
1842
return;
1843
}
1844
1845
if (!(activeTab.input instanceof TabInputTextMerge)) {
1846
return;
1847
}
1848
1849
const uri = activeTab.input.result;
1850
1851
const repository = this.model.getRepository(uri);
1852
if (!repository) {
1853
console.log(`FAILED to complete merge because uri ${uri.toString()} doesn't belong to any repository`);
1854
return;
1855
}
1856
1857
const result = await commands.executeCommand('mergeEditor.acceptMerge') as { successful: boolean };
1858
if (result.successful) {
1859
await repository.add([uri]);
1860
await commands.executeCommand('workbench.view.scm');
1861
}
1862
1863
/*
1864
if (!(uri instanceof Uri)) {
1865
return;
1866
}
1867
1868
1869
1870
1871
// make sure to save the merged document
1872
const doc = workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString());
1873
if (!doc) {
1874
console.log(`FAILED to complete merge because uri ${uri.toString()} doesn't match a document`);
1875
return;
1876
}
1877
if (doc.isDirty) {
1878
await doc.save();
1879
}
1880
1881
// find the merge editor tabs for the resource in question and close them all
1882
let didCloseTab = false;
1883
const mergeEditorTabs = window.tabGroups.all.map(group => group.tabs.filter(tab => tab.input instanceof TabInputTextMerge && tab.input.result.toString() === uri.toString())).flat();
1884
if (mergeEditorTabs.includes(activeTab)) {
1885
didCloseTab = await window.tabGroups.close(mergeEditorTabs, true);
1886
}
1887
1888
// Only stage if the merge editor has been successfully closed. That means all conflicts have been
1889
// handled or unhandled conflicts are OK by the user.
1890
if (didCloseTab) {
1891
await repository.add([uri]);
1892
await commands.executeCommand('workbench.view.scm');
1893
}*/
1894
}
1895
1896
@command('git.runGitMerge')
1897
async runGitMergeNoDiff3(): Promise<void> {
1898
await this.runGitMerge(false);
1899
}
1900
1901
@command('git.runGitMergeDiff3')
1902
async runGitMergeDiff3(): Promise<void> {
1903
await this.runGitMerge(true);
1904
}
1905
1906
private async runGitMerge(diff3: boolean): Promise<void> {
1907
const { activeTab } = window.tabGroups.activeTabGroup;
1908
if (!activeTab) {
1909
return;
1910
}
1911
1912
const input = activeTab.input;
1913
if (!(input instanceof TabInputTextMerge)) {
1914
return;
1915
}
1916
1917
const result = await this.git.mergeFile({
1918
basePath: input.base.fsPath,
1919
input1Path: input.input1.fsPath,
1920
input2Path: input.input2.fsPath,
1921
diff3,
1922
});
1923
1924
const doc = workspace.textDocuments.find(doc => doc.uri.toString() === input.result.toString());
1925
if (!doc) {
1926
return;
1927
}
1928
const e = new WorkspaceEdit();
1929
1930
e.replace(
1931
input.result,
1932
new Range(
1933
new Position(0, 0),
1934
new Position(doc.lineCount, 0),
1935
),
1936
result
1937
);
1938
await workspace.applyEdit(e);
1939
}
1940
1941
private async _stageChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
1942
const modifiedDocument = textEditor.document;
1943
const modifiedUri = modifiedDocument.uri;
1944
1945
if (modifiedUri.scheme !== 'file') {
1946
return;
1947
}
1948
1949
const originalUri = toGitUri(modifiedUri, '~');
1950
const originalDocument = await workspace.openTextDocument(originalUri);
1951
const result = applyLineChanges(originalDocument, modifiedDocument, changes);
1952
1953
await this.runByRepository(modifiedUri, async (repository, resource) =>
1954
await repository.stage(resource, result, modifiedDocument.encoding));
1955
}
1956
1957
@command('git.revertChange')
1958
async revertChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
1959
if (!uri) {
1960
return;
1961
}
1962
1963
const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];
1964
1965
if (!textEditor) {
1966
return;
1967
}
1968
1969
await this._revertChanges(textEditor, [...changes.slice(0, index), ...changes.slice(index + 1)]);
1970
1971
const firstStagedLine = changes[index].modifiedStartLineNumber;
1972
textEditor.selections = [new Selection(firstStagedLine, 0, firstStagedLine, 0)];
1973
}
1974
1975
@command('git.revertSelectedRanges')
1976
async revertSelectedRanges(): Promise<void> {
1977
const textEditor = window.activeTextEditor;
1978
1979
if (!textEditor) {
1980
return;
1981
}
1982
1983
const workingTreeDiffInformation = getWorkingTreeDiffInformation(textEditor);
1984
if (!workingTreeDiffInformation) {
1985
return;
1986
}
1987
1988
const workingTreeLineChanges = toLineChanges(workingTreeDiffInformation);
1989
1990
this.logger.trace(`[CommandCenter][revertSelectedRanges] diffInformation: ${JSON.stringify(workingTreeDiffInformation)}`);
1991
this.logger.trace(`[CommandCenter][revertSelectedRanges] diffInformation changes: ${JSON.stringify(workingTreeLineChanges)}`);
1992
1993
const modifiedDocument = textEditor.document;
1994
const selections = textEditor.selections;
1995
const selectedChanges = workingTreeLineChanges.filter(change => {
1996
const modifiedRange = getModifiedRange(modifiedDocument, change);
1997
return selections.every(selection => !selection.intersection(modifiedRange));
1998
});
1999
2000
if (selectedChanges.length === workingTreeLineChanges.length) {
2001
window.showInformationMessage(l10n.t('The selection range does not contain any changes.'));
2002
return;
2003
}
2004
2005
this.logger.trace(`[CommandCenter][revertSelectedRanges] selectedChanges: ${JSON.stringify(selectedChanges)}`);
2006
2007
const selectionsBeforeRevert = textEditor.selections;
2008
await this._revertChanges(textEditor, selectedChanges);
2009
textEditor.selections = selectionsBeforeRevert;
2010
}
2011
2012
private async _revertChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
2013
const modifiedDocument = textEditor.document;
2014
const modifiedUri = modifiedDocument.uri;
2015
2016
if (modifiedUri.scheme !== 'file') {
2017
return;
2018
}
2019
2020
const originalUri = toGitUri(modifiedUri, '~');
2021
const originalDocument = await workspace.openTextDocument(originalUri);
2022
const visibleRangesBeforeRevert = textEditor.visibleRanges;
2023
const result = applyLineChanges(originalDocument, modifiedDocument, changes);
2024
2025
const edit = new WorkspaceEdit();
2026
edit.replace(modifiedUri, new Range(new Position(0, 0), modifiedDocument.lineAt(modifiedDocument.lineCount - 1).range.end), result);
2027
workspace.applyEdit(edit);
2028
2029
await modifiedDocument.save();
2030
2031
textEditor.revealRange(visibleRangesBeforeRevert[0]);
2032
}
2033
2034
@command('git.unstage')
2035
async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
2036
resourceStates = resourceStates.filter(s => !!s);
2037
2038
if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
2039
const resource = this.getSCMResource();
2040
2041
if (!resource) {
2042
return;
2043
}
2044
2045
resourceStates = [resource];
2046
}
2047
2048
const scmResources = resourceStates
2049
.filter(s => s instanceof Resource && s.resourceGroupType === ResourceGroupType.Index) as Resource[];
2050
2051
if (!scmResources.length) {
2052
return;
2053
}
2054
2055
const resources = scmResources.map(r => r.resourceUri);
2056
await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
2057
}
2058
2059
@command('git.unstageAll', { repository: true })
2060
async unstageAll(repository: Repository): Promise<void> {
2061
await repository.revert([]);
2062
}
2063
2064
@command('git.unstageSelectedRanges')
2065
async unstageSelectedRanges(): Promise<void> {
2066
const textEditor = window.activeTextEditor;
2067
2068
if (!textEditor) {
2069
return;
2070
}
2071
2072
const modifiedDocument = textEditor.document;
2073
const modifiedUri = modifiedDocument.uri;
2074
2075
const repository = this.model.getRepository(modifiedUri);
2076
if (!repository) {
2077
return;
2078
}
2079
2080
const resource = repository.indexGroup.resourceStates
2081
.find(r => pathEquals(r.resourceUri.fsPath, modifiedUri.fsPath));
2082
if (!resource) {
2083
return;
2084
}
2085
2086
const indexDiffInformation = getIndexDiffInformation(textEditor);
2087
if (!indexDiffInformation) {
2088
return;
2089
}
2090
2091
const indexLineChanges = toLineChanges(indexDiffInformation);
2092
2093
this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation: ${JSON.stringify(indexDiffInformation)}`);
2094
this.logger.trace(`[CommandCenter][unstageSelectedRanges] diffInformation changes: ${JSON.stringify(indexLineChanges)}`);
2095
2096
const originalUri = toGitUri(resource.original, 'HEAD');
2097
const originalDocument = await workspace.openTextDocument(originalUri);
2098
const selectedLines = toLineRanges(textEditor.selections, modifiedDocument);
2099
2100
const selectedDiffs = indexLineChanges
2101
.map(change => selectedLines.reduce<LineChange | null>((result, range) => result || intersectDiffWithRange(modifiedDocument, change, range), null))
2102
.filter(c => !!c) as LineChange[];
2103
2104
if (!selectedDiffs.length) {
2105
window.showInformationMessage(l10n.t('The selection range does not contain any changes.'));
2106
return;
2107
}
2108
2109
this.logger.trace(`[CommandCenter][unstageSelectedRanges] selectedDiffs: ${JSON.stringify(selectedDiffs)}`);
2110
2111
// if (modifiedUri.scheme === 'file') {
2112
// // Editor
2113
// this.logger.trace(`[CommandCenter][unstageSelectedRanges] changes: ${JSON.stringify(selectedDiffs)}`);
2114
// await this._unstageChanges(textEditor, selectedDiffs);
2115
// return;
2116
// }
2117
2118
const selectedDiffsInverted = selectedDiffs.map(invertLineChange);
2119
this.logger.trace(`[CommandCenter][unstageSelectedRanges] selectedDiffsInverted: ${JSON.stringify(selectedDiffsInverted)}`);
2120
2121
const result = applyLineChanges(modifiedDocument, originalDocument, selectedDiffsInverted);
2122
await repository.stage(modifiedDocument.uri, result, modifiedDocument.encoding);
2123
}
2124
2125
@command('git.unstageFile')
2126
async unstageFile(uri: Uri): Promise<void> {
2127
uri = uri ?? window.activeTextEditor?.document.uri;
2128
2129
if (!uri) {
2130
return;
2131
}
2132
2133
const repository = this.model.getRepository(uri);
2134
if (!repository) {
2135
return;
2136
}
2137
2138
const resources = repository.indexGroup.resourceStates
2139
.filter(r => r.multiFileDiffEditorModifiedUri?.toString() === uri.toString())
2140
.map(r => r.resourceUri);
2141
2142
if (resources.length === 0) {
2143
return;
2144
}
2145
2146
await repository.revert(resources);
2147
}
2148
2149
@command('git.unstageChange')
2150
async unstageChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
2151
if (!uri) {
2152
return;
2153
}
2154
2155
const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];
2156
if (!textEditor) {
2157
return;
2158
}
2159
2160
await this._unstageChanges(textEditor, [changes[index]]);
2161
}
2162
2163
private async _unstageChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
2164
const modifiedDocument = textEditor.document;
2165
const modifiedUri = modifiedDocument.uri;
2166
2167
if (modifiedUri.scheme !== 'file') {
2168
return;
2169
}
2170
2171
const workingTreeDiffInformation = getWorkingTreeDiffInformation(textEditor);
2172
if (!workingTreeDiffInformation) {
2173
return;
2174
}
2175
2176
// Approach to unstage change(s):
2177
// - use file on disk as original document
2178
// - revert all changes from the working tree
2179
// - revert the specify change(s) from the index
2180
const workingTreeDiffs = toLineChanges(workingTreeDiffInformation);
2181
const workingTreeDiffsInverted = workingTreeDiffs.map(invertLineChange);
2182
const changesInverted = changes.map(invertLineChange);
2183
const diffsInverted = [...changesInverted, ...workingTreeDiffsInverted].sort(compareLineChanges);
2184
2185
const originalUri = toGitUri(modifiedUri, 'HEAD');
2186
const originalDocument = await workspace.openTextDocument(originalUri);
2187
const result = applyLineChanges(modifiedDocument, originalDocument, diffsInverted);
2188
2189
await this.runByRepository(modifiedUri, async (repository, resource) =>
2190
await repository.stage(resource, result, modifiedDocument.encoding));
2191
}
2192
2193
@command('git.clean')
2194
async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
2195
// Remove duplicate resources
2196
const resourceUris = new Set<string>();
2197
resourceStates = resourceStates.filter(s => {
2198
if (s === undefined) {
2199
return false;
2200
}
2201
2202
if (resourceUris.has(s.resourceUri.toString())) {
2203
return false;
2204
}
2205
2206
resourceUris.add(s.resourceUri.toString());
2207
return true;
2208
});
2209
2210
if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
2211
const resource = this.getSCMResource();
2212
2213
if (!resource) {
2214
return;
2215
}
2216
2217
resourceStates = [resource];
2218
}
2219
2220
const scmResources = resourceStates.filter(s => s instanceof Resource
2221
&& (s.resourceGroupType === ResourceGroupType.WorkingTree || s.resourceGroupType === ResourceGroupType.Untracked)) as Resource[];
2222
2223
if (!scmResources.length) {
2224
return;
2225
}
2226
2227
await this._cleanAll(scmResources);
2228
}
2229
2230
@command('git.cleanAll', { repository: true })
2231
async cleanAll(repository: Repository): Promise<void> {
2232
await this._cleanAll(repository.workingTreeGroup.resourceStates);
2233
}
2234
2235
@command('git.cleanAllTracked', { repository: true })
2236
async cleanAllTracked(repository: Repository): Promise<void> {
2237
const resources = repository.workingTreeGroup.resourceStates
2238
.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
2239
2240
if (resources.length === 0) {
2241
return;
2242
}
2243
2244
await this._cleanTrackedChanges(resources);
2245
}
2246
2247
@command('git.cleanAllUntracked', { repository: true })
2248
async cleanAllUntracked(repository: Repository): Promise<void> {
2249
const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates]
2250
.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);
2251
2252
if (resources.length === 0) {
2253
return;
2254
}
2255
2256
await this._cleanUntrackedChanges(resources);
2257
}
2258
2259
private async _cleanAll(resources: Resource[]): Promise<void> {
2260
if (resources.length === 0) {
2261
return;
2262
}
2263
2264
const trackedResources = resources.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
2265
const untrackedResources = resources.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);
2266
2267
if (untrackedResources.length === 0) {
2268
// Tracked files only
2269
await this._cleanTrackedChanges(resources);
2270
} else if (trackedResources.length === 0) {
2271
// Untracked files only
2272
await this._cleanUntrackedChanges(resources);
2273
} else {
2274
// Tracked & Untracked files
2275
const [untrackedMessage, untrackedMessageDetail] = this.getDiscardUntrackedChangesDialogDetails(untrackedResources);
2276
2277
const trackedMessage = trackedResources.length === 1
2278
? l10n.t('\n\nAre you sure you want to discard changes in \'{0}\'?', path.basename(trackedResources[0].resourceUri.fsPath))
2279
: l10n.t('\n\nAre you sure you want to discard ALL changes in {0} files?', trackedResources.length);
2280
2281
const yesTracked = trackedResources.length === 1
2282
? l10n.t('Discard 1 Tracked File')
2283
: l10n.t('Discard All {0} Tracked Files', trackedResources.length);
2284
2285
const yesAll = l10n.t('Discard All {0} Files', resources.length);
2286
const pick = await window.showWarningMessage(`${untrackedMessage} ${untrackedMessageDetail}${trackedMessage}\n\nThis is IRREVERSIBLE!\nYour current working set will be FOREVER LOST if you proceed.`, { modal: true }, yesTracked, yesAll);
2287
2288
if (pick === yesTracked) {
2289
resources = trackedResources;
2290
} else if (pick !== yesAll) {
2291
return;
2292
}
2293
2294
const resourceUris = resources.map(r => r.resourceUri);
2295
await this.runByRepository(resourceUris, async (repository, resources) => repository.clean(resources));
2296
}
2297
}
2298
2299
private async _cleanTrackedChanges(resources: Resource[]): Promise<void> {
2300
const allResourcesDeleted = resources.every(r => r.type === Status.DELETED);
2301
2302
const message = allResourcesDeleted
2303
? resources.length === 1
2304
? l10n.t('Are you sure you want to restore \'{0}\'?', path.basename(resources[0].resourceUri.fsPath))
2305
: l10n.t('Are you sure you want to restore ALL {0} files?', resources.length)
2306
: resources.length === 1
2307
? l10n.t('Are you sure you want to discard changes in \'{0}\'?', path.basename(resources[0].resourceUri.fsPath))
2308
: l10n.t('Are you sure you want to discard ALL changes in {0} files?\n\nThis is IRREVERSIBLE!\nYour current working set will be FOREVER LOST if you proceed.', resources.length);
2309
2310
const yes = allResourcesDeleted
2311
? resources.length === 1
2312
? l10n.t('Restore File')
2313
: l10n.t('Restore All {0} Files', resources.length)
2314
: resources.length === 1
2315
? l10n.t('Discard File')
2316
: l10n.t('Discard All {0} Files', resources.length);
2317
2318
const pick = await window.showWarningMessage(message, { modal: true }, yes);
2319
2320
if (pick !== yes) {
2321
return;
2322
}
2323
2324
const resourceUris = resources.map(r => r.resourceUri);
2325
await this.runByRepository(resourceUris, async (repository, resources) => repository.clean(resources));
2326
}
2327
2328
private async _cleanUntrackedChanges(resources: Resource[]): Promise<void> {
2329
const [message, messageDetail, primaryAction] = this.getDiscardUntrackedChangesDialogDetails(resources);
2330
const pick = await window.showWarningMessage(message, { detail: messageDetail, modal: true }, primaryAction);
2331
2332
if (pick !== primaryAction) {
2333
return;
2334
}
2335
2336
const resourceUris = resources.map(r => r.resourceUri);
2337
await this.runByRepository(resourceUris, async (repository, resources) => repository.clean(resources));
2338
}
2339
2340
private getDiscardUntrackedChangesDialogDetails(resources: Resource[]): [string, string, string] {
2341
const config = workspace.getConfiguration('git');
2342
const discardUntrackedChangesToTrash = config.get<boolean>('discardUntrackedChangesToTrash', true) && !isRemote && !isLinuxSnap;
2343
2344
const messageWarning = !discardUntrackedChangesToTrash
2345
? resources.length === 1
2346
? '\n\n' + l10n.t('This is IRREVERSIBLE!\nThis file will be FOREVER LOST if you proceed.')
2347
: '\n\n' + l10n.t('This is IRREVERSIBLE!\nThese files will be FOREVER LOST if you proceed.')
2348
: '';
2349
2350
const message = resources.length === 1
2351
? l10n.t('Are you sure you want to DELETE the following untracked file: \'{0}\'?{1}', path.basename(resources[0].resourceUri.fsPath), messageWarning)
2352
: l10n.t('Are you sure you want to DELETE the {0} untracked files?{1}', resources.length, messageWarning);
2353
2354
const messageDetail = discardUntrackedChangesToTrash
2355
? isWindows
2356
? resources.length === 1
2357
? l10n.t('You can restore this file from the Recycle Bin.')
2358
: l10n.t('You can restore these files from the Recycle Bin.')
2359
: resources.length === 1
2360
? l10n.t('You can restore this file from the Trash.')
2361
: l10n.t('You can restore these files from the Trash.')
2362
: '';
2363
2364
const primaryAction = discardUntrackedChangesToTrash
2365
? isWindows
2366
? l10n.t('Move to Recycle Bin')
2367
: l10n.t('Move to Trash')
2368
: resources.length === 1
2369
? l10n.t('Delete File')
2370
: l10n.t('Delete All {0} Files', resources.length);
2371
2372
return [message, messageDetail, primaryAction];
2373
}
2374
2375
private async smartCommit(
2376
repository: Repository,
2377
getCommitMessage: () => Promise<string | undefined>,
2378
opts: CommitOptions
2379
): Promise<void> {
2380
const config = workspace.getConfiguration('git', Uri.file(repository.root));
2381
let promptToSaveFilesBeforeCommit = config.get<'always' | 'staged' | 'never'>('promptToSaveFilesBeforeCommit');
2382
2383
// migration
2384
if (promptToSaveFilesBeforeCommit as any === true) {
2385
promptToSaveFilesBeforeCommit = 'always';
2386
} else if (promptToSaveFilesBeforeCommit as any === false) {
2387
promptToSaveFilesBeforeCommit = 'never';
2388
}
2389
2390
let enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
2391
const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
2392
let noStagedChanges = repository.indexGroup.resourceStates.length === 0;
2393
let noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
2394
2395
if (!opts.empty) {
2396
if (promptToSaveFilesBeforeCommit !== 'never') {
2397
let documents = workspace.textDocuments
2398
.filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath));
2399
2400
if (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) {
2401
documents = documents
2402
.filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath)));
2403
}
2404
2405
if (documents.length > 0) {
2406
const message = documents.length === 1
2407
? l10n.t('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))
2408
: l10n.t('There are {0} unsaved files.\n\nWould you like to save them before committing?', documents.length);
2409
const saveAndCommit = l10n.t('Save All & Commit Changes');
2410
const commit = l10n.t('Commit Changes');
2411
const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit);
2412
2413
if (pick === saveAndCommit) {
2414
await Promise.all(documents.map(d => d.save()));
2415
2416
// After saving the dirty documents, if there are any documents that are part of the
2417
// index group we have to add them back in order for the saved changes to be committed
2418
documents = documents
2419
.filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath)));
2420
await repository.add(documents.map(d => d.uri));
2421
2422
noStagedChanges = repository.indexGroup.resourceStates.length === 0;
2423
noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
2424
} else if (pick !== commit) {
2425
return; // do not commit on cancel
2426
}
2427
}
2428
}
2429
2430
// no changes, and the user has not configured to commit all in this case
2431
if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit && !opts.all && !opts.amend) {
2432
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
2433
2434
if (!suggestSmartCommit) {
2435
return;
2436
}
2437
2438
// prompt the user if we want to commit all or not
2439
const message = l10n.t('There are no staged changes to commit.\n\nWould you like to stage all your changes and commit them directly?');
2440
const yes = l10n.t('Yes');
2441
const always = l10n.t('Always');
2442
const never = l10n.t('Never');
2443
const pick = await window.showWarningMessage(message, { modal: true }, yes, always, never);
2444
2445
if (pick === always) {
2446
enableSmartCommit = true;
2447
config.update('enableSmartCommit', true, true);
2448
} else if (pick === never) {
2449
config.update('suggestSmartCommit', false, true);
2450
return;
2451
} else if (pick === yes) {
2452
enableSmartCommit = true;
2453
} else {
2454
// Cancel
2455
return;
2456
}
2457
}
2458
2459
// smart commit
2460
if (enableSmartCommit && !opts.all) {
2461
opts = { ...opts, all: noStagedChanges };
2462
}
2463
}
2464
2465
// enable signing of commits if configured
2466
opts.signCommit = enableCommitSigning;
2467
2468
if (config.get<boolean>('alwaysSignOff')) {
2469
opts.signoff = true;
2470
}
2471
2472
if (config.get<boolean>('useEditorAsCommitInput')) {
2473
opts.useEditor = true;
2474
2475
if (config.get<boolean>('verboseCommit')) {
2476
opts.verbose = true;
2477
}
2478
}
2479
2480
const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges');
2481
2482
if (
2483
(
2484
// no changes
2485
(noStagedChanges && noUnstagedChanges)
2486
// or no staged changes and not `all`
2487
|| (!opts.all && noStagedChanges)
2488
// no staged changes and no tracked unstaged changes
2489
|| (noStagedChanges && smartCommitChanges === 'tracked' && repository.workingTreeGroup.resourceStates.every(r => r.type === Status.UNTRACKED))
2490
)
2491
// amend allows changing only the commit message
2492
&& !opts.amend
2493
&& !opts.empty
2494
// merge not in progress
2495
&& !repository.mergeInProgress
2496
// rebase not in progress
2497
&& repository.rebaseCommit === undefined
2498
) {
2499
const commitAnyway = l10n.t('Create Empty Commit');
2500
const answer = await window.showInformationMessage(l10n.t('There are no changes to commit.'), commitAnyway);
2501
2502
if (answer !== commitAnyway) {
2503
return;
2504
}
2505
2506
opts.empty = true;
2507
}
2508
2509
if (opts.noVerify) {
2510
if (!config.get<boolean>('allowNoVerifyCommit')) {
2511
await window.showErrorMessage(l10n.t('Commits without verification are not allowed, please enable them with the "git.allowNoVerifyCommit" setting.'));
2512
return;
2513
}
2514
2515
if (config.get<boolean>('confirmNoVerifyCommit')) {
2516
const message = l10n.t('You are about to commit your changes without verification, this skips pre-commit hooks and can be undesirable.\n\nAre you sure to continue?');
2517
const yes = l10n.t('OK');
2518
const neverAgain = l10n.t('OK, Don\'t Ask Again');
2519
const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);
2520
2521
if (pick === neverAgain) {
2522
config.update('confirmNoVerifyCommit', false, true);
2523
} else if (pick !== yes) {
2524
return;
2525
}
2526
}
2527
}
2528
2529
const message = await getCommitMessage();
2530
2531
if (!message && !opts.amend && !opts.useEditor) {
2532
return;
2533
}
2534
2535
if (opts.all && smartCommitChanges === 'tracked') {
2536
opts.all = 'tracked';
2537
}
2538
2539
if (opts.all && config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges') !== 'mixed') {
2540
opts.all = 'tracked';
2541
}
2542
2543
// Diagnostics commit hook
2544
const diagnosticsResult = await evaluateDiagnosticsCommitHook(repository, opts);
2545
if (!diagnosticsResult) {
2546
return;
2547
}
2548
2549
// Branch protection commit hook
2550
const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;
2551
if (repository.isBranchProtected() && (branchProtectionPrompt === 'alwaysPrompt' || branchProtectionPrompt === 'alwaysCommitToNewBranch')) {
2552
const commitToNewBranch = l10n.t('Commit to a New Branch');
2553
2554
let pick: string | undefined = commitToNewBranch;
2555
2556
if (branchProtectionPrompt === 'alwaysPrompt') {
2557
const message = l10n.t('You are trying to commit to a protected branch and you might not have permission to push your commits to the remote.\n\nHow would you like to proceed?');
2558
const commit = l10n.t('Commit Anyway');
2559
2560
pick = await window.showWarningMessage(message, { modal: true }, commitToNewBranch, commit);
2561
}
2562
2563
if (!pick) {
2564
return;
2565
} else if (pick === commitToNewBranch) {
2566
const branchName = await this.promptForBranchName(repository);
2567
2568
if (!branchName) {
2569
return;
2570
}
2571
2572
await repository.branch(branchName, true);
2573
}
2574
}
2575
2576
await repository.commit(message, opts);
2577
}
2578
2579
private async commitWithAnyInput(repository: Repository, opts: CommitOptions): Promise<void> {
2580
const message = repository.inputBox.value;
2581
const root = Uri.file(repository.root);
2582
const config = workspace.getConfiguration('git', root);
2583
2584
const getCommitMessage = async () => {
2585
let _message: string | undefined = message;
2586
2587
if (!_message && !config.get<boolean>('useEditorAsCommitInput')) {
2588
const value: string | undefined = undefined;
2589
2590
if (opts && opts.amend && repository.HEAD && repository.HEAD.commit) {
2591
return undefined;
2592
}
2593
2594
const branchName = repository.headShortName;
2595
let placeHolder: string;
2596
2597
if (branchName) {
2598
placeHolder = l10n.t('Message (commit on "{0}")', branchName);
2599
} else {
2600
placeHolder = l10n.t('Commit message');
2601
}
2602
2603
_message = await window.showInputBox({
2604
value,
2605
placeHolder,
2606
prompt: l10n.t('Please provide a commit message'),
2607
ignoreFocusOut: true
2608
});
2609
}
2610
2611
return _message;
2612
};
2613
2614
await this.smartCommit(repository, getCommitMessage, opts);
2615
}
2616
2617
@command('git.commit', { repository: true })
2618
async commit(repository: Repository, postCommitCommand?: string | null): Promise<void> {
2619
await this.commitWithAnyInput(repository, { postCommitCommand });
2620
}
2621
2622
@command('git.commitAmend', { repository: true })
2623
async commitAmend(repository: Repository): Promise<void> {
2624
await this.commitWithAnyInput(repository, { amend: true });
2625
}
2626
2627
@command('git.commitSigned', { repository: true })
2628
async commitSigned(repository: Repository): Promise<void> {
2629
await this.commitWithAnyInput(repository, { signoff: true });
2630
}
2631
2632
@command('git.commitStaged', { repository: true })
2633
async commitStaged(repository: Repository): Promise<void> {
2634
await this.commitWithAnyInput(repository, { all: false });
2635
}
2636
2637
@command('git.commitStagedSigned', { repository: true })
2638
async commitStagedSigned(repository: Repository): Promise<void> {
2639
await this.commitWithAnyInput(repository, { all: false, signoff: true });
2640
}
2641
2642
@command('git.commitStagedAmend', { repository: true })
2643
async commitStagedAmend(repository: Repository): Promise<void> {
2644
await this.commitWithAnyInput(repository, { all: false, amend: true });
2645
}
2646
2647
@command('git.commitAll', { repository: true })
2648
async commitAll(repository: Repository): Promise<void> {
2649
await this.commitWithAnyInput(repository, { all: true });
2650
}
2651
2652
@command('git.commitAllSigned', { repository: true })
2653
async commitAllSigned(repository: Repository): Promise<void> {
2654
await this.commitWithAnyInput(repository, { all: true, signoff: true });
2655
}
2656
2657
@command('git.commitAllAmend', { repository: true })
2658
async commitAllAmend(repository: Repository): Promise<void> {
2659
await this.commitWithAnyInput(repository, { all: true, amend: true });
2660
}
2661
2662
@command('git.commitMessageAccept')
2663
async commitMessageAccept(arg?: Uri): Promise<void> {
2664
if (!arg && !window.activeTextEditor) { return; }
2665
arg ??= window.activeTextEditor!.document.uri;
2666
2667
// Close the tab
2668
this._closeEditorTab(arg);
2669
}
2670
2671
@command('git.commitMessageDiscard')
2672
async commitMessageDiscard(arg?: Uri): Promise<void> {
2673
if (!arg && !window.activeTextEditor) { return; }
2674
arg ??= window.activeTextEditor!.document.uri;
2675
2676
// Clear the contents of the editor
2677
const editors = window.visibleTextEditors
2678
.filter(e => e.document.languageId === 'git-commit' && e.document.uri.toString() === arg!.toString());
2679
2680
if (editors.length !== 1) { return; }
2681
2682
const commitMsgEditor = editors[0];
2683
const commitMsgDocument = commitMsgEditor.document;
2684
2685
const editResult = await commitMsgEditor.edit(builder => {
2686
const firstLine = commitMsgDocument.lineAt(0);
2687
const lastLine = commitMsgDocument.lineAt(commitMsgDocument.lineCount - 1);
2688
2689
builder.delete(new Range(firstLine.range.start, lastLine.range.end));
2690
});
2691
2692
if (!editResult) { return; }
2693
2694
// Save the document
2695
const saveResult = await commitMsgDocument.save();
2696
if (!saveResult) { return; }
2697
2698
// Close the tab
2699
this._closeEditorTab(arg);
2700
}
2701
2702
private _closeEditorTab(uri: Uri): void {
2703
const tabToClose = window.tabGroups.all.map(g => g.tabs).flat()
2704
.filter(t => t.input instanceof TabInputText && t.input.uri.toString() === uri.toString());
2705
2706
window.tabGroups.close(tabToClose);
2707
}
2708
2709
private async _commitEmpty(repository: Repository, noVerify?: boolean): Promise<void> {
2710
const root = Uri.file(repository.root);
2711
const config = workspace.getConfiguration('git', root);
2712
const shouldPrompt = config.get<boolean>('confirmEmptyCommits') === true;
2713
2714
if (shouldPrompt) {
2715
const message = l10n.t('Are you sure you want to create an empty commit?');
2716
const yes = l10n.t('Yes');
2717
const neverAgain = l10n.t('Yes, Don\'t Show Again');
2718
const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);
2719
2720
if (pick === neverAgain) {
2721
await config.update('confirmEmptyCommits', false, true);
2722
} else if (pick !== yes) {
2723
return;
2724
}
2725
}
2726
2727
await this.commitWithAnyInput(repository, { empty: true, noVerify });
2728
}
2729
2730
@command('git.commitEmpty', { repository: true })
2731
async commitEmpty(repository: Repository): Promise<void> {
2732
await this._commitEmpty(repository);
2733
}
2734
2735
@command('git.commitNoVerify', { repository: true })
2736
async commitNoVerify(repository: Repository): Promise<void> {
2737
await this.commitWithAnyInput(repository, { noVerify: true });
2738
}
2739
2740
@command('git.commitStagedNoVerify', { repository: true })
2741
async commitStagedNoVerify(repository: Repository): Promise<void> {
2742
await this.commitWithAnyInput(repository, { all: false, noVerify: true });
2743
}
2744
2745
@command('git.commitStagedSignedNoVerify', { repository: true })
2746
async commitStagedSignedNoVerify(repository: Repository): Promise<void> {
2747
await this.commitWithAnyInput(repository, { all: false, signoff: true, noVerify: true });
2748
}
2749
2750
@command('git.commitAmendNoVerify', { repository: true })
2751
async commitAmendNoVerify(repository: Repository): Promise<void> {
2752
await this.commitWithAnyInput(repository, { amend: true, noVerify: true });
2753
}
2754
2755
@command('git.commitSignedNoVerify', { repository: true })
2756
async commitSignedNoVerify(repository: Repository): Promise<void> {
2757
await this.commitWithAnyInput(repository, { signoff: true, noVerify: true });
2758
}
2759
2760
@command('git.commitStagedAmendNoVerify', { repository: true })
2761
async commitStagedAmendNoVerify(repository: Repository): Promise<void> {
2762
await this.commitWithAnyInput(repository, { all: false, amend: true, noVerify: true });
2763
}
2764
2765
@command('git.commitAllNoVerify', { repository: true })
2766
async commitAllNoVerify(repository: Repository): Promise<void> {
2767
await this.commitWithAnyInput(repository, { all: true, noVerify: true });
2768
}
2769
2770
@command('git.commitAllSignedNoVerify', { repository: true })
2771
async commitAllSignedNoVerify(repository: Repository): Promise<void> {
2772
await this.commitWithAnyInput(repository, { all: true, signoff: true, noVerify: true });
2773
}
2774
2775
@command('git.commitAllAmendNoVerify', { repository: true })
2776
async commitAllAmendNoVerify(repository: Repository): Promise<void> {
2777
await this.commitWithAnyInput(repository, { all: true, amend: true, noVerify: true });
2778
}
2779
2780
@command('git.commitEmptyNoVerify', { repository: true })
2781
async commitEmptyNoVerify(repository: Repository): Promise<void> {
2782
await this._commitEmpty(repository, true);
2783
}
2784
2785
@command('git.restoreCommitTemplate', { repository: true })
2786
async restoreCommitTemplate(repository: Repository): Promise<void> {
2787
repository.inputBox.value = await repository.getCommitTemplate();
2788
}
2789
2790
@command('git.undoCommit', { repository: true })
2791
async undoCommit(repository: Repository): Promise<void> {
2792
const HEAD = repository.HEAD;
2793
2794
if (!HEAD || !HEAD.commit) {
2795
window.showWarningMessage(l10n.t('Can\'t undo because HEAD doesn\'t point to any commit.'));
2796
return;
2797
}
2798
2799
const commit = await repository.getCommit('HEAD');
2800
2801
if (commit.parents.length > 1) {
2802
const yes = l10n.t('Undo merge commit');
2803
const result = await window.showWarningMessage(l10n.t('The last commit was a merge commit. Are you sure you want to undo it?'), { modal: true }, yes);
2804
2805
if (result !== yes) {
2806
return;
2807
}
2808
}
2809
2810
if (commit.parents.length > 0) {
2811
await repository.reset('HEAD~');
2812
} else {
2813
await repository.deleteRef('HEAD');
2814
await this.unstageAll(repository);
2815
}
2816
2817
repository.inputBox.value = commit.message;
2818
}
2819
2820
@command('git.checkout', { repository: true })
2821
async checkout(repository: Repository, treeish?: string): Promise<boolean> {
2822
return this._checkout(repository, { treeish });
2823
}
2824
2825
@command('git.graph.checkout', { repository: true })
2826
async checkout2(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise<void> {
2827
const historyItemRef = historyItem?.references?.find(r => r.id === historyItemRefId);
2828
if (!historyItemRef) {
2829
return;
2830
}
2831
2832
const config = workspace.getConfiguration('git', Uri.file(repository.root));
2833
const pullBeforeCheckout = config.get<boolean>('pullBeforeCheckout', false) === true;
2834
2835
// Branch, tag
2836
if (historyItemRef.id.startsWith('refs/heads/') || historyItemRef.id.startsWith('refs/tags/')) {
2837
await repository.checkout(historyItemRef.name, { pullBeforeCheckout });
2838
return;
2839
}
2840
2841
// Remote branch
2842
const branches = await repository.findTrackingBranches(historyItemRef.name);
2843
if (branches.length > 0) {
2844
await repository.checkout(branches[0].name!, { pullBeforeCheckout });
2845
} else {
2846
await repository.checkoutTracking(historyItemRef.name);
2847
}
2848
}
2849
2850
@command('git.checkoutDetached', { repository: true })
2851
async checkoutDetached(repository: Repository, treeish?: string): Promise<boolean> {
2852
return this._checkout(repository, { detached: true, treeish });
2853
}
2854
2855
@command('git.graph.checkoutDetached', { repository: true })
2856
async checkoutDetached2(repository: Repository, historyItem?: SourceControlHistoryItem): Promise<boolean> {
2857
if (!historyItem) {
2858
return false;
2859
}
2860
return this._checkout(repository, { detached: true, treeish: historyItem.id });
2861
}
2862
2863
private async _checkout(repository: Repository, opts?: { detached?: boolean; treeish?: string }): Promise<boolean> {
2864
if (typeof opts?.treeish === 'string') {
2865
await repository.checkout(opts?.treeish, opts);
2866
return true;
2867
}
2868
2869
const createBranch = new CreateBranchItem();
2870
const createBranchFrom = new CreateBranchFromItem();
2871
const checkoutDetached = new CheckoutDetachedItem();
2872
const picks: QuickPickItem[] = [];
2873
const commands: QuickPickItem[] = [];
2874
2875
if (!opts?.detached) {
2876
commands.push(createBranch, createBranchFrom, checkoutDetached);
2877
}
2878
2879
const disposables: Disposable[] = [];
2880
const quickPick = window.createQuickPick();
2881
quickPick.busy = true;
2882
quickPick.sortByLabel = false;
2883
quickPick.matchOnDetail = false;
2884
quickPick.placeholder = opts?.detached
2885
? l10n.t('Select a branch to checkout in detached mode')
2886
: l10n.t('Select a branch or tag to checkout');
2887
2888
quickPick.show();
2889
picks.push(... await createCheckoutItems(repository, opts?.detached));
2890
2891
const setQuickPickItems = () => {
2892
switch (true) {
2893
case quickPick.value === '':
2894
quickPick.items = [...commands, ...picks];
2895
break;
2896
case commands.length === 0:
2897
quickPick.items = picks;
2898
break;
2899
case picks.length === 0:
2900
quickPick.items = commands;
2901
break;
2902
default:
2903
quickPick.items = [...picks, { label: '', kind: QuickPickItemKind.Separator }, ...commands];
2904
break;
2905
}
2906
};
2907
2908
setQuickPickItems();
2909
quickPick.busy = false;
2910
2911
const choice = await new Promise<QuickPickItem | undefined>(c => {
2912
disposables.push(quickPick.onDidHide(() => c(undefined)));
2913
disposables.push(quickPick.onDidAccept(() => c(quickPick.activeItems[0])));
2914
disposables.push((quickPick.onDidTriggerItemButton((e) => {
2915
const button = e.button as QuickInputButton & { actual: RemoteSourceAction };
2916
const item = e.item as CheckoutItem;
2917
if (button.actual && item.refName) {
2918
button.actual.run(item.refRemote ? item.refName.substring(item.refRemote.length + 1) : item.refName);
2919
}
2920
2921
c(undefined);
2922
})));
2923
disposables.push(quickPick.onDidChangeValue(() => setQuickPickItems()));
2924
});
2925
2926
dispose(disposables);
2927
quickPick.dispose();
2928
2929
if (!choice) {
2930
return false;
2931
}
2932
2933
if (choice === createBranch) {
2934
await this._branch(repository, quickPick.value);
2935
} else if (choice === createBranchFrom) {
2936
await this._branch(repository, quickPick.value, true);
2937
} else if (choice === checkoutDetached) {
2938
return this._checkout(repository, { detached: true });
2939
} else {
2940
const item = choice as CheckoutItem;
2941
2942
try {
2943
await item.run(repository, opts);
2944
} catch (err) {
2945
if (err.gitErrorCode !== GitErrorCodes.DirtyWorkTree && err.gitErrorCode !== GitErrorCodes.WorktreeBranchAlreadyUsed) {
2946
throw err;
2947
}
2948
2949
if (err.gitErrorCode === GitErrorCodes.WorktreeBranchAlreadyUsed) {
2950
// Not checking out in a worktree (use standard error handling)
2951
if (!repository.dotGit.commonPath) {
2952
await this.handleWorktreeBranchAlreadyUsed(err);
2953
return false;
2954
}
2955
2956
// Check out in a worktree (check if worktree's main repository is open in workspace and if branch is already checked out in main repository)
2957
const commonPath = path.dirname(repository.dotGit.commonPath);
2958
if (workspace.workspaceFolders && workspace.workspaceFolders.some(folder => pathEquals(folder.uri.fsPath, commonPath))) {
2959
const mainRepository = this.model.getRepository(commonPath);
2960
if (mainRepository && item.refName && item.refName.replace(`${item.refRemote}/`, '') === mainRepository.HEAD?.name) {
2961
const message = l10n.t('Branch "{0}" is already checked out in the current window.', item.refName);
2962
await window.showErrorMessage(message, { modal: true });
2963
return false;
2964
}
2965
}
2966
2967
// Check out in a worktree, (branch is already checked out in existing worktree)
2968
await this.handleWorktreeBranchAlreadyUsed(err);
2969
return false;
2970
}
2971
2972
const stash = l10n.t('Stash & Checkout');
2973
const migrate = l10n.t('Migrate Changes');
2974
const force = l10n.t('Force Checkout');
2975
const choice = await window.showWarningMessage(l10n.t('Your local changes would be overwritten by checkout.'), { modal: true }, stash, migrate, force);
2976
2977
if (choice === force) {
2978
await this.cleanAll(repository);
2979
await item.run(repository, opts);
2980
} else if (choice === stash || choice === migrate) {
2981
if (await this._stash(repository, true)) {
2982
await item.run(repository, opts);
2983
2984
if (choice === migrate) {
2985
await this.stashPopLatest(repository);
2986
}
2987
}
2988
}
2989
}
2990
}
2991
2992
return true;
2993
}
2994
2995
@command('git.branch', { repository: true })
2996
async branch(repository: Repository, historyItem?: SourceControlHistoryItem): Promise<void> {
2997
await this._branch(repository, undefined, false, historyItem?.id);
2998
}
2999
3000
@command('git.branchFrom', { repository: true })
3001
async branchFrom(repository: Repository): Promise<void> {
3002
await this._branch(repository, undefined, true);
3003
}
3004
3005
private async generateRandomBranchName(repository: Repository, separator: string): Promise<string> {
3006
const config = workspace.getConfiguration('git');
3007
const branchRandomNameDictionary = config.get<string[]>('branchRandomName.dictionary')!;
3008
3009
const dictionaries: string[][] = [];
3010
for (const dictionary of branchRandomNameDictionary) {
3011
if (dictionary.toLowerCase() === 'adjectives') {
3012
dictionaries.push(adjectives);
3013
}
3014
if (dictionary.toLowerCase() === 'animals') {
3015
dictionaries.push(animals);
3016
}
3017
if (dictionary.toLowerCase() === 'colors') {
3018
dictionaries.push(colors);
3019
}
3020
if (dictionary.toLowerCase() === 'numbers') {
3021
dictionaries.push(NumberDictionary.generate({ length: 3 }));
3022
}
3023
}
3024
3025
if (dictionaries.length === 0) {
3026
return '';
3027
}
3028
3029
// 5 attempts to generate a random branch name
3030
for (let index = 0; index < 5; index++) {
3031
const randomName = uniqueNamesGenerator({
3032
dictionaries,
3033
length: dictionaries.length,
3034
separator
3035
});
3036
3037
// Check for local ref conflict
3038
const refs = await repository.getRefs({ pattern: `refs/heads/${randomName}` });
3039
if (refs.length === 0) {
3040
return randomName;
3041
}
3042
}
3043
3044
return '';
3045
}
3046
3047
private async promptForBranchName(repository: Repository, defaultName?: string, initialValue?: string): Promise<string> {
3048
const config = workspace.getConfiguration('git');
3049
const branchPrefix = config.get<string>('branchPrefix')!;
3050
const branchWhitespaceChar = config.get<string>('branchWhitespaceChar')!;
3051
const branchValidationRegex = config.get<string>('branchValidationRegex')!;
3052
const branchRandomNameEnabled = config.get<boolean>('branchRandomName.enable', false);
3053
const refs = await repository.getRefs({ pattern: 'refs/heads' });
3054
3055
if (defaultName) {
3056
return sanitizeBranchName(defaultName, branchWhitespaceChar);
3057
}
3058
3059
const getBranchName = async (): Promise<string> => {
3060
const branchName = branchRandomNameEnabled ? await this.generateRandomBranchName(repository, branchWhitespaceChar) : '';
3061
return `${branchPrefix}${branchName}`;
3062
};
3063
3064
const getValueSelection = (value: string): [number, number] | undefined => {
3065
return value.startsWith(branchPrefix) ? [branchPrefix.length, value.length] : undefined;
3066
};
3067
3068
const getValidationMessage = (name: string): string | InputBoxValidationMessage | undefined => {
3069
const validateName = new RegExp(branchValidationRegex);
3070
const sanitizedName = sanitizeBranchName(name, branchWhitespaceChar);
3071
3072
// Check if branch name already exists
3073
const existingBranch = refs.find(ref => ref.name === sanitizedName);
3074
if (existingBranch) {
3075
return l10n.t('Branch "{0}" already exists', sanitizedName);
3076
}
3077
3078
if (validateName.test(sanitizedName)) {
3079
// If the sanitized name that we will use is different than what is
3080
// in the input box, show an info message to the user informing them
3081
// the branch name that will be used.
3082
return name === sanitizedName
3083
? undefined
3084
: {
3085
message: l10n.t('The new branch will be "{0}"', sanitizedName),
3086
severity: InputBoxValidationSeverity.Info
3087
};
3088
}
3089
3090
return l10n.t('Branch name needs to match regex: {0}', branchValidationRegex);
3091
};
3092
3093
const disposables: Disposable[] = [];
3094
const inputBox = window.createInputBox();
3095
3096
inputBox.placeholder = l10n.t('Branch name');
3097
inputBox.prompt = l10n.t('Please provide a new branch name');
3098
3099
inputBox.buttons = branchRandomNameEnabled ? [
3100
{
3101
iconPath: new ThemeIcon('refresh'),
3102
tooltip: l10n.t('Regenerate Branch Name'),
3103
location: QuickInputButtonLocation.Inline
3104
}
3105
] : [];
3106
3107
inputBox.value = initialValue ?? await getBranchName();
3108
inputBox.valueSelection = getValueSelection(inputBox.value);
3109
inputBox.validationMessage = getValidationMessage(inputBox.value);
3110
inputBox.ignoreFocusOut = true;
3111
3112
inputBox.show();
3113
3114
const branchName = await new Promise<string | undefined>((resolve) => {
3115
disposables.push(inputBox.onDidHide(() => resolve(undefined)));
3116
disposables.push(inputBox.onDidAccept(() => resolve(inputBox.value)));
3117
disposables.push(inputBox.onDidChangeValue(value => {
3118
inputBox.validationMessage = getValidationMessage(value);
3119
}));
3120
disposables.push(inputBox.onDidTriggerButton(async () => {
3121
inputBox.value = await getBranchName();
3122
inputBox.valueSelection = getValueSelection(inputBox.value);
3123
}));
3124
});
3125
3126
dispose(disposables);
3127
inputBox.dispose();
3128
3129
return sanitizeBranchName(branchName || '', branchWhitespaceChar);
3130
}
3131
3132
private async _branch(repository: Repository, defaultName?: string, from = false, target?: string): Promise<void> {
3133
target = target ?? 'HEAD';
3134
3135
const config = workspace.getConfiguration('git');
3136
const showRefDetails = config.get<boolean>('showReferenceDetails') === true;
3137
const commitShortHashLength = config.get<number>('commitShortHashLength') ?? 7;
3138
3139
if (from) {
3140
const getRefPicks = async () => {
3141
const refs = await repository.getRefs({ includeCommitDetails: showRefDetails });
3142
const refProcessors = new RefItemsProcessor(repository, [
3143
new RefProcessor(RefType.Head),
3144
new RefProcessor(RefType.RemoteHead),
3145
new RefProcessor(RefType.Tag)
3146
]);
3147
3148
return [new HEADItem(repository, commitShortHashLength), ...refProcessors.processRefs(refs)];
3149
};
3150
3151
const placeHolder = l10n.t('Select a ref to create the branch from');
3152
const choice = await window.showQuickPick(getRefPicks(), { placeHolder });
3153
3154
if (!choice) {
3155
return;
3156
}
3157
3158
if (choice instanceof RefItem && choice.refName) {
3159
target = choice.refName;
3160
}
3161
}
3162
3163
const branchName = await this.promptForBranchName(repository, defaultName);
3164
3165
if (!branchName) {
3166
return;
3167
}
3168
3169
await repository.branch(branchName, true, target);
3170
}
3171
3172
private async pickRef<T extends QuickPickItem>(items: Promise<T[]>, placeHolder: string): Promise<T | undefined> {
3173
const disposables: Disposable[] = [];
3174
const quickPick = window.createQuickPick<T>();
3175
3176
quickPick.placeholder = placeHolder;
3177
quickPick.sortByLabel = false;
3178
quickPick.busy = true;
3179
3180
quickPick.show();
3181
3182
quickPick.items = await items;
3183
quickPick.busy = false;
3184
3185
const choice = await new Promise<T | undefined>(resolve => {
3186
disposables.push(quickPick.onDidHide(() => resolve(undefined)));
3187
disposables.push(quickPick.onDidAccept(() => resolve(quickPick.activeItems[0])));
3188
});
3189
3190
dispose(disposables);
3191
quickPick.dispose();
3192
3193
return choice;
3194
}
3195
3196
@command('git.deleteBranch', { repository: true })
3197
async deleteBranch(repository: Repository, name: string | undefined, force?: boolean): Promise<void> {
3198
await this._deleteBranch(repository, undefined, name, { remote: false, force });
3199
}
3200
3201
@command('git.graph.deleteBranch', { repository: true })
3202
async deleteBranch2(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise<void> {
3203
const historyItemRef = historyItem?.references?.find(r => r.id === historyItemRefId);
3204
if (!historyItemRef) {
3205
return;
3206
}
3207
3208
// Local branch
3209
if (historyItemRef.id.startsWith('refs/heads/')) {
3210
if (historyItemRef.id === repository.historyProvider.currentHistoryItemRef?.id) {
3211
window.showInformationMessage(l10n.t('The active branch cannot be deleted.'));
3212
return;
3213
}
3214
3215
await this._deleteBranch(repository, undefined, historyItemRef.name, { remote: false });
3216
return;
3217
}
3218
3219
// Remote branch
3220
if (historyItemRef.id === repository.historyProvider.currentHistoryItemRemoteRef?.id) {
3221
window.showInformationMessage(l10n.t('The remote branch of the active branch cannot be deleted.'));
3222
return;
3223
}
3224
3225
const index = historyItemRef.name.indexOf('/');
3226
if (index === -1) {
3227
return;
3228
}
3229
3230
const remoteName = historyItemRef.name.substring(0, index);
3231
const refName = historyItemRef.name.substring(index + 1);
3232
3233
await this._deleteBranch(repository, remoteName, refName, { remote: true });
3234
}
3235
3236
@command('git.deleteRemoteBranch', { repository: true })
3237
async deleteRemoteBranch(repository: Repository): Promise<void> {
3238
await this._deleteBranch(repository, undefined, undefined, { remote: true });
3239
}
3240
3241
private async _deleteBranch(repository: Repository, remote: string | undefined, name: string | undefined, options: { remote: boolean; force?: boolean }): Promise<void> {
3242
let run: (force?: boolean) => Promise<void>;
3243
3244
const config = workspace.getConfiguration('git');
3245
const showRefDetails = config.get<boolean>('showReferenceDetails') === true;
3246
3247
if (!options.remote && typeof name === 'string') {
3248
// Local branch
3249
run = force => repository.deleteBranch(name!, force);
3250
} else if (options.remote && typeof remote === 'string' && typeof name === 'string') {
3251
// Remote branch
3252
run = force => repository.deleteRemoteRef(remote, name!, { force });
3253
} else {
3254
const getBranchPicks = async () => {
3255
const pattern = options.remote ? 'refs/remotes' : 'refs/heads';
3256
const refs = await repository.getRefs({ pattern, includeCommitDetails: showRefDetails });
3257
const processors = options.remote
3258
? [new RefProcessor(RefType.RemoteHead, BranchDeleteItem)]
3259
: [new RefProcessor(RefType.Head, BranchDeleteItem)];
3260
3261
const itemsProcessor = new RefItemsProcessor(repository, processors, {
3262
skipCurrentBranch: true,
3263
skipCurrentBranchRemote: true
3264
});
3265
3266
return itemsProcessor.processRefs(refs);
3267
};
3268
3269
const placeHolder = !options.remote
3270
? l10n.t('Select a branch to delete')
3271
: l10n.t('Select a remote branch to delete');
3272
3273
const choice = await this.pickRef(getBranchPicks(), placeHolder);
3274
3275
if (!(choice instanceof BranchDeleteItem) || !choice.refName) {
3276
return;
3277
}
3278
name = choice.refName;
3279
run = force => choice.run(repository, force);
3280
}
3281
3282
try {
3283
await run(options.force);
3284
} catch (err) {
3285
if (err.gitErrorCode !== GitErrorCodes.BranchNotFullyMerged) {
3286
throw err;
3287
}
3288
3289
const message = l10n.t('The branch "{0}" is not fully merged. Delete anyway?', name);
3290
const yes = l10n.t('Delete Branch');
3291
const pick = await window.showWarningMessage(message, { modal: true }, yes);
3292
3293
if (pick === yes) {
3294
await run(true);
3295
}
3296
}
3297
}
3298
3299
@command('git.renameBranch', { repository: true })
3300
async renameBranch(repository: Repository): Promise<void> {
3301
const currentBranchName = repository.HEAD && repository.HEAD.name;
3302
const branchName = await this.promptForBranchName(repository, undefined, currentBranchName);
3303
3304
if (!branchName) {
3305
return;
3306
}
3307
3308
try {
3309
await repository.renameBranch(branchName);
3310
} catch (err) {
3311
switch (err.gitErrorCode) {
3312
case GitErrorCodes.InvalidBranchName:
3313
window.showErrorMessage(l10n.t('Invalid branch name'));
3314
return;
3315
case GitErrorCodes.BranchAlreadyExists:
3316
window.showErrorMessage(l10n.t('A branch named "{0}" already exists', branchName));
3317
return;
3318
default:
3319
throw err;
3320
}
3321
}
3322
}
3323
3324
@command('git.merge', { repository: true })
3325
async merge(repository: Repository): Promise<void> {
3326
const config = workspace.getConfiguration('git');
3327
const showRefDetails = config.get<boolean>('showReferenceDetails') === true;
3328
3329
const getQuickPickItems = async (): Promise<QuickPickItem[]> => {
3330
const refs = await repository.getRefs({ includeCommitDetails: showRefDetails });
3331
const itemsProcessor = new RefItemsProcessor(repository, [
3332
new RefProcessor(RefType.Head, MergeItem),
3333
new RefProcessor(RefType.RemoteHead, MergeItem),
3334
new RefProcessor(RefType.Tag, MergeItem)
3335
], {
3336
skipCurrentBranch: true,
3337
skipCurrentBranchRemote: true
3338
});
3339
3340
return itemsProcessor.processRefs(refs);
3341
};
3342
3343
const placeHolder = l10n.t('Select a branch or tag to merge from');
3344
const choice = await this.pickRef(getQuickPickItems(), placeHolder);
3345
3346
if (choice instanceof MergeItem) {
3347
await choice.run(repository);
3348
}
3349
}
3350
3351
@command('git.mergeAbort', { repository: true })
3352
async abortMerge(repository: Repository): Promise<void> {
3353
await repository.mergeAbort();
3354
}
3355
3356
@command('git.rebase', { repository: true })
3357
async rebase(repository: Repository): Promise<void> {
3358
const config = workspace.getConfiguration('git');
3359
const showRefDetails = config.get<boolean>('showReferenceDetails') === true;
3360
const commitShortHashLength = config.get<number>('commitShortHashLength') ?? 7;
3361
3362
const getQuickPickItems = async (): Promise<QuickPickItem[]> => {
3363
const refs = await repository.getRefs({ includeCommitDetails: showRefDetails });
3364
const itemsProcessor = new RefItemsProcessor(repository, [
3365
new RefProcessor(RefType.Head, RebaseItem),
3366
new RefProcessor(RefType.RemoteHead, RebaseItem)
3367
], {
3368
skipCurrentBranch: true,
3369
skipCurrentBranchRemote: true
3370
});
3371
3372
const quickPickItems = itemsProcessor.processRefs(refs);
3373
3374
if (repository.HEAD?.upstream) {
3375
const upstreamRef = refs.find(ref => ref.type === RefType.RemoteHead &&
3376
ref.name === `${repository.HEAD!.upstream!.remote}/${repository.HEAD!.upstream!.name}`);
3377
3378
if (upstreamRef) {
3379
quickPickItems.splice(0, 0, new RebaseUpstreamItem(upstreamRef, commitShortHashLength));
3380
}
3381
}
3382
3383
return quickPickItems;
3384
};
3385
3386
const placeHolder = l10n.t('Select a branch to rebase onto');
3387
const choice = await this.pickRef(getQuickPickItems(), placeHolder);
3388
3389
if (choice instanceof RebaseItem) {
3390
await choice.run(repository);
3391
}
3392
}
3393
3394
@command('git.createTag', { repository: true })
3395
async createTag(repository: Repository, historyItem?: SourceControlHistoryItem): Promise<void> {
3396
const inputTagName = await window.showInputBox({
3397
placeHolder: l10n.t('Tag name'),
3398
prompt: l10n.t('Please provide a tag name'),
3399
ignoreFocusOut: true
3400
});
3401
3402
if (!inputTagName) {
3403
return;
3404
}
3405
3406
const inputMessage = await window.showInputBox({
3407
placeHolder: l10n.t('Message'),
3408
prompt: l10n.t('Please provide a message to annotate the tag'),
3409
ignoreFocusOut: true
3410
});
3411
3412
const name = inputTagName.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-');
3413
await repository.tag({ name, message: inputMessage, ref: historyItem?.id });
3414
}
3415
3416
@command('git.deleteTag', { repository: true })
3417
async deleteTag(repository: Repository): Promise<void> {
3418
const config = workspace.getConfiguration('git');
3419
const showRefDetails = config.get<boolean>('showReferenceDetails') === true;
3420
const commitShortHashLength = config.get<number>('commitShortHashLength') ?? 7;
3421
3422
const tagPicks = async (): Promise<TagDeleteItem[] | QuickPickItem[]> => {
3423
const remoteTags = await repository.getRefs({ pattern: 'refs/tags', includeCommitDetails: showRefDetails });
3424
return remoteTags.length === 0
3425
? [{ label: l10n.t('$(info) This repository has no tags.') }]
3426
: remoteTags.map(ref => new TagDeleteItem(ref, commitShortHashLength));
3427
};
3428
3429
const placeHolder = l10n.t('Select a tag to delete');
3430
const choice = await this.pickRef<TagDeleteItem | QuickPickItem>(tagPicks(), placeHolder);
3431
3432
if (choice instanceof TagDeleteItem) {
3433
await choice.run(repository);
3434
}
3435
}
3436
3437
@command('git.migrateWorktreeChanges', { repository: true, repositoryFilter: ['repository', 'submodule'] })
3438
async migrateWorktreeChanges(repository: Repository): Promise<void> {
3439
const worktreePicks = async (): Promise<WorktreeItem[] | QuickPickItem[]> => {
3440
const worktrees = await repository.getWorktrees();
3441
return worktrees.length === 0
3442
? [{ label: l10n.t('$(info) This repository has no worktrees.') }]
3443
: worktrees.map(worktree => new WorktreeItem(worktree));
3444
};
3445
3446
const placeHolder = l10n.t('Select a worktree to migrate changes from');
3447
const choice = await this.pickRef<WorktreeItem | QuickPickItem>(worktreePicks(), placeHolder);
3448
3449
if (!choice || !(choice instanceof WorktreeItem)) {
3450
return;
3451
}
3452
3453
const worktreeRepository = this.model.getRepository(choice.worktree.path);
3454
if (!worktreeRepository) {
3455
return;
3456
}
3457
3458
if (worktreeRepository.indexGroup.resourceStates.length === 0 &&
3459
worktreeRepository.workingTreeGroup.resourceStates.length === 0 &&
3460
worktreeRepository.untrackedGroup.resourceStates.length === 0) {
3461
await window.showInformationMessage(l10n.t('There are no changes in the selected worktree to migrate.'));
3462
return;
3463
}
3464
3465
const worktreeChangedFilePaths = [
3466
...worktreeRepository.indexGroup.resourceStates,
3467
...worktreeRepository.workingTreeGroup.resourceStates,
3468
...worktreeRepository.untrackedGroup.resourceStates
3469
].map(resource => path.relative(worktreeRepository.root, resource.resourceUri.fsPath));
3470
3471
const targetChangedFilePaths = [
3472
...repository.workingTreeGroup.resourceStates,
3473
...repository.untrackedGroup.resourceStates
3474
].map(resource => path.relative(repository.root, resource.resourceUri.fsPath));
3475
3476
// Detect overlapping unstaged files in worktree stash and target repository
3477
const conflicts = worktreeChangedFilePaths.filter(path => targetChangedFilePaths.includes(path));
3478
3479
// Check for 'LocalChangesOverwritten' error
3480
if (conflicts.length > 0) {
3481
const maxFilesShown = 5;
3482
const filesToShow = conflicts.slice(0, maxFilesShown);
3483
const remainingCount = conflicts.length - maxFilesShown;
3484
3485
const fileList = filesToShow.join('\n ') +
3486
(remainingCount > 0 ? l10n.t('\n and {0} more file{1}...', remainingCount, remainingCount > 1 ? 's' : '') : '');
3487
3488
const message = l10n.t('Your local changes to the following files would be overwritten by merge:\n {0}\n\nPlease stage, commit, or stash your changes in the repository before migrating changes.', fileList);
3489
await window.showErrorMessage(message, { modal: true });
3490
return;
3491
}
3492
3493
const message = l10n.t('Proceed with migrating changes to the current repository?');
3494
const detail = l10n.t('This will apply the worktree\'s changes to this repository and discard changes in the worktree.\nThis is IRREVERSIBLE!');
3495
const proceed = l10n.t('Proceed');
3496
const pick = await window.showWarningMessage(message, { modal: true, detail }, proceed);
3497
if (pick !== proceed) {
3498
return;
3499
}
3500
3501
await worktreeRepository.createStash(undefined, true);
3502
const stashes = await worktreeRepository.getStashes();
3503
3504
try {
3505
await repository.applyStash(stashes[0].index);
3506
worktreeRepository.dropStash(stashes[0].index);
3507
} catch (err) {
3508
if (err.gitErrorCode !== GitErrorCodes.StashConflict) {
3509
await worktreeRepository.popStash();
3510
throw err;
3511
}
3512
repository.isWorktreeMigrating = true;
3513
3514
const message = l10n.t('There are merge conflicts from migrating changes. Please resolve them before committing.');
3515
const show = l10n.t('Show Changes');
3516
const choice = await window.showWarningMessage(message, show);
3517
if (choice === show) {
3518
await commands.executeCommand('workbench.view.scm');
3519
}
3520
worktreeRepository.dropStash(stashes[0].index);
3521
}
3522
}
3523
3524
@command('git.openWorktreeMergeEditor')
3525
async openWorktreeMergeEditor(uri: Uri): Promise<void> {
3526
type InputData = { uri: Uri; title: string };
3527
const mergeUris = toMergeUris(uri);
3528
3529
const current: InputData = { uri: mergeUris.ours, title: l10n.t('Workspace') };
3530
const incoming: InputData = { uri: mergeUris.theirs, title: l10n.t('Worktree') };
3531
3532
await commands.executeCommand('_open.mergeEditor', {
3533
base: mergeUris.base,
3534
input1: current,
3535
input2: incoming,
3536
output: uri
3537
});
3538
}
3539
3540
@command('git.createWorktree')
3541
async createWorktree(repository: any): Promise<void> {
3542
repository = this.model.getRepository(repository);
3543
3544
if (!repository) {
3545
// Single repository/submodule/worktree
3546
if (this.model.repositories.length === 1) {
3547
repository = this.model.repositories[0];
3548
}
3549
}
3550
3551
if (!repository) {
3552
// Single repository/submodule
3553
const repositories = this.model.repositories
3554
.filter(r => r.kind === 'repository' || r.kind === 'submodule');
3555
3556
if (repositories.length === 1) {
3557
repository = repositories[0];
3558
}
3559
}
3560
3561
if (!repository) {
3562
// Multiple repositories/submodules
3563
repository = await this.model.pickRepository(['repository', 'submodule']);
3564
}
3565
3566
if (!repository) {
3567
return;
3568
}
3569
3570
await this._createWorktree(repository);
3571
}
3572
3573
private async _createWorktree(repository: Repository): Promise<void> {
3574
const config = workspace.getConfiguration('git');
3575
const branchPrefix = config.get<string>('branchPrefix')!;
3576
const showRefDetails = config.get<boolean>('showReferenceDetails') === true;
3577
3578
const createBranch = new CreateBranchItem();
3579
const getBranchPicks = async () => {
3580
const refs = await repository.getRefs({ includeCommitDetails: showRefDetails });
3581
const itemsProcessor = new RefItemsProcessor(repository, [
3582
new RefProcessor(RefType.Head),
3583
new RefProcessor(RefType.RemoteHead),
3584
new RefProcessor(RefType.Tag)
3585
]);
3586
const branchItems = itemsProcessor.processRefs(refs);
3587
return [createBranch, { label: '', kind: QuickPickItemKind.Separator }, ...branchItems];
3588
};
3589
3590
const placeHolder = l10n.t('Select a branch or tag to create the new worktree from');
3591
const choice = await this.pickRef(getBranchPicks(), placeHolder);
3592
3593
if (!choice) {
3594
return;
3595
}
3596
3597
let branch: string | undefined = undefined;
3598
let commitish: string;
3599
3600
if (choice === createBranch) {
3601
branch = await this.promptForBranchName(repository);
3602
3603
if (!branch) {
3604
return;
3605
}
3606
3607
commitish = 'HEAD';
3608
} else {
3609
if (!(choice instanceof RefItem) || !choice.refName) {
3610
return;
3611
}
3612
3613
if (choice.refName === repository.HEAD?.name) {
3614
const message = l10n.t('Branch "{0}" is already checked out in the current repository.', choice.refName);
3615
const createBranch = l10n.t('Create New Branch');
3616
const pick = await window.showWarningMessage(message, { modal: true }, createBranch);
3617
3618
if (pick === createBranch) {
3619
branch = await this.promptForBranchName(repository);
3620
3621
if (!branch) {
3622
return;
3623
}
3624
3625
commitish = 'HEAD';
3626
} else {
3627
return;
3628
}
3629
} else {
3630
// Check whether the selected branch is checked out in an existing worktree
3631
const worktree = repository.worktrees.find(worktree => worktree.ref === choice.refId);
3632
if (worktree) {
3633
const message = l10n.t('Branch "{0}" is already checked out in the worktree at "{1}".', choice.refName, worktree.path);
3634
await this.handleWorktreeConflict(worktree.path, message);
3635
return;
3636
}
3637
commitish = choice.refName;
3638
}
3639
}
3640
3641
const worktreeName = ((branch ?? commitish).startsWith(branchPrefix)
3642
? (branch ?? commitish).substring(branchPrefix.length).replace(/\//g, '-')
3643
: (branch ?? commitish).replace(/\//g, '-'));
3644
3645
// If user selects folder button, they manually select the worktree path through folder picker
3646
const getWorktreePath = async (): Promise<string | undefined> => {
3647
const worktreeRoot = this.globalState.get<string>(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`);
3648
const defaultUri = worktreeRoot ? Uri.file(worktreeRoot) : Uri.file(path.dirname(repository.root));
3649
3650
const uris = await window.showOpenDialog({
3651
defaultUri,
3652
canSelectFiles: false,
3653
canSelectFolders: true,
3654
canSelectMany: false,
3655
openLabel: l10n.t('Select as Worktree Destination'),
3656
});
3657
3658
if (!uris || uris.length === 0) {
3659
return;
3660
}
3661
3662
return path.join(uris[0].fsPath, worktreeName);
3663
};
3664
3665
const getValueSelection = (value: string): [number, number] | undefined => {
3666
if (!value || !worktreeName) {
3667
return;
3668
}
3669
3670
const start = value.length - worktreeName.length;
3671
return [start, value.length];
3672
};
3673
3674
const getValidationMessage = (value: string): InputBoxValidationMessage | undefined => {
3675
const worktree = repository.worktrees.find(worktree => pathEquals(path.normalize(worktree.path), path.normalize(value)));
3676
return worktree ? {
3677
message: l10n.t('A worktree already exists at "{0}".', value),
3678
severity: InputBoxValidationSeverity.Warning
3679
} : undefined;
3680
};
3681
3682
// Default worktree path is based on the last worktree location or a worktree folder for the repository
3683
const defaultWorktreeRoot = this.globalState.get<string>(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`);
3684
const defaultWorktreePath = defaultWorktreeRoot
3685
? path.join(defaultWorktreeRoot, worktreeName)
3686
: path.join(path.dirname(repository.root), `${path.basename(repository.root)}.worktrees`, worktreeName);
3687
3688
const disposables: Disposable[] = [];
3689
const inputBox = window.createInputBox();
3690
disposables.push(inputBox);
3691
3692
inputBox.placeholder = l10n.t('Worktree path');
3693
inputBox.prompt = l10n.t('Please provide a worktree path');
3694
inputBox.value = defaultWorktreePath;
3695
inputBox.valueSelection = getValueSelection(inputBox.value);
3696
inputBox.validationMessage = getValidationMessage(inputBox.value);
3697
inputBox.ignoreFocusOut = true;
3698
inputBox.buttons = [
3699
{
3700
iconPath: new ThemeIcon('folder'),
3701
tooltip: l10n.t('Select Worktree Destination'),
3702
location: QuickInputButtonLocation.Inline
3703
}
3704
];
3705
3706
inputBox.show();
3707
3708
const worktreePath = await new Promise<string | undefined>((resolve) => {
3709
disposables.push(inputBox.onDidHide(() => resolve(undefined)));
3710
disposables.push(inputBox.onDidAccept(() => resolve(inputBox.value)));
3711
disposables.push(inputBox.onDidChangeValue(value => {
3712
inputBox.validationMessage = getValidationMessage(value);
3713
}));
3714
disposables.push(inputBox.onDidTriggerButton(async () => {
3715
inputBox.value = await getWorktreePath() ?? '';
3716
inputBox.valueSelection = getValueSelection(inputBox.value);
3717
}));
3718
});
3719
3720
dispose(disposables);
3721
3722
if (!worktreePath) {
3723
return;
3724
}
3725
3726
try {
3727
await repository.addWorktree({ path: worktreePath, branch, commitish: commitish });
3728
3729
// Update worktree root in global state
3730
const worktreeRoot = path.dirname(worktreePath);
3731
if (worktreeRoot !== defaultWorktreeRoot) {
3732
this.globalState.update(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`, worktreeRoot);
3733
}
3734
} catch (err) {
3735
if (err.gitErrorCode === GitErrorCodes.WorktreeAlreadyExists) {
3736
await this.handleWorktreeAlreadyExists(err);
3737
} else if (err.gitErrorCode === GitErrorCodes.WorktreeBranchAlreadyUsed) {
3738
await this.handleWorktreeBranchAlreadyUsed(err);
3739
} else {
3740
throw err;
3741
}
3742
3743
return;
3744
}
3745
}
3746
3747
private async handleWorktreeBranchAlreadyUsed(err: any): Promise<void> {
3748
const match = err.stderr.match(/fatal: '([^']+)' is already used by worktree at '([^']+)'/);
3749
3750
if (!match) {
3751
return;
3752
}
3753
3754
const [, branch, path] = match;
3755
const message = l10n.t('Branch "{0}" is already checked out in the worktree at "{1}".', branch, path);
3756
await this.handleWorktreeConflict(path, message);
3757
}
3758
3759
private async handleWorktreeAlreadyExists(err: any): Promise<void> {
3760
const match = err.stderr.match(/fatal: '([^']+)'/);
3761
3762
if (!match) {
3763
return;
3764
}
3765
3766
const [, path] = match;
3767
const message = l10n.t('A worktree already exists at "{0}".', path);
3768
await this.handleWorktreeConflict(path, message);
3769
}
3770
3771
private async handleWorktreeConflict(path: string, message: string): Promise<void> {
3772
await this.model.openRepository(path, true);
3773
3774
const worktreeRepository = this.model.getRepository(path);
3775
3776
if (!worktreeRepository) {
3777
return;
3778
}
3779
3780
const openWorktree = l10n.t('Open Worktree in Current Window');
3781
const openWorktreeInNewWindow = l10n.t('Open Worktree in New Window');
3782
const choice = await window.showWarningMessage(message, { modal: true }, openWorktree, openWorktreeInNewWindow);
3783
3784
if (choice === openWorktree) {
3785
await this.openWorktreeInCurrentWindow(worktreeRepository);
3786
} else if (choice === openWorktreeInNewWindow) {
3787
await this.openWorktreeInNewWindow(worktreeRepository);
3788
}
3789
return;
3790
}
3791
3792
@command('git.deleteWorktree', { repository: true, repositoryFilter: ['worktree'] })
3793
async deleteWorktree(repository: Repository): Promise<void> {
3794
if (!repository.dotGit.commonPath) {
3795
return;
3796
}
3797
3798
const mainRepository = this.model.getRepository(path.dirname(repository.dotGit.commonPath));
3799
if (!mainRepository) {
3800
await window.showErrorMessage(l10n.t('You cannot delete the worktree you are currently in. Please switch to the main repository first.'), { modal: true });
3801
return;
3802
}
3803
3804
// Dispose worktree repository
3805
this.model.disposeRepository(repository);
3806
3807
try {
3808
await mainRepository.deleteWorktree(repository.root);
3809
} catch (err) {
3810
if (err.gitErrorCode === GitErrorCodes.WorktreeContainsChanges) {
3811
const forceDelete = l10n.t('Force Delete');
3812
const message = l10n.t('The worktree contains modified or untracked files. Do you want to force delete?');
3813
const choice = await window.showWarningMessage(message, { modal: true }, forceDelete);
3814
if (choice === forceDelete) {
3815
await mainRepository.deleteWorktree(repository.root, { force: true });
3816
} else {
3817
await this.model.openRepository(repository.root);
3818
}
3819
3820
return;
3821
}
3822
3823
throw err;
3824
}
3825
}
3826
3827
@command('git.deleteWorktreeFromPalette', { repository: true, repositoryFilter: ['repository', 'submodule'] })
3828
async deleteWorktreeFromPalette(repository: Repository): Promise<void> {
3829
const worktreePicks = async (): Promise<WorktreeDeleteItem[] | QuickPickItem[]> => {
3830
const worktrees = await repository.getWorktrees();
3831
return worktrees.length === 0
3832
? [{ label: l10n.t('$(info) This repository has no worktrees.') }]
3833
: worktrees.map(worktree => new WorktreeDeleteItem(worktree));
3834
};
3835
3836
const placeHolder = l10n.t('Select a worktree to delete');
3837
const choice = await this.pickRef<WorktreeDeleteItem | QuickPickItem>(worktreePicks(), placeHolder);
3838
3839
if (choice instanceof WorktreeDeleteItem) {
3840
await choice.run(repository);
3841
}
3842
}
3843
3844
@command('git.openWorktree', { repository: true })
3845
async openWorktreeInCurrentWindow(repository: Repository): Promise<void> {
3846
if (!repository) {
3847
return;
3848
}
3849
3850
const uri = Uri.file(repository.root);
3851
await commands.executeCommand('vscode.openFolder', uri, { forceReuseWindow: true });
3852
}
3853
3854
@command('git.openWorktreeInNewWindow', { repository: true })
3855
async openWorktreeInNewWindow(repository: Repository): Promise<void> {
3856
if (!repository) {
3857
return;
3858
}
3859
3860
const uri = Uri.file(repository.root);
3861
await commands.executeCommand('vscode.openFolder', uri, { forceNewWindow: true });
3862
}
3863
3864
@command('git.graph.deleteTag', { repository: true })
3865
async deleteTag2(repository: Repository, historyItem?: SourceControlHistoryItem, historyItemRefId?: string): Promise<void> {
3866
const historyItemRef = historyItem?.references?.find(r => r.id === historyItemRefId);
3867
if (!historyItemRef) {
3868
return;
3869
}
3870
3871
await repository.deleteTag(historyItemRef.name);
3872
}
3873
3874
@command('git.deleteRemoteTag', { repository: true })
3875
async deleteRemoteTag(repository: Repository): Promise<void> {
3876
const config = workspace.getConfiguration('git');
3877
const commitShortHashLength = config.get<number>('commitShortHashLength') ?? 7;
3878
3879
const remotePicks = repository.remotes
3880
.filter(r => r.pushUrl !== undefined)
3881
.map(r => new RemoteItem(repository, r));
3882
3883
if (remotePicks.length === 0) {
3884
window.showErrorMessage(l10n.t("Your repository has no remotes configured to push to."));
3885
return;
3886
}
3887
3888
let remoteName = remotePicks[0].remoteName;
3889
if (remotePicks.length > 1) {
3890
const remotePickPlaceholder = l10n.t('Select a remote to delete a tag from');
3891
const remotePick = await window.showQuickPick(remotePicks, { placeHolder: remotePickPlaceholder });
3892
3893
if (!remotePick) {
3894
return;
3895
}
3896
3897
remoteName = remotePick.remoteName;
3898
}
3899
3900
const remoteTagPicks = async (): Promise<RemoteTagDeleteItem[] | QuickPickItem[]> => {
3901
const remoteTagsRaw = await repository.getRemoteRefs(remoteName, { tags: true });
3902
3903
// Deduplicate annotated and lightweight tags
3904
const remoteTagNames = new Set<string>();
3905
const remoteTags: Ref[] = [];
3906
3907
for (const tag of remoteTagsRaw) {
3908
const tagName = (tag.name ?? '').replace(/\^{}$/, '');
3909
if (!remoteTagNames.has(tagName)) {
3910
remoteTags.push({ ...tag, name: tagName });
3911
remoteTagNames.add(tagName);
3912
}
3913
}
3914
3915
return remoteTags.length === 0
3916
? [{ label: l10n.t('$(info) Remote "{0}" has no tags.', remoteName) }]
3917
: remoteTags.map(ref => new RemoteTagDeleteItem(ref, commitShortHashLength));
3918
};
3919
3920
const tagPickPlaceholder = l10n.t('Select a remote tag to delete');
3921
const remoteTagPick = await window.showQuickPick<RemoteTagDeleteItem | QuickPickItem>(remoteTagPicks(), { placeHolder: tagPickPlaceholder });
3922
3923
if (remoteTagPick instanceof RemoteTagDeleteItem) {
3924
await remoteTagPick.run(repository, remoteName);
3925
}
3926
}
3927
3928
@command('git.fetch', { repository: true })
3929
async fetch(repository: Repository): Promise<void> {
3930
if (repository.remotes.length === 0) {
3931
window.showWarningMessage(l10n.t('This repository has no remotes configured to fetch from.'));
3932
return;
3933
}
3934
3935
if (repository.remotes.length === 1) {
3936
await repository.fetchDefault();
3937
return;
3938
}
3939
3940
const remoteItems: RemoteItem[] = repository.remotes.map(r => new RemoteItem(repository, r));
3941
3942
if (repository.HEAD?.upstream?.remote) {
3943
// Move default remote to the top
3944
const defaultRemoteIndex = remoteItems
3945
.findIndex(r => r.remoteName === repository.HEAD!.upstream!.remote);
3946
3947
if (defaultRemoteIndex !== -1) {
3948
remoteItems.splice(0, 0, ...remoteItems.splice(defaultRemoteIndex, 1));
3949
}
3950
}
3951
3952
const quickpick = window.createQuickPick();
3953
quickpick.placeholder = l10n.t('Select a remote to fetch');
3954
quickpick.canSelectMany = false;
3955
quickpick.items = [...remoteItems, { label: '', kind: QuickPickItemKind.Separator }, new FetchAllRemotesItem(repository)];
3956
3957
quickpick.show();
3958
const remoteItem = await new Promise<RemoteItem | FetchAllRemotesItem | undefined>(resolve => {
3959
quickpick.onDidAccept(() => resolve(quickpick.activeItems[0] as RemoteItem | FetchAllRemotesItem));
3960
quickpick.onDidHide(() => resolve(undefined));
3961
});
3962
quickpick.hide();
3963
3964
if (!remoteItem) {
3965
return;
3966
}
3967
3968
await remoteItem.run();
3969
}
3970
3971
@command('git.fetchPrune', { repository: true })
3972
async fetchPrune(repository: Repository): Promise<void> {
3973
if (repository.remotes.length === 0) {
3974
window.showWarningMessage(l10n.t('This repository has no remotes configured to fetch from.'));
3975
return;
3976
}
3977
3978
await repository.fetchPrune();
3979
}
3980
3981
3982
@command('git.fetchAll', { repository: true })
3983
async fetchAll(repository: Repository): Promise<void> {
3984
if (repository.remotes.length === 0) {
3985
window.showWarningMessage(l10n.t('This repository has no remotes configured to fetch from.'));
3986
return;
3987
}
3988
3989
await repository.fetchAll();
3990
}
3991
3992
@command('git.fetchRef', { repository: true })
3993
async fetchRef(repository: Repository, ref?: string): Promise<void> {
3994
ref = ref ?? repository?.historyProvider.currentHistoryItemRemoteRef?.id;
3995
if (!repository || !ref) {
3996
return;
3997
}
3998
3999
const branch = await repository.getBranch(ref);
4000
await repository.fetch({ remote: branch.remote, ref: branch.name });
4001
}
4002
4003
@command('git.pullFrom', { repository: true })
4004
async pullFrom(repository: Repository): Promise<void> {
4005
const config = workspace.getConfiguration('git');
4006
const commitShortHashLength = config.get<number>('commitShortHashLength') ?? 7;
4007
4008
const remotes = repository.remotes;
4009
4010
if (remotes.length === 0) {
4011
window.showWarningMessage(l10n.t('Your repository has no remotes configured to pull from.'));
4012
return;
4013
}
4014
4015
let remoteName = remotes[0].name;
4016
if (remotes.length > 1) {
4017
const remotePicks = remotes.filter(r => r.fetchUrl !== undefined).map(r => ({ label: r.name, description: r.fetchUrl! }));
4018
const placeHolder = l10n.t('Pick a remote to pull the branch from');
4019
const remotePick = await window.showQuickPick(remotePicks, { placeHolder });
4020
4021
if (!remotePick) {
4022
return;
4023
}
4024
4025
remoteName = remotePick.label;
4026
}
4027
4028
const getBranchPicks = async (): Promise<RefItem[]> => {
4029
const remoteRefs = await repository.getRefs({ pattern: `refs/remotes/${remoteName}/` });
4030
return remoteRefs.map(r => new RefItem(r, commitShortHashLength));
4031
};
4032
4033
const branchPlaceHolder = l10n.t('Pick a branch to pull from');
4034
const branchPick = await this.pickRef(getBranchPicks(), branchPlaceHolder);
4035
4036
if (!branchPick || !branchPick.refName) {
4037
return;
4038
}
4039
4040
const remoteCharCnt = remoteName.length;
4041
await repository.pullFrom(false, remoteName, branchPick.refName.slice(remoteCharCnt + 1));
4042
}
4043
4044
@command('git.pull', { repository: true })
4045
async pull(repository: Repository): Promise<void> {
4046
const remotes = repository.remotes;
4047
4048
if (remotes.length === 0) {
4049
window.showWarningMessage(l10n.t('Your repository has no remotes configured to pull from.'));
4050
return;
4051
}
4052
4053
await repository.pull(repository.HEAD);
4054
}
4055
4056
@command('git.pullRebase', { repository: true })
4057
async pullRebase(repository: Repository): Promise<void> {
4058
const remotes = repository.remotes;
4059
4060
if (remotes.length === 0) {
4061
window.showWarningMessage(l10n.t('Your repository has no remotes configured to pull from.'));
4062
return;
4063
}
4064
4065
await repository.pullWithRebase(repository.HEAD);
4066
}
4067
4068
@command('git.pullRef', { repository: true })
4069
async pullRef(repository: Repository, ref?: string): Promise<void> {
4070
ref = ref ?? repository?.historyProvider.currentHistoryItemRemoteRef?.id;
4071
if (!repository || !ref) {
4072
return;
4073
}
4074
4075
const branch = await repository.getBranch(ref);
4076
await repository.pullFrom(false, branch.remote, branch.name);
4077
}
4078
4079
private async _push(repository: Repository, pushOptions: PushOptions) {
4080
const remotes = repository.remotes;
4081
4082
if (remotes.length === 0) {
4083
if (pushOptions.silent) {
4084
return;
4085
}
4086
4087
const addRemote = l10n.t('Add Remote');
4088
const result = await window.showWarningMessage(l10n.t('Your repository has no remotes configured to push to.'), addRemote);
4089
4090
if (result === addRemote) {
4091
await this.addRemote(repository);
4092
}
4093
4094
return;
4095
}
4096
4097
const config = workspace.getConfiguration('git', Uri.file(repository.root));
4098
let forcePushMode: ForcePushMode | undefined = undefined;
4099
4100
if (pushOptions.forcePush) {
4101
if (!config.get<boolean>('allowForcePush')) {
4102
await window.showErrorMessage(l10n.t('Force push is not allowed, please enable it with the "git.allowForcePush" setting.'));
4103
return;
4104
}
4105
4106
const useForcePushWithLease = config.get<boolean>('useForcePushWithLease') === true;
4107
const useForcePushIfIncludes = config.get<boolean>('useForcePushIfIncludes') === true;
4108
forcePushMode = useForcePushWithLease ? useForcePushIfIncludes ? ForcePushMode.ForceWithLeaseIfIncludes : ForcePushMode.ForceWithLease : ForcePushMode.Force;
4109
4110
if (config.get<boolean>('confirmForcePush')) {
4111
const message = l10n.t('You are about to force push your changes, this can be destructive and could inadvertently overwrite changes made by others.\n\nAre you sure to continue?');
4112
const yes = l10n.t('OK');
4113
const neverAgain = l10n.t('OK, Don\'t Ask Again');
4114
const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);
4115
4116
if (pick === neverAgain) {
4117
config.update('confirmForcePush', false, true);
4118
} else if (pick !== yes) {
4119
return;
4120
}
4121
}
4122
}
4123
4124
if (pushOptions.pushType === PushType.PushFollowTags) {
4125
await repository.pushFollowTags(undefined, forcePushMode);
4126
return;
4127
}
4128
4129
if (pushOptions.pushType === PushType.PushTags) {
4130
await repository.pushTags(undefined, forcePushMode);
4131
}
4132
4133
if (!repository.HEAD || !repository.HEAD.name) {
4134
if (!pushOptions.silent) {
4135
window.showWarningMessage(l10n.t('Please check out a branch to push to a remote.'));
4136
}
4137
return;
4138
}
4139
4140
if (pushOptions.pushType === PushType.Push) {
4141
try {
4142
await repository.push(repository.HEAD, forcePushMode);
4143
} catch (err) {
4144
if (err.gitErrorCode !== GitErrorCodes.NoUpstreamBranch) {
4145
throw err;
4146
}
4147
4148
if (pushOptions.silent) {
4149
return;
4150
}
4151
4152
if (this.globalState.get<boolean>('confirmBranchPublish', true)) {
4153
const branchName = repository.HEAD.name;
4154
const message = l10n.t('The branch "{0}" has no remote branch. Would you like to publish this branch?', branchName);
4155
const yes = l10n.t('OK');
4156
const neverAgain = l10n.t('OK, Don\'t Ask Again');
4157
const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);
4158
4159
if (pick === yes || pick === neverAgain) {
4160
if (pick === neverAgain) {
4161
this.globalState.update('confirmBranchPublish', false);
4162
}
4163
await this.publish(repository);
4164
}
4165
} else {
4166
await this.publish(repository);
4167
}
4168
}
4169
} else {
4170
const branchName = repository.HEAD.name;
4171
if (!pushOptions.pushTo?.remote) {
4172
const addRemote = new AddRemoteItem(this);
4173
const picks = [...remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl })), addRemote];
4174
const placeHolder = l10n.t('Pick a remote to publish the branch "{0}" to:', branchName);
4175
const choice = await window.showQuickPick(picks, { placeHolder });
4176
4177
if (!choice) {
4178
return;
4179
}
4180
4181
if (choice === addRemote) {
4182
const newRemote = await this.addRemote(repository);
4183
4184
if (newRemote) {
4185
await repository.pushTo(newRemote, branchName, undefined, forcePushMode);
4186
}
4187
} else {
4188
await repository.pushTo(choice.label, branchName, undefined, forcePushMode);
4189
}
4190
} else {
4191
await repository.pushTo(pushOptions.pushTo.remote, pushOptions.pushTo.refspec || branchName, pushOptions.pushTo.setUpstream, forcePushMode);
4192
}
4193
}
4194
}
4195
4196
@command('git.push', { repository: true })
4197
async push(repository: Repository): Promise<void> {
4198
await this._push(repository, { pushType: PushType.Push });
4199
}
4200
4201
@command('git.pushForce', { repository: true })
4202
async pushForce(repository: Repository): Promise<void> {
4203
await this._push(repository, { pushType: PushType.Push, forcePush: true });
4204
}
4205
4206
@command('git.pushWithTags', { repository: true })
4207
async pushFollowTags(repository: Repository): Promise<void> {
4208
await this._push(repository, { pushType: PushType.PushFollowTags });
4209
}
4210
4211
@command('git.pushWithTagsForce', { repository: true })
4212
async pushFollowTagsForce(repository: Repository): Promise<void> {
4213
await this._push(repository, { pushType: PushType.PushFollowTags, forcePush: true });
4214
}
4215
4216
@command('git.pushRef', { repository: true })
4217
async pushRef(repository: Repository): Promise<void> {
4218
if (!repository) {
4219
return;
4220
}
4221
4222
await this._push(repository, { pushType: PushType.Push });
4223
}
4224
4225
@command('git.cherryPick', { repository: true })
4226
async cherryPick(repository: Repository): Promise<void> {
4227
const hash = await window.showInputBox({
4228
placeHolder: l10n.t('Commit Hash'),
4229
prompt: l10n.t('Please provide the commit hash'),
4230
ignoreFocusOut: true
4231
});
4232
4233
if (!hash) {
4234
return;
4235
}
4236
4237
await repository.cherryPick(hash);
4238
}
4239
4240
@command('git.graph.cherryPick', { repository: true })
4241
async cherryPick2(repository: Repository, historyItem?: SourceControlHistoryItem): Promise<void> {
4242
if (!historyItem) {
4243
return;
4244
}
4245
4246
await repository.cherryPick(historyItem.id);
4247
}
4248
4249
@command('git.cherryPickAbort', { repository: true })
4250
async cherryPickAbort(repository: Repository): Promise<void> {
4251
await repository.cherryPickAbort();
4252
}
4253
4254
@command('git.pushTo', { repository: true })
4255
async pushTo(repository: Repository, remote?: string, refspec?: string, setUpstream?: boolean): Promise<void> {
4256
await this._push(repository, { pushType: PushType.PushTo, pushTo: { remote: remote, refspec: refspec, setUpstream: setUpstream } });
4257
}
4258
4259
@command('git.pushToForce', { repository: true })
4260
async pushToForce(repository: Repository, remote?: string, refspec?: string, setUpstream?: boolean): Promise<void> {
4261
await this._push(repository, { pushType: PushType.PushTo, pushTo: { remote: remote, refspec: refspec, setUpstream: setUpstream }, forcePush: true });
4262
}
4263
4264
@command('git.pushTags', { repository: true })
4265
async pushTags(repository: Repository): Promise<void> {
4266
await this._push(repository, { pushType: PushType.PushTags });
4267
}
4268
4269
@command('git.addRemote', { repository: true })
4270
async addRemote(repository: Repository): Promise<string | undefined> {
4271
const url = await pickRemoteSource({
4272
providerLabel: provider => l10n.t('Add remote from {0}', provider.name),
4273
urlLabel: l10n.t('Add remote from URL')
4274
});
4275
4276
if (!url) {
4277
return;
4278
}
4279
4280
const resultName = await window.showInputBox({
4281
placeHolder: l10n.t('Remote name'),
4282
prompt: l10n.t('Please provide a remote name'),
4283
ignoreFocusOut: true,
4284
validateInput: (name: string) => {
4285
if (!sanitizeRemoteName(name)) {
4286
return l10n.t('Remote name format invalid');
4287
} else if (repository.remotes.find(r => r.name === name)) {
4288
return l10n.t('Remote "{0}" already exists.', name);
4289
}
4290
4291
return null;
4292
}
4293
});
4294
4295
const name = sanitizeRemoteName(resultName || '');
4296
4297
if (!name) {
4298
return;
4299
}
4300
4301
await repository.addRemote(name, url.trim());
4302
await repository.fetch({ remote: name });
4303
return name;
4304
}
4305
4306
@command('git.removeRemote', { repository: true })
4307
async removeRemote(repository: Repository): Promise<void> {
4308
const remotes = repository.remotes;
4309
4310
if (remotes.length === 0) {
4311
window.showErrorMessage(l10n.t('Your repository has no remotes.'));
4312
return;
4313
}
4314
4315
const picks: RemoteItem[] = repository.remotes.map(r => new RemoteItem(repository, r));
4316
const placeHolder = l10n.t('Pick a remote to remove');
4317
4318
const remote = await window.showQuickPick(picks, { placeHolder });
4319
4320
if (!remote) {
4321
return;
4322
}
4323
4324
await repository.removeRemote(remote.remoteName);
4325
}
4326
4327
private async _sync(repository: Repository, rebase: boolean): Promise<void> {
4328
const HEAD = repository.HEAD;
4329
4330
if (!HEAD) {
4331
return;
4332
} else if (!HEAD.upstream) {
4333
this._push(repository, { pushType: PushType.Push });
4334
return;
4335
}
4336
4337
const remoteName = HEAD.remote || HEAD.upstream.remote;
4338
const remote = repository.remotes.find(r => r.name === remoteName);
4339
const isReadonly = remote && remote.isReadOnly;
4340
4341
const config = workspace.getConfiguration('git');
4342
const shouldPrompt = !isReadonly && config.get<boolean>('confirmSync') === true;
4343
4344
if (shouldPrompt) {
4345
const message = l10n.t('This action will pull and push commits from and to "{0}/{1}".', HEAD.upstream.remote, HEAD.upstream.name);
4346
const yes = l10n.t('OK');
4347
const neverAgain = l10n.t('OK, Don\'t Show Again');
4348
const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);
4349
4350
if (pick === neverAgain) {
4351
await config.update('confirmSync', false, true);
4352
} else if (pick !== yes) {
4353
return;
4354
}
4355
}
4356
4357
await repository.sync(HEAD, rebase);
4358
}
4359
4360
@command('git.sync', { repository: true })
4361
async sync(repository: Repository): Promise<void> {
4362
const config = workspace.getConfiguration('git', Uri.file(repository.root));
4363
const rebase = config.get<boolean>('rebaseWhenSync', false) === true;
4364
4365
try {
4366
await this._sync(repository, rebase);
4367
} catch (err) {
4368
if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
4369
return;
4370
}
4371
4372
throw err;
4373
}
4374
}
4375
4376
@command('git._syncAll')
4377
async syncAll(): Promise<void> {
4378
await Promise.all(this.model.repositories.map(async repository => {
4379
const config = workspace.getConfiguration('git', Uri.file(repository.root));
4380
const rebase = config.get<boolean>('rebaseWhenSync', false) === true;
4381
4382
const HEAD = repository.HEAD;
4383
4384
if (!HEAD || !HEAD.upstream) {
4385
return;
4386
}
4387
4388
await repository.sync(HEAD, rebase);
4389
}));
4390
}
4391
4392
@command('git.syncRebase', { repository: true })
4393
async syncRebase(repository: Repository): Promise<void> {
4394
try {
4395
await this._sync(repository, true);
4396
} catch (err) {
4397
if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
4398
return;
4399
}
4400
4401
throw err;
4402
}
4403
}
4404
4405
@command('git.publish', { repository: true })
4406
async publish(repository: Repository): Promise<void> {
4407
const branchName = repository.HEAD && repository.HEAD.name || '';
4408
const remotes = repository.remotes;
4409
4410
if (remotes.length === 0) {
4411
const publishers = this.model.getRemoteSourcePublishers();
4412
4413
if (publishers.length === 0) {
4414
window.showWarningMessage(l10n.t('Your repository has no remotes configured to publish to.'));
4415
return;
4416
}
4417
4418
let publisher: RemoteSourcePublisher;
4419
4420
if (publishers.length === 1) {
4421
publisher = publishers[0];
4422
} else {
4423
const picks = publishers
4424
.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + l10n.t('Publish to {0}', provider.name), alwaysShow: true, provider }));
4425
const placeHolder = l10n.t('Pick a provider to publish the branch "{0}" to:', branchName);
4426
const choice = await window.showQuickPick(picks, { placeHolder });
4427
4428
if (!choice) {
4429
return;
4430
}
4431
4432
publisher = choice.provider;
4433
}
4434
4435
await publisher.publishRepository(new ApiRepository(repository));
4436
this.model.firePublishEvent(repository, branchName);
4437
4438
return;
4439
}
4440
4441
if (remotes.length === 1) {
4442
await repository.pushTo(remotes[0].name, branchName, true);
4443
this.model.firePublishEvent(repository, branchName);
4444
4445
return;
4446
}
4447
4448
const addRemote = new AddRemoteItem(this);
4449
const picks = [...repository.remotes.map(r => ({ label: r.name, description: r.pushUrl })), addRemote];
4450
const placeHolder = l10n.t('Pick a remote to publish the branch "{0}" to:', branchName);
4451
const choice = await window.showQuickPick(picks, { placeHolder });
4452
4453
if (!choice) {
4454
return;
4455
}
4456
4457
if (choice === addRemote) {
4458
const newRemote = await this.addRemote(repository);
4459
4460
if (newRemote) {
4461
await repository.pushTo(newRemote, branchName, true);
4462
4463
this.model.firePublishEvent(repository, branchName);
4464
}
4465
} else {
4466
await repository.pushTo(choice.label, branchName, true);
4467
4468
this.model.firePublishEvent(repository, branchName);
4469
}
4470
}
4471
4472
@command('git.ignore')
4473
async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
4474
resourceStates = resourceStates.filter(s => !!s);
4475
4476
if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
4477
const resource = this.getSCMResource();
4478
4479
if (!resource) {
4480
return;
4481
}
4482
4483
resourceStates = [resource];
4484
}
4485
4486
const resources = resourceStates
4487
.filter(s => s instanceof Resource)
4488
.map(r => r.resourceUri);
4489
4490
if (!resources.length) {
4491
return;
4492
}
4493
4494
await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
4495
}
4496
4497
@command('git.revealInExplorer')
4498
async revealInExplorer(resourceState: SourceControlResourceState): Promise<void> {
4499
if (!resourceState) {
4500
return;
4501
}
4502
4503
if (!(resourceState.resourceUri instanceof Uri)) {
4504
return;
4505
}
4506
4507
await commands.executeCommand('revealInExplorer', resourceState.resourceUri);
4508
}
4509
4510
@command('git.revealFileInOS.linux')
4511
@command('git.revealFileInOS.mac')
4512
@command('git.revealFileInOS.windows')
4513
async revealFileInOS(resourceState: SourceControlResourceState): Promise<void> {
4514
if (!resourceState) {
4515
return;
4516
}
4517
4518
if (!(resourceState.resourceUri instanceof Uri)) {
4519
return;
4520
}
4521
4522
await commands.executeCommand('revealFileInOS', resourceState.resourceUri);
4523
}
4524
4525
private async _stash(repository: Repository, includeUntracked = false, staged = false): Promise<boolean> {
4526
const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0
4527
&& (!includeUntracked || repository.untrackedGroup.resourceStates.length === 0);
4528
const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
4529
4530
if (staged) {
4531
if (noStagedChanges) {
4532
window.showInformationMessage(l10n.t('There are no staged changes to stash.'));
4533
return false;
4534
}
4535
} else {
4536
if (noUnstagedChanges && noStagedChanges) {
4537
window.showInformationMessage(l10n.t('There are no changes to stash.'));
4538
return false;
4539
}
4540
}
4541
4542
const config = workspace.getConfiguration('git', Uri.file(repository.root));
4543
const promptToSaveFilesBeforeStashing = config.get<'always' | 'staged' | 'never'>('promptToSaveFilesBeforeStash');
4544
4545
if (promptToSaveFilesBeforeStashing !== 'never') {
4546
let documents = workspace.textDocuments
4547
.filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath));
4548
4549
if (promptToSaveFilesBeforeStashing === 'staged' || repository.indexGroup.resourceStates.length > 0) {
4550
documents = documents
4551
.filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath)));
4552
}
4553
4554
if (documents.length > 0) {
4555
const message = documents.length === 1
4556
? l10n.t('The following file has unsaved changes which won\'t be included in the stash if you proceed: {0}.\n\nWould you like to save it before stashing?', path.basename(documents[0].uri.fsPath))
4557
: l10n.t('There are {0} unsaved files.\n\nWould you like to save them before stashing?', documents.length);
4558
const saveAndStash = l10n.t('Save All & Stash');
4559
const stash = l10n.t('Stash Anyway');
4560
const pick = await window.showWarningMessage(message, { modal: true }, saveAndStash, stash);
4561
4562
if (pick === saveAndStash) {
4563
await Promise.all(documents.map(d => d.save()));
4564
} else if (pick !== stash) {
4565
return false; // do not stash on cancel
4566
}
4567
}
4568
}
4569
4570
let message: string | undefined;
4571
4572
if (config.get<boolean>('useCommitInputAsStashMessage') && (!repository.sourceControl.commitTemplate || repository.inputBox.value !== repository.sourceControl.commitTemplate)) {
4573
message = repository.inputBox.value;
4574
}
4575
4576
message = await window.showInputBox({
4577
value: message,
4578
prompt: l10n.t('Optionally provide a stash message'),
4579
placeHolder: l10n.t('Stash message')
4580
});
4581
4582
if (typeof message === 'undefined') {
4583
return false;
4584
}
4585
4586
try {
4587
await repository.createStash(message, includeUntracked, staged);
4588
return true;
4589
} catch (err) {
4590
if (/You do not have the initial commit yet/.test(err.stderr || '')) {
4591
window.showInformationMessage(l10n.t('The repository does not have any commits. Please make an initial commit before creating a stash.'));
4592
return false;
4593
}
4594
4595
throw err;
4596
}
4597
}
4598
4599
@command('git.stash', { repository: true })
4600
async stash(repository: Repository): Promise<boolean> {
4601
const result = await this._stash(repository);
4602
return result;
4603
}
4604
4605
@command('git.stashStaged', { repository: true })
4606
async stashStaged(repository: Repository): Promise<boolean> {
4607
const result = await this._stash(repository, false, true);
4608
return result;
4609
}
4610
4611
@command('git.stashIncludeUntracked', { repository: true })
4612
async stashIncludeUntracked(repository: Repository): Promise<boolean> {
4613
const result = await this._stash(repository, true);
4614
return result;
4615
}
4616
4617
@command('git.stashPop', { repository: true })
4618
async stashPop(repository: Repository): Promise<void> {
4619
const placeHolder = l10n.t('Pick a stash to pop');
4620
const stash = await this.pickStash(repository, placeHolder);
4621
4622
if (!stash) {
4623
return;
4624
}
4625
4626
await repository.popStash(stash.index);
4627
}
4628
4629
@command('git.stashPopLatest', { repository: true })
4630
async stashPopLatest(repository: Repository): Promise<void> {
4631
const stashes = await repository.getStashes();
4632
4633
if (stashes.length === 0) {
4634
window.showInformationMessage(l10n.t('There are no stashes in the repository.'));
4635
return;
4636
}
4637
4638
await repository.popStash();
4639
}
4640
4641
@command('git.stashPopEditor')
4642
async stashPopEditor(uri: Uri): Promise<void> {
4643
const result = await this.getStashFromUri(uri);
4644
if (!result) {
4645
return;
4646
}
4647
4648
await commands.executeCommand('workbench.action.closeActiveEditor');
4649
await result.repository.popStash(result.stash.index);
4650
}
4651
4652
@command('git.stashApply', { repository: true })
4653
async stashApply(repository: Repository): Promise<void> {
4654
const placeHolder = l10n.t('Pick a stash to apply');
4655
const stash = await this.pickStash(repository, placeHolder);
4656
4657
if (!stash) {
4658
return;
4659
}
4660
4661
await repository.applyStash(stash.index);
4662
}
4663
4664
@command('git.stashApplyLatest', { repository: true })
4665
async stashApplyLatest(repository: Repository): Promise<void> {
4666
const stashes = await repository.getStashes();
4667
4668
if (stashes.length === 0) {
4669
window.showInformationMessage(l10n.t('There are no stashes in the repository.'));
4670
return;
4671
}
4672
4673
await repository.applyStash();
4674
}
4675
4676
@command('git.stashApplyEditor')
4677
async stashApplyEditor(uri: Uri): Promise<void> {
4678
const result = await this.getStashFromUri(uri);
4679
if (!result) {
4680
return;
4681
}
4682
4683
await commands.executeCommand('workbench.action.closeActiveEditor');
4684
await result.repository.applyStash(result.stash.index);
4685
}
4686
4687
@command('git.stashDrop', { repository: true })
4688
async stashDrop(repository: Repository): Promise<void> {
4689
const placeHolder = l10n.t('Pick a stash to drop');
4690
const stash = await this.pickStash(repository, placeHolder);
4691
4692
if (!stash) {
4693
return;
4694
}
4695
4696
await this._stashDrop(repository, stash);
4697
}
4698
4699
@command('git.stashDropAll', { repository: true })
4700
async stashDropAll(repository: Repository): Promise<void> {
4701
const stashes = await repository.getStashes();
4702
4703
if (stashes.length === 0) {
4704
window.showInformationMessage(l10n.t('There are no stashes in the repository.'));
4705
return;
4706
}
4707
4708
// request confirmation for the operation
4709
const yes = l10n.t('Yes');
4710
const question = stashes.length === 1 ?
4711
l10n.t('Are you sure you want to drop ALL stashes? There is 1 stash that will be subject to pruning, and MAY BE IMPOSSIBLE TO RECOVER.') :
4712
l10n.t('Are you sure you want to drop ALL stashes? There are {0} stashes that will be subject to pruning, and MAY BE IMPOSSIBLE TO RECOVER.', stashes.length);
4713
4714
const result = await window.showWarningMessage(question, { modal: true }, yes);
4715
if (result !== yes) {
4716
return;
4717
}
4718
4719
await repository.dropStash();
4720
}
4721
4722
@command('git.stashDropEditor')
4723
async stashDropEditor(uri: Uri): Promise<void> {
4724
const result = await this.getStashFromUri(uri);
4725
if (!result) {
4726
return;
4727
}
4728
4729
if (await this._stashDrop(result.repository, result.stash)) {
4730
await commands.executeCommand('workbench.action.closeActiveEditor');
4731
}
4732
}
4733
4734
async _stashDrop(repository: Repository, stash: Stash): Promise<boolean> {
4735
const yes = l10n.t('Yes');
4736
const result = await window.showWarningMessage(
4737
l10n.t('Are you sure you want to drop the stash: {0}?', stash.description),
4738
{ modal: true },
4739
yes
4740
);
4741
if (result !== yes) {
4742
return false;
4743
}
4744
4745
await repository.dropStash(stash.index);
4746
return true;
4747
}
4748
4749
@command('git.stashView', { repository: true })
4750
async stashView(repository: Repository): Promise<void> {
4751
const placeHolder = l10n.t('Pick a stash to view');
4752
const stash = await this.pickStash(repository, placeHolder);
4753
4754
if (!stash) {
4755
return;
4756
}
4757
4758
const stashChanges = await repository.showStash(stash.index);
4759
if (!stashChanges || stashChanges.length === 0) {
4760
return;
4761
}
4762
4763
// A stash commit can have up to 3 parents:
4764
// 1. The first parent is the commit that was HEAD when the stash was created.
4765
// 2. The second parent is the commit that represents the index when the stash was created.
4766
// 3. The third parent (when present) represents the untracked files when the stash was created.
4767
const stashFirstParentCommit = stash.parents.length > 0 ? stash.parents[0] : `${stash.hash}^`;
4768
const stashUntrackedFilesParentCommit = stash.parents.length === 3 ? stash.parents[2] : undefined;
4769
const stashUntrackedFiles: string[] = [];
4770
4771
if (stashUntrackedFilesParentCommit) {
4772
const untrackedFiles = await repository.getObjectFiles(stashUntrackedFilesParentCommit);
4773
stashUntrackedFiles.push(...untrackedFiles.map(f => path.join(repository.root, f.file)));
4774
}
4775
4776
const title = `Git Stash #${stash.index}: ${stash.description}`;
4777
const multiDiffSourceUri = toGitUri(Uri.file(repository.root), `stash@{${stash.index}}`, { scheme: 'git-stash' });
4778
4779
const resources: { originalUri: Uri | undefined; modifiedUri: Uri | undefined }[] = [];
4780
for (const change of stashChanges) {
4781
const isChangeUntracked = !!stashUntrackedFiles.find(f => pathEquals(f, change.uri.fsPath));
4782
const modifiedUriRef = !isChangeUntracked ? stash.hash : stashUntrackedFilesParentCommit ?? stash.hash;
4783
4784
resources.push(toMultiFileDiffEditorUris(change, stashFirstParentCommit, modifiedUriRef));
4785
}
4786
4787
commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources });
4788
}
4789
4790
private async pickStash(repository: Repository, placeHolder: string): Promise<Stash | undefined> {
4791
const getStashQuickPickItems = async (): Promise<StashItem[] | QuickPickItem[]> => {
4792
const stashes = await repository.getStashes();
4793
return stashes.length > 0 ?
4794
stashes.map(stash => new StashItem(stash)) :
4795
[{ label: l10n.t('$(info) This repository has no stashes.') }];
4796
};
4797
4798
const result = await window.showQuickPick<StashItem | QuickPickItem>(getStashQuickPickItems(), { placeHolder });
4799
return result instanceof StashItem ? result.stash : undefined;
4800
}
4801
4802
private async getStashFromUri(uri: Uri | undefined): Promise<{ repository: Repository; stash: Stash } | undefined> {
4803
if (!uri || uri.scheme !== 'git-stash') {
4804
return undefined;
4805
}
4806
4807
const stashUri = fromGitUri(uri);
4808
4809
// Repository
4810
const repository = this.model.getRepository(stashUri.path);
4811
if (!repository) {
4812
return undefined;
4813
}
4814
4815
// Stash
4816
const regex = /^stash@{(\d+)}$/;
4817
const match = regex.exec(stashUri.ref);
4818
if (!match) {
4819
return undefined;
4820
}
4821
4822
const [, index] = match;
4823
const stashes = await repository.getStashes();
4824
const stash = stashes.find(stash => stash.index === parseInt(index));
4825
if (!stash) {
4826
return undefined;
4827
}
4828
4829
return { repository, stash };
4830
}
4831
4832
@command('git.timeline.openDiff', { repository: false })
4833
async timelineOpenDiff(item: TimelineItem, uri: Uri | undefined, _source: string) {
4834
const cmd = this.resolveTimelineOpenDiffCommand(
4835
item, uri,
4836
{
4837
preserveFocus: true,
4838
preview: true,
4839
viewColumn: ViewColumn.Active
4840
},
4841
);
4842
if (cmd === undefined) {
4843
return undefined;
4844
}
4845
4846
return commands.executeCommand(cmd.command, ...(cmd.arguments ?? []));
4847
}
4848
4849
resolveTimelineOpenDiffCommand(item: TimelineItem, uri: Uri | undefined, options?: TextDocumentShowOptions): Command | undefined {
4850
if (uri === undefined || uri === null || !GitTimelineItem.is(item)) {
4851
return undefined;
4852
}
4853
4854
const basename = path.basename(uri.fsPath);
4855
4856
let title;
4857
if ((item.previousRef === 'HEAD' || item.previousRef === '~') && item.ref === '') {
4858
title = l10n.t('{0} (Working Tree)', basename);
4859
}
4860
else if (item.previousRef === 'HEAD' && item.ref === '~') {
4861
title = l10n.t('{0} (Index)', basename);
4862
} else {
4863
title = l10n.t('{0} ({1}) ↔ {0} ({2})', basename, item.shortPreviousRef, item.shortRef);
4864
}
4865
4866
return {
4867
command: 'vscode.diff',
4868
title: l10n.t('Open Comparison'),
4869
arguments: [toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title, options]
4870
};
4871
}
4872
4873
@command('git.timeline.viewCommit', { repository: false })
4874
async timelineViewCommit(item: TimelineItem, uri: Uri | undefined, _source: string) {
4875
if (!GitTimelineItem.is(item)) {
4876
return;
4877
}
4878
4879
const cmd = await this._resolveTimelineOpenCommitCommand(
4880
item, uri,
4881
{
4882
preserveFocus: true,
4883
preview: true,
4884
viewColumn: ViewColumn.Active
4885
},
4886
);
4887
if (cmd === undefined) {
4888
return undefined;
4889
}
4890
4891
return commands.executeCommand(cmd.command, ...(cmd.arguments ?? []));
4892
}
4893
4894
private async _resolveTimelineOpenCommitCommand(item: TimelineItem, uri: Uri | undefined, options?: TextDocumentShowOptions): Promise<Command | undefined> {
4895
if (uri === undefined || uri === null || !GitTimelineItem.is(item)) {
4896
return undefined;
4897
}
4898
4899
const repository = await this.model.getRepository(uri.fsPath);
4900
if (!repository) {
4901
return undefined;
4902
}
4903
4904
const commit = await repository.getCommit(item.ref);
4905
const commitParentId = commit.parents.length > 0 ? commit.parents[0] : await repository.getEmptyTree();
4906
const changes = await repository.diffTrees(commitParentId, commit.hash);
4907
const resources = changes.map(c => toMultiFileDiffEditorUris(c, commitParentId, commit.hash));
4908
4909
const title = `${item.shortRef} - ${truncate(commit.message)}`;
4910
const multiDiffSourceUri = Uri.from({ scheme: 'scm-history-item', path: `${repository.root}/${commitParentId}..${commit.hash}` });
4911
const reveal = { modifiedUri: toGitUri(uri, commit.hash) };
4912
4913
return {
4914
command: '_workbench.openMultiDiffEditor',
4915
title: l10n.t('Open Commit'),
4916
arguments: [{ multiDiffSourceUri, title, resources, reveal }, options]
4917
};
4918
}
4919
4920
@command('git.timeline.copyCommitId', { repository: false })
4921
async timelineCopyCommitId(item: TimelineItem, _uri: Uri | undefined, _source: string) {
4922
if (!GitTimelineItem.is(item)) {
4923
return;
4924
}
4925
4926
env.clipboard.writeText(item.ref);
4927
}
4928
4929
@command('git.timeline.copyCommitMessage', { repository: false })
4930
async timelineCopyCommitMessage(item: TimelineItem, _uri: Uri | undefined, _source: string) {
4931
if (!GitTimelineItem.is(item)) {
4932
return;
4933
}
4934
4935
env.clipboard.writeText(item.message);
4936
}
4937
4938
private _selectedForCompare: { uri: Uri; item: GitTimelineItem } | undefined;
4939
4940
@command('git.timeline.selectForCompare', { repository: false })
4941
async timelineSelectForCompare(item: TimelineItem, uri: Uri | undefined, _source: string) {
4942
if (!GitTimelineItem.is(item) || !uri) {
4943
return;
4944
}
4945
4946
this._selectedForCompare = { uri, item };
4947
await commands.executeCommand('setContext', 'git.timeline.selectedForCompare', true);
4948
}
4949
4950
@command('git.timeline.compareWithSelected', { repository: false })
4951
async timelineCompareWithSelected(item: TimelineItem, uri: Uri | undefined, _source: string) {
4952
if (!GitTimelineItem.is(item) || !uri || !this._selectedForCompare || uri.toString() !== this._selectedForCompare.uri.toString()) {
4953
return;
4954
}
4955
4956
const { item: selected } = this._selectedForCompare;
4957
4958
const basename = path.basename(uri.fsPath);
4959
let leftTitle;
4960
if ((selected.previousRef === 'HEAD' || selected.previousRef === '~') && selected.ref === '') {
4961
leftTitle = l10n.t('{0} (Working Tree)', basename);
4962
}
4963
else if (selected.previousRef === 'HEAD' && selected.ref === '~') {
4964
leftTitle = l10n.t('{0} (Index)', basename);
4965
} else {
4966
leftTitle = l10n.t('{0} ({1})', basename, selected.shortRef);
4967
}
4968
4969
let rightTitle;
4970
if ((item.previousRef === 'HEAD' || item.previousRef === '~') && item.ref === '') {
4971
rightTitle = l10n.t('{0} (Working Tree)', basename);
4972
}
4973
else if (item.previousRef === 'HEAD' && item.ref === '~') {
4974
rightTitle = l10n.t('{0} (Index)', basename);
4975
} else {
4976
rightTitle = l10n.t('{0} ({1})', basename, item.shortRef);
4977
}
4978
4979
4980
const title = l10n.t('{0} ↔ {1}', leftTitle, rightTitle);
4981
await commands.executeCommand('vscode.diff', selected.ref === '' ? uri : toGitUri(uri, selected.ref), item.ref === '' ? uri : toGitUri(uri, item.ref), title);
4982
}
4983
4984
@command('git.rebaseAbort', { repository: true })
4985
async rebaseAbort(repository: Repository): Promise<void> {
4986
if (repository.rebaseCommit) {
4987
await repository.rebaseAbort();
4988
} else {
4989
await window.showInformationMessage(l10n.t('No rebase in progress.'));
4990
}
4991
}
4992
4993
@command('git.closeAllDiffEditors', { repository: true })
4994
closeDiffEditors(repository: Repository): void {
4995
repository.closeDiffEditors(undefined, undefined, true);
4996
}
4997
4998
@command('git.closeAllUnmodifiedEditors')
4999
closeUnmodifiedEditors(): void {
5000
const editorTabsToClose: Tab[] = [];
5001
5002
// Collect all modified files
5003
const modifiedFiles: string[] = [];
5004
for (const repository of this.model.repositories) {
5005
modifiedFiles.push(...repository.indexGroup.resourceStates.map(r => r.resourceUri.fsPath));
5006
modifiedFiles.push(...repository.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath));
5007
modifiedFiles.push(...repository.untrackedGroup.resourceStates.map(r => r.resourceUri.fsPath));
5008
modifiedFiles.push(...repository.mergeGroup.resourceStates.map(r => r.resourceUri.fsPath));
5009
}
5010
5011
// Collect all editor tabs that are not dirty and not modified
5012
for (const tab of window.tabGroups.all.map(g => g.tabs).flat()) {
5013
if (tab.isDirty) {
5014
continue;
5015
}
5016
5017
if (tab.input instanceof TabInputText || tab.input instanceof TabInputNotebook) {
5018
const { uri } = tab.input;
5019
if (!modifiedFiles.find(p => pathEquals(p, uri.fsPath))) {
5020
editorTabsToClose.push(tab);
5021
}
5022
}
5023
}
5024
5025
// Close editors
5026
window.tabGroups.close(editorTabsToClose, true);
5027
}
5028
5029
@command('git.openRepositoriesInParentFolders')
5030
async openRepositoriesInParentFolders(): Promise<void> {
5031
const parentRepositories: string[] = [];
5032
5033
const title = l10n.t('Open Repositories In Parent Folders');
5034
const placeHolder = l10n.t('Pick a repository to open');
5035
5036
const allRepositoriesLabel = l10n.t('All Repositories');
5037
const allRepositoriesQuickPickItem: QuickPickItem = { label: allRepositoriesLabel };
5038
const repositoriesQuickPickItems: QuickPickItem[] = this.model.parentRepositories
5039
.sort(compareRepositoryLabel).map(r => new RepositoryItem(r));
5040
5041
const items = this.model.parentRepositories.length === 1 ? [...repositoriesQuickPickItems] :
5042
[...repositoriesQuickPickItems, { label: '', kind: QuickPickItemKind.Separator }, allRepositoriesQuickPickItem];
5043
5044
const repositoryItem = await window.showQuickPick(items, { title, placeHolder });
5045
if (!repositoryItem) {
5046
return;
5047
}
5048
5049
if (repositoryItem === allRepositoriesQuickPickItem) {
5050
// All Repositories
5051
parentRepositories.push(...this.model.parentRepositories);
5052
} else {
5053
// One Repository
5054
parentRepositories.push((repositoryItem as RepositoryItem).path);
5055
}
5056
5057
for (const parentRepository of parentRepositories) {
5058
await this.model.openParentRepository(parentRepository);
5059
}
5060
}
5061
5062
@command('git.manageUnsafeRepositories')
5063
async manageUnsafeRepositories(): Promise<void> {
5064
const unsafeRepositories: string[] = [];
5065
5066
const quickpick = window.createQuickPick();
5067
quickpick.title = l10n.t('Manage Unsafe Repositories');
5068
quickpick.placeholder = l10n.t('Pick a repository to mark as safe and open');
5069
5070
const allRepositoriesLabel = l10n.t('All Repositories');
5071
const allRepositoriesQuickPickItem: QuickPickItem = { label: allRepositoriesLabel };
5072
const repositoriesQuickPickItems: QuickPickItem[] = this.model.unsafeRepositories
5073
.sort(compareRepositoryLabel).map(r => new RepositoryItem(r));
5074
5075
quickpick.items = this.model.unsafeRepositories.length === 1 ? [...repositoriesQuickPickItems] :
5076
[...repositoriesQuickPickItems, { label: '', kind: QuickPickItemKind.Separator }, allRepositoriesQuickPickItem];
5077
5078
quickpick.show();
5079
const repositoryItem = await new Promise<RepositoryItem | QuickPickItem | undefined>(
5080
resolve => {
5081
quickpick.onDidAccept(() => resolve(quickpick.activeItems[0]));
5082
quickpick.onDidHide(() => resolve(undefined));
5083
});
5084
quickpick.hide();
5085
5086
if (!repositoryItem) {
5087
return;
5088
}
5089
5090
if (repositoryItem.label === allRepositoriesLabel) {
5091
// All Repositories
5092
unsafeRepositories.push(...this.model.unsafeRepositories);
5093
} else {
5094
// One Repository
5095
unsafeRepositories.push((repositoryItem as RepositoryItem).path);
5096
}
5097
5098
for (const unsafeRepository of unsafeRepositories) {
5099
// Mark as Safe
5100
await this.git.addSafeDirectory(this.model.getUnsafeRepositoryPath(unsafeRepository)!);
5101
5102
// Open Repository
5103
await this.model.openRepository(unsafeRepository);
5104
this.model.deleteUnsafeRepository(unsafeRepository);
5105
}
5106
}
5107
5108
@command('git.viewChanges', { repository: true })
5109
async viewChanges(repository: Repository): Promise<void> {
5110
await this._viewResourceGroupChanges(repository, repository.workingTreeGroup);
5111
}
5112
5113
@command('git.viewStagedChanges', { repository: true })
5114
async viewStagedChanges(repository: Repository): Promise<void> {
5115
await this._viewResourceGroupChanges(repository, repository.indexGroup);
5116
}
5117
5118
@command('git.viewUntrackedChanges', { repository: true })
5119
async viewUnstagedChanges(repository: Repository): Promise<void> {
5120
await this._viewResourceGroupChanges(repository, repository.untrackedGroup);
5121
}
5122
5123
private async _viewResourceGroupChanges(repository: Repository, resourceGroup: GitResourceGroup): Promise<void> {
5124
if (resourceGroup.resourceStates.length === 0) {
5125
switch (resourceGroup.id) {
5126
case 'index':
5127
window.showInformationMessage(l10n.t('The repository does not have any staged changes.'));
5128
break;
5129
case 'workingTree':
5130
window.showInformationMessage(l10n.t('The repository does not have any changes.'));
5131
break;
5132
case 'untracked':
5133
window.showInformationMessage(l10n.t('The repository does not have any untracked changes.'));
5134
break;
5135
}
5136
return;
5137
}
5138
5139
await commands.executeCommand('_workbench.openScmMultiDiffEditor', {
5140
title: `${repository.sourceControl.label}: ${resourceGroup.label}`,
5141
repositoryUri: Uri.file(repository.root),
5142
resourceGroupId: resourceGroup.id
5143
});
5144
}
5145
5146
@command('git.copyCommitId', { repository: true })
5147
async copyCommitId(repository: Repository, historyItem: SourceControlHistoryItem): Promise<void> {
5148
if (!repository || !historyItem) {
5149
return;
5150
}
5151
5152
env.clipboard.writeText(historyItem.id);
5153
}
5154
5155
@command('git.copyCommitMessage', { repository: true })
5156
async copyCommitMessage(repository: Repository, historyItem: SourceControlHistoryItem): Promise<void> {
5157
if (!repository || !historyItem) {
5158
return;
5159
}
5160
5161
env.clipboard.writeText(historyItem.message);
5162
}
5163
5164
@command('git.viewCommit', { repository: true })
5165
async viewCommit(repository: Repository, historyItemId: string, revealUri?: Uri): Promise<void> {
5166
if (!repository || !historyItemId) {
5167
return;
5168
}
5169
5170
const rootUri = Uri.file(repository.root);
5171
const config = workspace.getConfiguration('git', rootUri);
5172
const commitShortHashLength = config.get<number>('commitShortHashLength', 7);
5173
5174
const commit = await repository.getCommit(historyItemId);
5175
const title = `${truncate(historyItemId, commitShortHashLength, false)} - ${truncate(commit.message)}`;
5176
const historyItemParentId = commit.parents.length > 0 ? commit.parents[0] : await repository.getEmptyTree();
5177
5178
const multiDiffSourceUri = Uri.from({ scheme: 'scm-history-item', path: `${repository.root}/${historyItemParentId}..${historyItemId}` });
5179
5180
const changes = await repository.diffTrees(historyItemParentId, historyItemId);
5181
const resources = changes.map(c => toMultiFileDiffEditorUris(c, historyItemParentId, historyItemId));
5182
const reveal = revealUri ? { modifiedUri: toGitUri(revealUri, historyItemId) } : undefined;
5183
5184
await commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources, reveal });
5185
}
5186
5187
@command('git.copyContentToClipboard')
5188
async copyContentToClipboard(content: string): Promise<void> {
5189
if (typeof content !== 'string') {
5190
return;
5191
}
5192
5193
env.clipboard.writeText(content);
5194
}
5195
5196
@command('git.blame.toggleEditorDecoration')
5197
toggleBlameEditorDecoration(): void {
5198
this._toggleBlameSetting('blame.editorDecoration.enabled');
5199
}
5200
5201
@command('git.blame.toggleStatusBarItem')
5202
toggleBlameStatusBarItem(): void {
5203
this._toggleBlameSetting('blame.statusBarItem.enabled');
5204
}
5205
5206
private _toggleBlameSetting(setting: string): void {
5207
const config = workspace.getConfiguration('git');
5208
const enabled = config.get<boolean>(setting) === true;
5209
5210
config.update(setting, !enabled, true);
5211
}
5212
5213
private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any {
5214
const result = (...args: any[]) => {
5215
let result: Promise<any>;
5216
5217
if (!options.repository) {
5218
result = Promise.resolve(method.apply(this, args));
5219
} else {
5220
// try to guess the repository based on the first argument
5221
const repository = this.model.getRepository(args[0]);
5222
let repositoryPromise: Promise<Repository | undefined>;
5223
5224
if (repository) {
5225
repositoryPromise = Promise.resolve(repository);
5226
} else {
5227
repositoryPromise = this.model.pickRepository(options.repositoryFilter);
5228
}
5229
5230
result = repositoryPromise.then(repository => {
5231
if (!repository) {
5232
return Promise.resolve();
5233
}
5234
5235
return Promise.resolve(method.apply(this, [repository, ...args.slice(1)]));
5236
});
5237
}
5238
5239
/* __GDPR__
5240
"git.command" : {
5241
"owner": "lszomoru",
5242
"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The command id of the command being executed" }
5243
}
5244
*/
5245
this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });
5246
5247
return result.catch(err => {
5248
const options: MessageOptions = {
5249
modal: true
5250
};
5251
5252
let message: string;
5253
let type: 'error' | 'warning' | 'information' = 'error';
5254
5255
const choices = new Map<string, () => void>();
5256
const openOutputChannelChoice = l10n.t('Open Git Log');
5257
const outputChannelLogger = this.logger;
5258
choices.set(openOutputChannelChoice, () => outputChannelLogger.show());
5259
5260
const showCommandOutputChoice = l10n.t('Show Command Output');
5261
if (err.stderr) {
5262
choices.set(showCommandOutputChoice, async () => {
5263
const timestamp = new Date().getTime();
5264
const uri = Uri.parse(`git-output:/git-error-${timestamp}`);
5265
5266
let command = 'git';
5267
5268
if (err.gitArgs) {
5269
command = `${command} ${err.gitArgs.join(' ')}`;
5270
} else if (err.gitCommand) {
5271
command = `${command} ${err.gitCommand}`;
5272
}
5273
5274
this.commandErrors.set(uri, `> ${command}\n${err.stderr}`);
5275
5276
try {
5277
const doc = await workspace.openTextDocument(uri);
5278
await window.showTextDocument(doc);
5279
} finally {
5280
this.commandErrors.delete(uri);
5281
}
5282
});
5283
}
5284
5285
switch (err.gitErrorCode) {
5286
case GitErrorCodes.DirtyWorkTree:
5287
message = l10n.t('Please clean your repository working tree before checkout.');
5288
break;
5289
case GitErrorCodes.PushRejected:
5290
message = l10n.t('Can\'t push refs to remote. Try running "Pull" first to integrate your changes.');
5291
break;
5292
case GitErrorCodes.ForcePushWithLeaseRejected:
5293
case GitErrorCodes.ForcePushWithLeaseIfIncludesRejected:
5294
message = l10n.t('Can\'t force push refs to remote. The tip of the remote-tracking branch has been updated since the last checkout. Try running "Pull" first to pull the latest changes from the remote branch first.');
5295
break;
5296
case GitErrorCodes.Conflict:
5297
message = l10n.t('There are merge conflicts. Please resolve them before committing your changes.');
5298
type = 'warning';
5299
choices.clear();
5300
choices.set(l10n.t('Show Changes'), () => commands.executeCommand('workbench.view.scm'));
5301
options.modal = false;
5302
break;
5303
case GitErrorCodes.StashConflict:
5304
message = l10n.t('There are merge conflicts while applying the stash. Please resolve them before committing your changes.');
5305
type = 'warning';
5306
choices.clear();
5307
choices.set(l10n.t('Show Changes'), () => commands.executeCommand('workbench.view.scm'));
5308
options.modal = false;
5309
break;
5310
case GitErrorCodes.AuthenticationFailed: {
5311
const regex = /Authentication failed for '(.*)'/i;
5312
const match = regex.exec(err.stderr || String(err));
5313
5314
message = match
5315
? l10n.t('Failed to authenticate to git remote:\n\n{0}', match[1])
5316
: l10n.t('Failed to authenticate to git remote.');
5317
break;
5318
}
5319
case GitErrorCodes.NoUserNameConfigured:
5320
case GitErrorCodes.NoUserEmailConfigured:
5321
message = l10n.t('Make sure you configure your "user.name" and "user.email" in git.');
5322
choices.set(l10n.t('Learn More'), () => commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-setup-git')));
5323
break;
5324
case GitErrorCodes.EmptyCommitMessage:
5325
message = l10n.t('Commit operation was cancelled due to empty commit message.');
5326
choices.clear();
5327
type = 'information';
5328
options.modal = false;
5329
break;
5330
case GitErrorCodes.CherryPickEmpty:
5331
message = l10n.t('The changes are already present in the current branch.');
5332
choices.clear();
5333
type = 'information';
5334
options.modal = false;
5335
break;
5336
case GitErrorCodes.CherryPickConflict:
5337
message = l10n.t('There were merge conflicts while cherry picking the changes. Resolve the conflicts before committing them.');
5338
type = 'warning';
5339
choices.set(l10n.t('Show Changes'), () => commands.executeCommand('workbench.view.scm'));
5340
options.modal = false;
5341
break;
5342
default: {
5343
const hint = (err.stderr || err.message || String(err))
5344
.replace(/^error: /mi, '')
5345
.replace(/^> husky.*$/mi, '')
5346
.split(/[\r\n]/)
5347
.filter((line: string) => !!line)
5348
[0];
5349
5350
message = hint
5351
? l10n.t('Git: {0}', hint)
5352
: l10n.t('Git error');
5353
5354
break;
5355
}
5356
}
5357
5358
if (!message) {
5359
console.error(err);
5360
return;
5361
}
5362
5363
// We explicitly do not await this promise, because we do not
5364
// want the command execution to be stuck waiting for the user
5365
// to take action on the notification.
5366
this.showErrorNotification(type, message, options, choices);
5367
});
5368
};
5369
5370
// patch this object, so people can call methods directly
5371
(this as any)[key] = result;
5372
5373
return result;
5374
}
5375
5376
private async showErrorNotification(type: 'error' | 'warning' | 'information', message: string, options: MessageOptions, choices: Map<string, () => void>): Promise<void> {
5377
let result: string | undefined;
5378
const allChoices = Array.from(choices.keys());
5379
5380
switch (type) {
5381
case 'error':
5382
result = await window.showErrorMessage(message, options, ...allChoices);
5383
break;
5384
case 'warning':
5385
result = await window.showWarningMessage(message, options, ...allChoices);
5386
break;
5387
case 'information':
5388
result = await window.showInformationMessage(message, options, ...allChoices);
5389
break;
5390
}
5391
5392
if (result) {
5393
const resultFn = choices.get(result);
5394
5395
resultFn?.();
5396
}
5397
}
5398
5399
private getSCMResource(uri?: Uri): Resource | undefined {
5400
uri = uri ? uri : (window.activeTextEditor && window.activeTextEditor.document.uri);
5401
5402
this.logger.debug(`[CommandCenter][getSCMResource] git.getSCMResource.uri: ${uri && uri.toString()}`);
5403
5404
for (const r of this.model.repositories.map(r => r.root)) {
5405
this.logger.debug(`[CommandCenter][getSCMResource] repo root: ${r}`);
5406
}
5407
5408
if (!uri) {
5409
return undefined;
5410
}
5411
5412
if (isGitUri(uri)) {
5413
const { path } = fromGitUri(uri);
5414
uri = Uri.file(path);
5415
}
5416
5417
if (uri.scheme === 'file') {
5418
const uriString = uri.toString();
5419
const repository = this.model.getRepository(uri);
5420
5421
if (!repository) {
5422
return undefined;
5423
}
5424
5425
return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
5426
|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
5427
|| repository.mergeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
5428
}
5429
return undefined;
5430
}
5431
5432
private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;
5433
private runByRepository<T>(resources: Uri[], fn: (repository: Repository, resources: Uri[]) => Promise<T>): Promise<T[]>;
5434
private async runByRepository<T>(arg: Uri | Uri[], fn: (repository: Repository, resources: any) => Promise<T>): Promise<T[]> {
5435
const resources = arg instanceof Uri ? [arg] : arg;
5436
const isSingleResource = arg instanceof Uri;
5437
5438
const groups = resources.reduce((result, resource) => {
5439
let repository = this.model.getRepository(resource);
5440
5441
if (!repository) {
5442
console.warn('Could not find git repository for ', resource);
5443
return result;
5444
}
5445
5446
// Could it be a submodule?
5447
if (pathEquals(resource.fsPath, repository.root)) {
5448
repository = this.model.getRepositoryForSubmodule(resource) || repository;
5449
}
5450
5451
const tuple = result.filter(p => p.repository === repository)[0];
5452
5453
if (tuple) {
5454
tuple.resources.push(resource);
5455
} else {
5456
result.push({ repository, resources: [resource] });
5457
}
5458
5459
return result;
5460
}, [] as { repository: Repository; resources: Uri[] }[]);
5461
5462
const promises = groups
5463
.map(({ repository, resources }) => fn(repository as Repository, isSingleResource ? resources[0] : resources));
5464
5465
return Promise.all(promises);
5466
}
5467
5468
dispose(): void {
5469
this.disposables.forEach(d => d.dispose());
5470
}
5471
}
5472
5473