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