Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/repository.ts
3314 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 TelemetryReporter from '@vscode/extension-telemetry';
7
import * as fs from 'fs';
8
import * as path from 'path';
9
import picomatch from 'picomatch';
10
import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, FileType, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit } from 'vscode';
11
import { ActionButton } from './actionButton';
12
import { ApiRepository } from './api/api1';
13
import { Branch, BranchQuery, Change, CommitOptions, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status } from './api/git';
14
import { AutoFetcher } from './autofetch';
15
import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection';
16
import { debounce, memoize, sequentialize, throttle } from './decorators';
17
import { Repository as BaseRepository, BlameInformation, Commit, GitError, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule, Worktree } from './git';
18
import { GitHistoryProvider } from './historyProvider';
19
import { Operation, OperationKind, OperationManager, OperationResult } from './operation';
20
import { CommitCommandsCenter, IPostCommitCommandsProviderRegistry } from './postCommitCommands';
21
import { IPushErrorHandlerRegistry } from './pushError';
22
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
23
import { StatusBarCommands } from './statusbar';
24
import { toGitUri } from './uri';
25
import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isDescendant, isLinuxSnap, isRemote, isWindows, Limiter, onceEvent, pathEquals, relativePath } from './util';
26
import { IFileWatcher, watch } from './watch';
27
import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider';
28
29
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
30
31
const iconsRootPath = path.join(path.dirname(__dirname), 'resources', 'icons');
32
33
function getIconUri(iconName: string, theme: string): Uri {
34
return Uri.file(path.join(iconsRootPath, theme, `${iconName}.svg`));
35
}
36
37
export const enum RepositoryState {
38
Idle,
39
Disposed
40
}
41
42
export const enum ResourceGroupType {
43
Merge,
44
Index,
45
WorkingTree,
46
Untracked
47
}
48
49
export class Resource implements SourceControlResourceState {
50
51
static getStatusLetter(type: Status): string {
52
switch (type) {
53
case Status.INDEX_MODIFIED:
54
case Status.MODIFIED:
55
return 'M';
56
case Status.INDEX_ADDED:
57
case Status.INTENT_TO_ADD:
58
return 'A';
59
case Status.INDEX_DELETED:
60
case Status.DELETED:
61
return 'D';
62
case Status.INDEX_RENAMED:
63
case Status.INTENT_TO_RENAME:
64
return 'R';
65
case Status.TYPE_CHANGED:
66
return 'T';
67
case Status.UNTRACKED:
68
return 'U';
69
case Status.IGNORED:
70
return 'I';
71
case Status.INDEX_COPIED:
72
return 'C';
73
case Status.BOTH_DELETED:
74
case Status.ADDED_BY_US:
75
case Status.DELETED_BY_THEM:
76
case Status.ADDED_BY_THEM:
77
case Status.DELETED_BY_US:
78
case Status.BOTH_ADDED:
79
case Status.BOTH_MODIFIED:
80
return '!'; // Using ! instead of ⚠, because the latter looks really bad on windows
81
default:
82
throw new Error('Unknown git status: ' + type);
83
}
84
}
85
86
static getStatusText(type: Status) {
87
switch (type) {
88
case Status.INDEX_MODIFIED: return l10n.t('Index Modified');
89
case Status.MODIFIED: return l10n.t('Modified');
90
case Status.INDEX_ADDED: return l10n.t('Index Added');
91
case Status.INDEX_DELETED: return l10n.t('Index Deleted');
92
case Status.DELETED: return l10n.t('Deleted');
93
case Status.INDEX_RENAMED: return l10n.t('Index Renamed');
94
case Status.INDEX_COPIED: return l10n.t('Index Copied');
95
case Status.UNTRACKED: return l10n.t('Untracked');
96
case Status.IGNORED: return l10n.t('Ignored');
97
case Status.INTENT_TO_ADD: return l10n.t('Intent to Add');
98
case Status.INTENT_TO_RENAME: return l10n.t('Intent to Rename');
99
case Status.TYPE_CHANGED: return l10n.t('Type Changed');
100
case Status.BOTH_DELETED: return l10n.t('Conflict: Both Deleted');
101
case Status.ADDED_BY_US: return l10n.t('Conflict: Added By Us');
102
case Status.DELETED_BY_THEM: return l10n.t('Conflict: Deleted By Them');
103
case Status.ADDED_BY_THEM: return l10n.t('Conflict: Added By Them');
104
case Status.DELETED_BY_US: return l10n.t('Conflict: Deleted By Us');
105
case Status.BOTH_ADDED: return l10n.t('Conflict: Both Added');
106
case Status.BOTH_MODIFIED: return l10n.t('Conflict: Both Modified');
107
default: return '';
108
}
109
}
110
111
static getStatusColor(type: Status): ThemeColor {
112
switch (type) {
113
case Status.INDEX_MODIFIED:
114
return new ThemeColor('gitDecoration.stageModifiedResourceForeground');
115
case Status.MODIFIED:
116
case Status.TYPE_CHANGED:
117
return new ThemeColor('gitDecoration.modifiedResourceForeground');
118
case Status.INDEX_DELETED:
119
return new ThemeColor('gitDecoration.stageDeletedResourceForeground');
120
case Status.DELETED:
121
return new ThemeColor('gitDecoration.deletedResourceForeground');
122
case Status.INDEX_ADDED:
123
case Status.INTENT_TO_ADD:
124
return new ThemeColor('gitDecoration.addedResourceForeground');
125
case Status.INDEX_COPIED:
126
case Status.INDEX_RENAMED:
127
case Status.INTENT_TO_RENAME:
128
return new ThemeColor('gitDecoration.renamedResourceForeground');
129
case Status.UNTRACKED:
130
return new ThemeColor('gitDecoration.untrackedResourceForeground');
131
case Status.IGNORED:
132
return new ThemeColor('gitDecoration.ignoredResourceForeground');
133
case Status.BOTH_DELETED:
134
case Status.ADDED_BY_US:
135
case Status.DELETED_BY_THEM:
136
case Status.ADDED_BY_THEM:
137
case Status.DELETED_BY_US:
138
case Status.BOTH_ADDED:
139
case Status.BOTH_MODIFIED:
140
return new ThemeColor('gitDecoration.conflictingResourceForeground');
141
default:
142
throw new Error('Unknown git status: ' + type);
143
}
144
}
145
146
@memoize
147
get resourceUri(): Uri {
148
if (this.renameResourceUri && (this._type === Status.MODIFIED || this._type === Status.DELETED || this._type === Status.INDEX_RENAMED || this._type === Status.INDEX_COPIED || this._type === Status.INTENT_TO_RENAME)) {
149
return this.renameResourceUri;
150
}
151
152
return this._resourceUri;
153
}
154
155
get leftUri(): Uri | undefined {
156
return this.resources.left;
157
}
158
159
get rightUri(): Uri | undefined {
160
return this.resources.right;
161
}
162
163
get multiDiffEditorOriginalUri(): Uri | undefined {
164
return this.resources.original;
165
}
166
167
get multiFileDiffEditorModifiedUri(): Uri | undefined {
168
return this.resources.modified;
169
}
170
171
@memoize
172
get command(): Command {
173
return this._commandResolver.resolveDefaultCommand(this);
174
}
175
176
@memoize
177
private get resources(): { left: Uri | undefined; right: Uri | undefined; original: Uri | undefined; modified: Uri | undefined } {
178
return this._commandResolver.getResources(this);
179
}
180
181
get resourceGroupType(): ResourceGroupType { return this._resourceGroupType; }
182
get type(): Status { return this._type; }
183
get original(): Uri { return this._resourceUri; }
184
get renameResourceUri(): Uri | undefined { return this._renameResourceUri; }
185
get contextValue(): string | undefined { return this._repositoryKind; }
186
187
private static Icons: any = {
188
light: {
189
Modified: getIconUri('status-modified', 'light'),
190
Added: getIconUri('status-added', 'light'),
191
Deleted: getIconUri('status-deleted', 'light'),
192
Renamed: getIconUri('status-renamed', 'light'),
193
Copied: getIconUri('status-copied', 'light'),
194
Untracked: getIconUri('status-untracked', 'light'),
195
Ignored: getIconUri('status-ignored', 'light'),
196
Conflict: getIconUri('status-conflict', 'light'),
197
TypeChanged: getIconUri('status-type-changed', 'light')
198
},
199
dark: {
200
Modified: getIconUri('status-modified', 'dark'),
201
Added: getIconUri('status-added', 'dark'),
202
Deleted: getIconUri('status-deleted', 'dark'),
203
Renamed: getIconUri('status-renamed', 'dark'),
204
Copied: getIconUri('status-copied', 'dark'),
205
Untracked: getIconUri('status-untracked', 'dark'),
206
Ignored: getIconUri('status-ignored', 'dark'),
207
Conflict: getIconUri('status-conflict', 'dark'),
208
TypeChanged: getIconUri('status-type-changed', 'dark')
209
}
210
};
211
212
private getIconPath(theme: string): Uri {
213
switch (this.type) {
214
case Status.INDEX_MODIFIED: return Resource.Icons[theme].Modified;
215
case Status.MODIFIED: return Resource.Icons[theme].Modified;
216
case Status.INDEX_ADDED: return Resource.Icons[theme].Added;
217
case Status.INDEX_DELETED: return Resource.Icons[theme].Deleted;
218
case Status.DELETED: return Resource.Icons[theme].Deleted;
219
case Status.INDEX_RENAMED: return Resource.Icons[theme].Renamed;
220
case Status.INDEX_COPIED: return Resource.Icons[theme].Copied;
221
case Status.UNTRACKED: return Resource.Icons[theme].Untracked;
222
case Status.IGNORED: return Resource.Icons[theme].Ignored;
223
case Status.INTENT_TO_ADD: return Resource.Icons[theme].Added;
224
case Status.INTENT_TO_RENAME: return Resource.Icons[theme].Renamed;
225
case Status.TYPE_CHANGED: return Resource.Icons[theme].TypeChanged;
226
case Status.BOTH_DELETED: return Resource.Icons[theme].Conflict;
227
case Status.ADDED_BY_US: return Resource.Icons[theme].Conflict;
228
case Status.DELETED_BY_THEM: return Resource.Icons[theme].Conflict;
229
case Status.ADDED_BY_THEM: return Resource.Icons[theme].Conflict;
230
case Status.DELETED_BY_US: return Resource.Icons[theme].Conflict;
231
case Status.BOTH_ADDED: return Resource.Icons[theme].Conflict;
232
case Status.BOTH_MODIFIED: return Resource.Icons[theme].Conflict;
233
default: throw new Error('Unknown git status: ' + this.type);
234
}
235
}
236
237
private get tooltip(): string {
238
return Resource.getStatusText(this.type);
239
}
240
241
private get strikeThrough(): boolean {
242
switch (this.type) {
243
case Status.DELETED:
244
case Status.BOTH_DELETED:
245
case Status.DELETED_BY_THEM:
246
case Status.DELETED_BY_US:
247
case Status.INDEX_DELETED:
248
return true;
249
default:
250
return false;
251
}
252
}
253
254
@memoize
255
private get faded(): boolean {
256
// TODO@joao
257
return false;
258
// const workspaceRootPath = this.workspaceRoot.fsPath;
259
// return this.resourceUri.fsPath.substr(0, workspaceRootPath.length) !== workspaceRootPath;
260
}
261
262
get decorations(): SourceControlResourceDecorations {
263
const light = this._useIcons ? { iconPath: this.getIconPath('light') } : undefined;
264
const dark = this._useIcons ? { iconPath: this.getIconPath('dark') } : undefined;
265
const tooltip = this.tooltip;
266
const strikeThrough = this.strikeThrough;
267
const faded = this.faded;
268
return { strikeThrough, faded, tooltip, light, dark };
269
}
270
271
get letter(): string {
272
return Resource.getStatusLetter(this.type);
273
}
274
275
get color(): ThemeColor {
276
return Resource.getStatusColor(this.type);
277
}
278
279
get priority(): number {
280
switch (this.type) {
281
case Status.INDEX_MODIFIED:
282
case Status.MODIFIED:
283
case Status.INDEX_COPIED:
284
case Status.TYPE_CHANGED:
285
return 2;
286
case Status.IGNORED:
287
return 3;
288
case Status.BOTH_DELETED:
289
case Status.ADDED_BY_US:
290
case Status.DELETED_BY_THEM:
291
case Status.ADDED_BY_THEM:
292
case Status.DELETED_BY_US:
293
case Status.BOTH_ADDED:
294
case Status.BOTH_MODIFIED:
295
return 4;
296
default:
297
return 1;
298
}
299
}
300
301
get resourceDecoration(): FileDecoration {
302
const res = new FileDecoration(this.letter, this.tooltip, this.color);
303
res.propagate = this.type !== Status.DELETED && this.type !== Status.INDEX_DELETED;
304
return res;
305
}
306
307
constructor(
308
private _commandResolver: ResourceCommandResolver,
309
private _resourceGroupType: ResourceGroupType,
310
private _resourceUri: Uri,
311
private _type: Status,
312
private _useIcons: boolean,
313
private _renameResourceUri?: Uri,
314
private _repositoryKind?: 'repository' | 'submodule' | 'worktree',
315
) { }
316
317
async open(): Promise<void> {
318
const command = this.command;
319
await commands.executeCommand<void>(command.command, ...(command.arguments || []));
320
}
321
322
async openFile(): Promise<void> {
323
const command = this._commandResolver.resolveFileCommand(this);
324
await commands.executeCommand<void>(command.command, ...(command.arguments || []));
325
}
326
327
async openChange(): Promise<void> {
328
const command = this._commandResolver.resolveChangeCommand(this);
329
await commands.executeCommand<void>(command.command, ...(command.arguments || []));
330
}
331
332
async compareWithWorkspace(): Promise<void> {
333
const command = this._commandResolver.resolveCompareWithWorkspaceCommand(this);
334
await commands.executeCommand<void>(command.command, ...(command.arguments || []));
335
}
336
337
clone(resourceGroupType?: ResourceGroupType) {
338
return new Resource(this._commandResolver, resourceGroupType ?? this._resourceGroupType, this._resourceUri, this._type, this._useIcons, this._renameResourceUri, this._repositoryKind);
339
}
340
}
341
342
export interface GitResourceGroup extends SourceControlResourceGroup {
343
resourceStates: Resource[];
344
}
345
346
interface GitResourceGroups {
347
indexGroup?: Resource[];
348
mergeGroup?: Resource[];
349
untrackedGroup?: Resource[];
350
workingTreeGroup?: Resource[];
351
}
352
353
class ProgressManager {
354
355
private enabled = false;
356
private disposable: IDisposable = EmptyDisposable;
357
358
constructor(private repository: Repository) {
359
const onDidChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git', Uri.file(this.repository.root)));
360
onDidChange(_ => this.updateEnablement());
361
this.updateEnablement();
362
363
this.repository.onDidChangeOperations(() => {
364
// Disable input box when the commit operation is running
365
this.repository.sourceControl.inputBox.enabled = !this.repository.operations.isRunning(OperationKind.Commit);
366
});
367
}
368
369
private updateEnablement(): void {
370
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
371
372
if (config.get<boolean>('showProgress')) {
373
this.enable();
374
} else {
375
this.disable();
376
}
377
}
378
379
private enable(): void {
380
if (this.enabled) {
381
return;
382
}
383
384
const start = onceEvent(filterEvent(this.repository.onDidChangeOperations, () => this.repository.operations.shouldShowProgress()));
385
const end = onceEvent(filterEvent(debounceEvent(this.repository.onDidChangeOperations, 300), () => !this.repository.operations.shouldShowProgress()));
386
387
const setup = () => {
388
this.disposable = start(() => {
389
const promise = eventToPromise(end).then(() => setup());
390
window.withProgress({ location: ProgressLocation.SourceControl }, () => promise);
391
});
392
};
393
394
setup();
395
this.enabled = true;
396
}
397
398
private disable(): void {
399
if (!this.enabled) {
400
return;
401
}
402
403
this.disposable.dispose();
404
this.disposable = EmptyDisposable;
405
this.enabled = false;
406
}
407
408
dispose(): void {
409
this.disable();
410
}
411
}
412
413
class FileEventLogger {
414
415
private eventDisposable: IDisposable = EmptyDisposable;
416
private logLevelDisposable: IDisposable = EmptyDisposable;
417
418
constructor(
419
private onWorkspaceWorkingTreeFileChange: Event<Uri>,
420
private onDotGitFileChange: Event<Uri>,
421
private logger: LogOutputChannel
422
) {
423
this.logLevelDisposable = logger.onDidChangeLogLevel(this.onDidChangeLogLevel, this);
424
this.onDidChangeLogLevel(logger.logLevel);
425
}
426
427
private onDidChangeLogLevel(logLevel: LogLevel): void {
428
this.eventDisposable.dispose();
429
430
if (logLevel > LogLevel.Debug) {
431
return;
432
}
433
434
this.eventDisposable = combinedDisposable([
435
this.onWorkspaceWorkingTreeFileChange(uri => this.logger.debug(`[FileEventLogger][onWorkspaceWorkingTreeFileChange] ${uri.fsPath}`)),
436
this.onDotGitFileChange(uri => this.logger.debug(`[FileEventLogger][onDotGitFileChange] ${uri.fsPath}`))
437
]);
438
}
439
440
dispose(): void {
441
this.eventDisposable.dispose();
442
this.logLevelDisposable.dispose();
443
}
444
}
445
446
class DotGitWatcher implements IFileWatcher {
447
448
readonly event: Event<Uri>;
449
450
private emitter = new EventEmitter<Uri>();
451
private transientDisposables: IDisposable[] = [];
452
private disposables: IDisposable[] = [];
453
454
constructor(
455
private repository: Repository,
456
private logger: LogOutputChannel
457
) {
458
const rootWatcher = watch(repository.dotGit.path);
459
this.disposables.push(rootWatcher);
460
461
// Ignore changes to the "index.lock" file, and watchman fsmonitor hook (https://git-scm.com/docs/githooks#_fsmonitor_watchman) cookie files.
462
// Watchman creates a cookie file inside the git directory whenever a query is run (https://facebook.github.io/watchman/docs/cookies.html).
463
const filteredRootWatcher = filterEvent(rootWatcher.event, uri => uri.scheme === 'file' && !/\/\.git(\/index\.lock)?$|\/\.watchman-cookie-/.test(uri.path));
464
this.event = anyEvent(filteredRootWatcher, this.emitter.event);
465
466
repository.onDidRunGitStatus(this.updateTransientWatchers, this, this.disposables);
467
this.updateTransientWatchers();
468
}
469
470
private updateTransientWatchers() {
471
this.transientDisposables = dispose(this.transientDisposables);
472
473
if (!this.repository.HEAD || !this.repository.HEAD.upstream) {
474
return;
475
}
476
477
this.transientDisposables = dispose(this.transientDisposables);
478
479
const { name, remote } = this.repository.HEAD.upstream;
480
const upstreamPath = path.join(this.repository.dotGit.commonPath ?? this.repository.dotGit.path, 'refs', 'remotes', remote, name);
481
482
try {
483
const upstreamWatcher = watch(upstreamPath);
484
this.transientDisposables.push(upstreamWatcher);
485
upstreamWatcher.event(this.emitter.fire, this.emitter, this.transientDisposables);
486
} catch (err) {
487
this.logger.warn(`[DotGitWatcher][updateTransientWatchers] Failed to watch ref '${upstreamPath}', is most likely packed.`);
488
}
489
}
490
491
dispose() {
492
this.emitter.dispose();
493
this.transientDisposables = dispose(this.transientDisposables);
494
this.disposables = dispose(this.disposables);
495
}
496
}
497
498
class ResourceCommandResolver {
499
500
constructor(private repository: Repository) { }
501
502
resolveDefaultCommand(resource: Resource): Command {
503
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
504
const openDiffOnClick = config.get<boolean>('openDiffOnClick', true);
505
return openDiffOnClick ? this.resolveChangeCommand(resource) : this.resolveFileCommand(resource);
506
}
507
508
resolveFileCommand(resource: Resource): Command {
509
return {
510
command: 'vscode.open',
511
title: l10n.t('Open'),
512
arguments: [resource.resourceUri]
513
};
514
}
515
516
resolveChangeCommand(resource: Resource, compareWithWorkspace?: boolean, leftUri?: Uri): Command {
517
if (!compareWithWorkspace) {
518
leftUri = resource.leftUri;
519
}
520
521
const title = this.getTitle(resource);
522
523
if (!leftUri) {
524
const bothModified = resource.type === Status.BOTH_MODIFIED;
525
if (resource.rightUri && workspace.getConfiguration('git').get<boolean>('mergeEditor', false) && (bothModified || resource.type === Status.BOTH_ADDED)) {
526
const command = this.repository.isWorktreeMigrating ? 'git.openWorktreeMergeEditor' : 'git.openMergeEditor';
527
return {
528
command,
529
title: l10n.t('Open Merge'),
530
arguments: [resource.rightUri]
531
};
532
} else {
533
return {
534
command: 'vscode.open',
535
title: l10n.t('Open'),
536
arguments: [resource.rightUri, { override: bothModified ? false : undefined }, title]
537
};
538
}
539
} else {
540
return {
541
command: 'vscode.diff',
542
title: l10n.t('Open'),
543
arguments: [leftUri, resource.rightUri, title]
544
};
545
}
546
}
547
548
resolveCompareWithWorkspaceCommand(resource: Resource): Command {
549
// Resource is not a worktree
550
if (!this.repository.dotGit.commonPath) {
551
return this.resolveChangeCommand(resource);
552
}
553
554
const parentRepoRoot = path.dirname(this.repository.dotGit.commonPath);
555
const relPath = path.relative(this.repository.root, resource.resourceUri.fsPath);
556
const candidateFsPath = path.join(parentRepoRoot, relPath);
557
558
const leftUri = fs.existsSync(candidateFsPath) ? Uri.file(candidateFsPath) : undefined;
559
560
return this.resolveChangeCommand(resource, true, leftUri);
561
}
562
563
getResources(resource: Resource): { left: Uri | undefined; right: Uri | undefined; original: Uri | undefined; modified: Uri | undefined } {
564
for (const submodule of this.repository.submodules) {
565
if (path.join(this.repository.root, submodule.path) === resource.resourceUri.fsPath) {
566
const original = undefined;
567
const modified = toGitUri(resource.resourceUri, resource.resourceGroupType === ResourceGroupType.Index ? 'index' : 'wt', { submoduleOf: this.repository.root });
568
return { left: original, right: modified, original, modified };
569
}
570
}
571
572
const left = this.getLeftResource(resource);
573
const right = this.getRightResource(resource);
574
575
return {
576
left: left.original ?? left.modified,
577
right: right.original ?? right.modified,
578
original: left.original ?? right.original,
579
modified: left.modified ?? right.modified,
580
};
581
}
582
583
private getLeftResource(resource: Resource): ModifiedOrOriginal {
584
switch (resource.type) {
585
case Status.INDEX_MODIFIED:
586
case Status.INDEX_RENAMED:
587
case Status.INTENT_TO_RENAME:
588
case Status.TYPE_CHANGED:
589
return { original: toGitUri(resource.original, 'HEAD') };
590
591
case Status.MODIFIED:
592
return { original: toGitUri(resource.resourceUri, '~') };
593
594
case Status.DELETED_BY_US:
595
case Status.DELETED_BY_THEM:
596
return { original: toGitUri(resource.resourceUri, '~1') };
597
}
598
return {};
599
}
600
601
private getRightResource(resource: Resource): ModifiedOrOriginal {
602
switch (resource.type) {
603
case Status.INDEX_MODIFIED:
604
case Status.INDEX_ADDED:
605
case Status.INDEX_COPIED:
606
case Status.INDEX_RENAMED:
607
return { modified: toGitUri(resource.resourceUri, '') };
608
609
case Status.INDEX_DELETED:
610
case Status.DELETED:
611
return { original: toGitUri(resource.resourceUri, 'HEAD') };
612
613
case Status.DELETED_BY_US:
614
return { original: toGitUri(resource.resourceUri, '~3') };
615
616
case Status.DELETED_BY_THEM:
617
return { original: toGitUri(resource.resourceUri, '~2') };
618
619
case Status.MODIFIED:
620
case Status.UNTRACKED:
621
case Status.IGNORED:
622
case Status.INTENT_TO_ADD:
623
case Status.INTENT_TO_RENAME:
624
case Status.TYPE_CHANGED: {
625
const uriString = resource.resourceUri.toString();
626
const [indexStatus] = this.repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
627
628
if (indexStatus && indexStatus.renameResourceUri) {
629
return { modified: indexStatus.renameResourceUri };
630
}
631
632
return { modified: resource.resourceUri };
633
}
634
case Status.BOTH_ADDED:
635
case Status.BOTH_MODIFIED:
636
return { modified: resource.resourceUri };
637
}
638
639
return {};
640
}
641
642
private getTitle(resource: Resource): string {
643
const basename = path.basename(resource.resourceUri.fsPath);
644
645
switch (resource.type) {
646
case Status.INDEX_MODIFIED:
647
case Status.INDEX_RENAMED:
648
case Status.INDEX_ADDED:
649
return l10n.t('{0} (Index)', basename);
650
651
case Status.MODIFIED:
652
case Status.BOTH_ADDED:
653
case Status.BOTH_MODIFIED:
654
return l10n.t('{0} (Working Tree)', basename);
655
656
case Status.INDEX_DELETED:
657
case Status.DELETED:
658
return l10n.t('{0} (Deleted)', basename);
659
660
case Status.DELETED_BY_US:
661
return l10n.t('{0} (Theirs)', basename);
662
663
case Status.DELETED_BY_THEM:
664
return l10n.t('{0} (Ours)', basename);
665
666
case Status.UNTRACKED:
667
return l10n.t('{0} (Untracked)', basename);
668
669
case Status.INTENT_TO_ADD:
670
case Status.INTENT_TO_RENAME:
671
return l10n.t('{0} (Intent to add)', basename);
672
673
case Status.TYPE_CHANGED:
674
return l10n.t('{0} (Type changed)', basename);
675
676
default:
677
return '';
678
}
679
}
680
}
681
682
interface ModifiedOrOriginal {
683
modified?: Uri | undefined;
684
original?: Uri | undefined;
685
}
686
687
interface BranchProtectionMatcher {
688
include?: picomatch.Matcher;
689
exclude?: picomatch.Matcher;
690
}
691
692
export interface IRepositoryResolver {
693
getRepository(sourceControl: SourceControl): Repository | undefined;
694
getRepository(resourceGroup: SourceControlResourceGroup): Repository | undefined;
695
getRepository(path: string): Repository | undefined;
696
getRepository(resource: Uri): Repository | undefined;
697
getRepository(hint: any): Repository | undefined;
698
}
699
700
export class Repository implements Disposable {
701
702
private _onDidChangeRepository = new EventEmitter<Uri>();
703
readonly onDidChangeRepository: Event<Uri> = this._onDidChangeRepository.event;
704
705
private _onDidChangeState = new EventEmitter<RepositoryState>();
706
readonly onDidChangeState: Event<RepositoryState> = this._onDidChangeState.event;
707
708
private _onDidChangeStatus = new EventEmitter<void>();
709
readonly onDidRunGitStatus: Event<void> = this._onDidChangeStatus.event;
710
711
private _onDidChangeOriginalResource = new EventEmitter<Uri>();
712
readonly onDidChangeOriginalResource: Event<Uri> = this._onDidChangeOriginalResource.event;
713
714
private _onRunOperation = new EventEmitter<OperationKind>();
715
readonly onRunOperation: Event<OperationKind> = this._onRunOperation.event;
716
717
private _onDidRunOperation = new EventEmitter<OperationResult>();
718
readonly onDidRunOperation: Event<OperationResult> = this._onDidRunOperation.event;
719
720
private _onDidChangeBranchProtection = new EventEmitter<void>();
721
readonly onDidChangeBranchProtection: Event<void> = this._onDidChangeBranchProtection.event;
722
723
@memoize
724
get onDidChangeOperations(): Event<void> {
725
return anyEvent(this.onRunOperation as Event<any>, this.onDidRunOperation as Event<any>);
726
}
727
728
private _sourceControl: SourceControl;
729
get sourceControl(): SourceControl { return this._sourceControl; }
730
731
get inputBox(): SourceControlInputBox { return this._sourceControl.inputBox; }
732
733
private _mergeGroup: SourceControlResourceGroup;
734
get mergeGroup(): GitResourceGroup { return this._mergeGroup as GitResourceGroup; }
735
736
private _indexGroup: SourceControlResourceGroup;
737
get indexGroup(): GitResourceGroup { return this._indexGroup as GitResourceGroup; }
738
739
private _workingTreeGroup: SourceControlResourceGroup;
740
get workingTreeGroup(): GitResourceGroup { return this._workingTreeGroup as GitResourceGroup; }
741
742
private _untrackedGroup: SourceControlResourceGroup;
743
get untrackedGroup(): GitResourceGroup { return this._untrackedGroup as GitResourceGroup; }
744
745
private _EMPTY_TREE: string | undefined;
746
747
private _HEAD: Branch | undefined;
748
get HEAD(): Branch | undefined {
749
return this._HEAD;
750
}
751
752
private _refs: Ref[] = [];
753
get refs(): Ref[] {
754
return this._refs;
755
}
756
757
get headShortName(): string | undefined {
758
if (!this.HEAD) {
759
return;
760
}
761
762
const HEAD = this.HEAD;
763
764
if (HEAD.name) {
765
return HEAD.name;
766
}
767
768
return (HEAD.commit || '').substr(0, 8);
769
}
770
771
private _remotes: Remote[] = [];
772
get remotes(): Remote[] {
773
return this._remotes;
774
}
775
776
private _submodules: Submodule[] = [];
777
get submodules(): Submodule[] {
778
return this._submodules;
779
}
780
781
private _worktrees: Worktree[] = [];
782
get worktrees(): Worktree[] {
783
return this._worktrees;
784
}
785
786
private _rebaseCommit: Commit | undefined = undefined;
787
788
set rebaseCommit(rebaseCommit: Commit | undefined) {
789
if (this._rebaseCommit && !rebaseCommit) {
790
this.inputBox.value = '';
791
} else if (rebaseCommit && (!this._rebaseCommit || this._rebaseCommit.hash !== rebaseCommit.hash)) {
792
this.inputBox.value = rebaseCommit.message;
793
}
794
795
const shouldUpdateContext = !!this._rebaseCommit !== !!rebaseCommit;
796
this._rebaseCommit = rebaseCommit;
797
798
if (shouldUpdateContext) {
799
commands.executeCommand('setContext', 'gitRebaseInProgress', !!this._rebaseCommit);
800
}
801
}
802
803
get rebaseCommit(): Commit | undefined {
804
return this._rebaseCommit;
805
}
806
807
private _mergeInProgress: boolean = false;
808
809
set mergeInProgress(value: boolean) {
810
if (this._mergeInProgress === value) {
811
return;
812
}
813
814
this._mergeInProgress = value;
815
commands.executeCommand('setContext', 'gitMergeInProgress', value);
816
}
817
818
get mergeInProgress() {
819
return this._mergeInProgress;
820
}
821
822
private _cherryPickInProgress: boolean = false;
823
824
set cherryPickInProgress(value: boolean) {
825
if (this._cherryPickInProgress === value) {
826
return;
827
}
828
829
this._cherryPickInProgress = value;
830
commands.executeCommand('setContext', 'gitCherryPickInProgress', value);
831
}
832
833
get cherryPickInProgress() {
834
return this._cherryPickInProgress;
835
}
836
837
private _isWorktreeMigrating: boolean = false;
838
get isWorktreeMigrating(): boolean { return this._isWorktreeMigrating; }
839
set isWorktreeMigrating(value: boolean) { this._isWorktreeMigrating = value; }
840
841
private readonly _operations: OperationManager;
842
get operations(): OperationManager { return this._operations; }
843
844
private _state = RepositoryState.Idle;
845
get state(): RepositoryState { return this._state; }
846
set state(state: RepositoryState) {
847
this._state = state;
848
this._onDidChangeState.fire(state);
849
850
this._HEAD = undefined;
851
this._remotes = [];
852
this.mergeGroup.resourceStates = [];
853
this.indexGroup.resourceStates = [];
854
this.workingTreeGroup.resourceStates = [];
855
this.untrackedGroup.resourceStates = [];
856
this._sourceControl.count = 0;
857
}
858
859
get root(): string {
860
return this.repository.root;
861
}
862
863
get rootRealPath(): string | undefined {
864
return this.repository.rootRealPath;
865
}
866
867
get dotGit(): { path: string; commonPath?: string } {
868
return this.repository.dotGit;
869
}
870
871
get kind(): 'repository' | 'submodule' | 'worktree' {
872
return this.repository.kind;
873
}
874
875
private _historyProvider: GitHistoryProvider;
876
get historyProvider(): GitHistoryProvider { return this._historyProvider; }
877
878
private isRepositoryHuge: false | { limit: number } = false;
879
private didWarnAboutLimit = false;
880
881
private unpublishedCommits: Set<string> | undefined = undefined;
882
private branchProtection = new Map<string, BranchProtectionMatcher[]>();
883
private commitCommandCenter: CommitCommandsCenter;
884
private resourceCommandResolver = new ResourceCommandResolver(this);
885
private updateModelStateCancellationTokenSource: CancellationTokenSource | undefined;
886
private disposables: Disposable[] = [];
887
888
constructor(
889
private readonly repository: BaseRepository,
890
private readonly repositoryResolver: IRepositoryResolver,
891
private pushErrorHandlerRegistry: IPushErrorHandlerRegistry,
892
remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry,
893
postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry,
894
private readonly branchProtectionProviderRegistry: IBranchProtectionProviderRegistry,
895
historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailsProviderRegistry,
896
globalState: Memento,
897
private readonly logger: LogOutputChannel,
898
private telemetryReporter: TelemetryReporter
899
) {
900
this._operations = new OperationManager(this.logger);
901
902
const repositoryWatcher = workspace.createFileSystemWatcher(new RelativePattern(Uri.file(repository.root), '**'));
903
this.disposables.push(repositoryWatcher);
904
905
const onRepositoryFileChange = anyEvent(repositoryWatcher.onDidChange, repositoryWatcher.onDidCreate, repositoryWatcher.onDidDelete);
906
const onRepositoryWorkingTreeFileChange = filterEvent(onRepositoryFileChange, uri => !/\.git($|\\|\/)/.test(relativePath(repository.root, uri.fsPath)));
907
908
let onRepositoryDotGitFileChange: Event<Uri>;
909
910
try {
911
const dotGitFileWatcher = new DotGitWatcher(this, logger);
912
onRepositoryDotGitFileChange = dotGitFileWatcher.event;
913
this.disposables.push(dotGitFileWatcher);
914
} catch (err) {
915
logger.error(`Failed to watch path:'${this.dotGit.path}' or commonPath:'${this.dotGit.commonPath}', reverting to legacy API file watched. Some events might be lost.\n${err.stack || err}`);
916
917
onRepositoryDotGitFileChange = filterEvent(onRepositoryFileChange, uri => /\.git($|\\|\/)/.test(uri.path));
918
}
919
920
// FS changes should trigger `git status`:
921
// - any change inside the repository working tree
922
// - any change whithin the first level of the `.git` folder, except the folder itself and `index.lock`
923
const onFileChange = anyEvent(onRepositoryWorkingTreeFileChange, onRepositoryDotGitFileChange);
924
onFileChange(this.onFileChange, this, this.disposables);
925
926
// Relevate repository changes should trigger virtual document change events
927
onRepositoryDotGitFileChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables);
928
929
this.disposables.push(new FileEventLogger(onRepositoryWorkingTreeFileChange, onRepositoryDotGitFileChange, logger));
930
931
// Parent source control
932
const parentRoot = repository.kind === 'submodule'
933
? repository.dotGit.superProjectPath
934
: repository.kind === 'worktree' && repository.dotGit.commonPath
935
? path.dirname(repository.dotGit.commonPath)
936
: undefined;
937
const parent = this.repositoryResolver.getRepository(parentRoot)?.sourceControl;
938
939
// Icon
940
const icon = repository.kind === 'submodule'
941
? new ThemeIcon('archive')
942
: repository.kind === 'worktree'
943
? new ThemeIcon('list-tree')
944
: new ThemeIcon('repo');
945
946
const root = Uri.file(repository.root);
947
this._sourceControl = scm.createSourceControl('git', 'Git', root, icon, parent);
948
this._sourceControl.contextValue = repository.kind;
949
950
this._sourceControl.quickDiffProvider = this;
951
this._sourceControl.secondaryQuickDiffProvider = new StagedResourceQuickDiffProvider(this, logger);
952
953
this._historyProvider = new GitHistoryProvider(historyItemDetailProviderRegistry, this, logger);
954
this._sourceControl.historyProvider = this._historyProvider;
955
this.disposables.push(this._historyProvider);
956
957
this._sourceControl.acceptInputCommand = { command: 'git.commit', title: l10n.t('Commit'), arguments: [this._sourceControl] };
958
this._sourceControl.inputBox.validateInput = this.validateInput.bind(this);
959
960
this.disposables.push(this._sourceControl);
961
962
this.updateInputBoxPlaceholder();
963
this.disposables.push(this.onDidRunGitStatus(() => this.updateInputBoxPlaceholder()));
964
965
this._mergeGroup = this._sourceControl.createResourceGroup('merge', l10n.t('Merge Changes'));
966
this._indexGroup = this._sourceControl.createResourceGroup('index', l10n.t('Staged Changes'), { multiDiffEditorEnableViewChanges: true });
967
this._workingTreeGroup = this._sourceControl.createResourceGroup('workingTree', l10n.t('Changes'), { multiDiffEditorEnableViewChanges: true });
968
this._untrackedGroup = this._sourceControl.createResourceGroup('untracked', l10n.t('Untracked Changes'), { multiDiffEditorEnableViewChanges: true });
969
970
const updateIndexGroupVisibility = () => {
971
const config = workspace.getConfiguration('git', root);
972
this.indexGroup.hideWhenEmpty = !config.get<boolean>('alwaysShowStagedChangesResourceGroup');
973
};
974
975
const onConfigListener = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.alwaysShowStagedChangesResourceGroup', root));
976
onConfigListener(updateIndexGroupVisibility, this, this.disposables);
977
updateIndexGroupVisibility();
978
979
workspace.onDidChangeConfiguration(e => {
980
if (e.affectsConfiguration('git.mergeEditor')) {
981
this.mergeGroup.resourceStates = this.mergeGroup.resourceStates.map(r => r.clone());
982
}
983
}, undefined, this.disposables);
984
985
filterEvent(workspace.onDidChangeConfiguration, e =>
986
e.affectsConfiguration('git.branchSortOrder', root)
987
|| e.affectsConfiguration('git.untrackedChanges', root)
988
|| e.affectsConfiguration('git.ignoreSubmodules', root)
989
|| e.affectsConfiguration('git.openDiffOnClick', root)
990
|| e.affectsConfiguration('git.showActionButton', root)
991
|| e.affectsConfiguration('git.similarityThreshold', root)
992
)(() => this.updateModelState(), this, this.disposables);
993
994
const updateInputBoxVisibility = () => {
995
const config = workspace.getConfiguration('git', root);
996
this._sourceControl.inputBox.visible = config.get<boolean>('showCommitInput', true);
997
};
998
999
const onConfigListenerForInputBoxVisibility = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.showCommitInput', root));
1000
onConfigListenerForInputBoxVisibility(updateInputBoxVisibility, this, this.disposables);
1001
updateInputBoxVisibility();
1002
1003
this.mergeGroup.hideWhenEmpty = true;
1004
this.untrackedGroup.hideWhenEmpty = true;
1005
1006
this.disposables.push(this.mergeGroup);
1007
this.disposables.push(this.indexGroup);
1008
this.disposables.push(this.workingTreeGroup);
1009
this.disposables.push(this.untrackedGroup);
1010
1011
// Don't allow auto-fetch in untrusted workspaces
1012
if (workspace.isTrusted) {
1013
this.disposables.push(new AutoFetcher(this, globalState));
1014
} else {
1015
const trustDisposable = workspace.onDidGrantWorkspaceTrust(() => {
1016
trustDisposable.dispose();
1017
this.disposables.push(new AutoFetcher(this, globalState));
1018
});
1019
this.disposables.push(trustDisposable);
1020
}
1021
1022
// https://github.com/microsoft/vscode/issues/39039
1023
const onSuccessfulPush = filterEvent(this.onDidRunOperation, e => e.operation.kind === OperationKind.Push && !e.error);
1024
onSuccessfulPush(() => {
1025
const gitConfig = workspace.getConfiguration('git');
1026
1027
if (gitConfig.get<boolean>('showPushSuccessNotification')) {
1028
window.showInformationMessage(l10n.t('Successfully pushed.'));
1029
}
1030
}, null, this.disposables);
1031
1032
// Default branch protection provider
1033
const onBranchProtectionProviderChanged = filterEvent(this.branchProtectionProviderRegistry.onDidChangeBranchProtectionProviders, e => pathEquals(e.fsPath, root.fsPath));
1034
this.disposables.push(onBranchProtectionProviderChanged(root => this.updateBranchProtectionMatchers(root)));
1035
this.disposables.push(this.branchProtectionProviderRegistry.registerBranchProtectionProvider(root, new GitBranchProtectionProvider(root)));
1036
1037
const statusBar = new StatusBarCommands(this, remoteSourcePublisherRegistry);
1038
this.disposables.push(statusBar);
1039
statusBar.onDidChange(() => this._sourceControl.statusBarCommands = statusBar.commands, null, this.disposables);
1040
this._sourceControl.statusBarCommands = statusBar.commands;
1041
1042
this.commitCommandCenter = new CommitCommandsCenter(globalState, this, postCommitCommandsProviderRegistry);
1043
this.disposables.push(this.commitCommandCenter);
1044
1045
const actionButton = new ActionButton(this, this.commitCommandCenter, this.logger);
1046
this.disposables.push(actionButton);
1047
actionButton.onDidChange(() => this._sourceControl.actionButton = actionButton.button, this, this.disposables);
1048
this._sourceControl.actionButton = actionButton.button;
1049
1050
const progressManager = new ProgressManager(this);
1051
this.disposables.push(progressManager);
1052
1053
const onDidChangeCountBadge = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.countBadge', root));
1054
onDidChangeCountBadge(this.setCountBadge, this, this.disposables);
1055
this.setCountBadge();
1056
}
1057
1058
validateInput(text: string, _: number): SourceControlInputBoxValidation | undefined {
1059
if (this.isRepositoryHuge) {
1060
return {
1061
message: l10n.t('Too many changes were detected. Only the first {0} changes will be shown below.', this.isRepositoryHuge.limit),
1062
type: SourceControlInputBoxValidationType.Warning
1063
};
1064
}
1065
1066
if (this.rebaseCommit) {
1067
if (this.rebaseCommit.message !== text) {
1068
return {
1069
message: l10n.t('It\'s not possible to change the commit message in the middle of a rebase. Please complete the rebase operation and use interactive rebase instead.'),
1070
type: SourceControlInputBoxValidationType.Warning
1071
};
1072
}
1073
}
1074
1075
return undefined;
1076
}
1077
1078
/**
1079
* Quick diff label
1080
*/
1081
get label(): string {
1082
return l10n.t('Git Local Changes (Working Tree)');
1083
}
1084
1085
async provideOriginalResource(uri: Uri): Promise<Uri | undefined> {
1086
this.logger.trace(`[Repository][provideOriginalResource] Resource: ${uri.toString()}`);
1087
1088
if (uri.scheme !== 'file') {
1089
this.logger.trace(`[Repository][provideOriginalResource] Resource is not a file: ${uri.scheme}`);
1090
return undefined;
1091
}
1092
1093
// Ignore symbolic links
1094
const stat = await workspace.fs.stat(uri);
1095
if ((stat.type & FileType.SymbolicLink) !== 0) {
1096
this.logger.trace(`[Repository][provideOriginalResource] Resource is a symbolic link: ${uri.toString()}`);
1097
return undefined;
1098
}
1099
1100
// Ignore path that is not inside the current repository
1101
if (this.repositoryResolver.getRepository(uri) !== this) {
1102
this.logger.trace(`[Repository][provideOriginalResource] Resource is not part of the repository: ${uri.toString()}`);
1103
return undefined;
1104
}
1105
1106
// Ignore path that is inside a merge group
1107
if (this.mergeGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) {
1108
this.logger.trace(`[Repository][provideOriginalResource] Resource is part of a merge group: ${uri.toString()}`);
1109
return undefined;
1110
}
1111
1112
// Ignore path that is untracked
1113
if (this.untrackedGroup.resourceStates.some(r => pathEquals(r.resourceUri.path, uri.path)) ||
1114
this.workingTreeGroup.resourceStates.some(r => pathEquals(r.resourceUri.path, uri.path) && r.type === Status.UNTRACKED)) {
1115
this.logger.trace(`[Repository][provideOriginalResource] Resource is untracked: ${uri.toString()}`);
1116
return undefined;
1117
}
1118
1119
const activeTabInput = window.tabGroups.activeTabGroup.activeTab?.input;
1120
1121
// Ignore file that is on the right-hand side of a diff editor
1122
if (activeTabInput instanceof TabInputTextDiff && pathEquals(activeTabInput.modified.fsPath, uri.fsPath)) {
1123
this.logger.trace(`[Repository][provideOriginalResource] Resource is on the right-hand side of a diff editor: ${uri.toString()}`);
1124
return undefined;
1125
}
1126
1127
// Ignore file that is on the right -hand side of a multi-file diff editor
1128
if (activeTabInput instanceof TabInputTextMultiDiff && activeTabInput.textDiffs.some(diff => pathEquals(diff.modified.fsPath, uri.fsPath))) {
1129
this.logger.trace(`[Repository][provideOriginalResource] Resource is on the right-hand side of a multi-file diff editor: ${uri.toString()}`);
1130
return undefined;
1131
}
1132
1133
const originalResource = toGitUri(uri, '', { replaceFileExtension: true });
1134
this.logger.trace(`[Repository][provideOriginalResource] Original resource: ${originalResource.toString()}`);
1135
1136
return originalResource;
1137
}
1138
1139
async getInputTemplate(): Promise<string> {
1140
const commitMessage = (await Promise.all([this.repository.getMergeMessage(), this.repository.getSquashMessage()])).find(msg => !!msg);
1141
1142
if (commitMessage) {
1143
return commitMessage;
1144
}
1145
1146
return await this.repository.getCommitTemplate();
1147
}
1148
1149
getConfigs(): Promise<{ key: string; value: string }[]> {
1150
return this.run(Operation.Config(true), () => this.repository.getConfigs('local'));
1151
}
1152
1153
getConfig(key: string): Promise<string> {
1154
return this.run(Operation.Config(true), () => this.repository.config('get', 'local', key));
1155
}
1156
1157
getGlobalConfig(key: string): Promise<string> {
1158
return this.run(Operation.Config(true), () => this.repository.config('get', 'global', key));
1159
}
1160
1161
setConfig(key: string, value: string): Promise<string> {
1162
return this.run(Operation.Config(false), () => this.repository.config('add', 'local', key, value));
1163
}
1164
1165
unsetConfig(key: string): Promise<string> {
1166
return this.run(Operation.Config(false), () => this.repository.config('unset', 'local', key));
1167
}
1168
1169
log(options?: LogOptions & { silent?: boolean }, cancellationToken?: CancellationToken): Promise<Commit[]> {
1170
const showProgress = !options || options.silent !== true;
1171
return this.run(Operation.Log(showProgress), () => this.repository.log(options, cancellationToken));
1172
}
1173
1174
logFile(uri: Uri, options?: LogFileOptions, cancellationToken?: CancellationToken): Promise<Commit[]> {
1175
// TODO: This probably needs per-uri granularity
1176
return this.run(Operation.LogFile, () => this.repository.logFile(uri, options, cancellationToken));
1177
}
1178
1179
@throttle
1180
async status(): Promise<void> {
1181
await this.run(Operation.Status);
1182
}
1183
1184
@throttle
1185
async refresh(): Promise<void> {
1186
await this.run(Operation.Refresh);
1187
}
1188
1189
diff(cached?: boolean): Promise<string> {
1190
return this.run(Operation.Diff, () => this.repository.diff(cached));
1191
}
1192
1193
diffWithHEAD(): Promise<Change[]>;
1194
diffWithHEAD(path: string): Promise<string>;
1195
diffWithHEAD(path?: string | undefined): Promise<string | Change[]>;
1196
diffWithHEAD(path?: string | undefined): Promise<string | Change[]> {
1197
return this.run(Operation.Diff, () => this.repository.diffWithHEAD(path));
1198
}
1199
1200
diffWith(ref: string): Promise<Change[]>;
1201
diffWith(ref: string, path: string): Promise<string>;
1202
diffWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
1203
diffWith(ref: string, path?: string): Promise<string | Change[]> {
1204
return this.run(Operation.Diff, () => this.repository.diffWith(ref, path));
1205
}
1206
1207
diffIndexWithHEAD(): Promise<Change[]>;
1208
diffIndexWithHEAD(path: string): Promise<string>;
1209
diffIndexWithHEAD(path?: string | undefined): Promise<string | Change[]>;
1210
diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
1211
return this.run(Operation.Diff, () => this.repository.diffIndexWithHEAD(path));
1212
}
1213
1214
diffIndexWith(ref: string): Promise<Change[]>;
1215
diffIndexWith(ref: string, path: string): Promise<string>;
1216
diffIndexWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
1217
diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
1218
return this.run(Operation.Diff, () => this.repository.diffIndexWith(ref, path));
1219
}
1220
1221
diffBlobs(object1: string, object2: string): Promise<string> {
1222
return this.run(Operation.Diff, () => this.repository.diffBlobs(object1, object2));
1223
}
1224
1225
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
1226
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
1227
diffBetween(ref1: string, ref2: string, path?: string | undefined): Promise<string | Change[]>;
1228
diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
1229
return this.run(Operation.Diff, () => this.repository.diffBetween(ref1, ref2, path));
1230
}
1231
1232
diffBetweenShortStat(ref1: string, ref2: string): Promise<{ files: number; insertions: number; deletions: number }> {
1233
return this.run(Operation.Diff, () => this.repository.diffBetweenShortStat(ref1, ref2));
1234
}
1235
1236
diffTrees(treeish1: string, treeish2?: string): Promise<Change[]> {
1237
const scopedConfig = workspace.getConfiguration('git', Uri.file(this.root));
1238
const similarityThreshold = scopedConfig.get<number>('similarityThreshold', 50);
1239
1240
return this.run(Operation.Diff, () => this.repository.diffTrees(treeish1, treeish2, { similarityThreshold }));
1241
}
1242
1243
getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise<string | undefined> {
1244
return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2, ...refs));
1245
}
1246
1247
async hashObject(data: string): Promise<string> {
1248
return this.run(Operation.HashObject, () => this.repository.hashObject(data));
1249
}
1250
1251
async add(resources: Uri[], opts?: { update?: boolean }): Promise<void> {
1252
await this.run(
1253
Operation.Add(!this.optimisticUpdateEnabled()),
1254
async () => {
1255
await this.repository.add(resources.map(r => r.fsPath), opts);
1256
this.closeDiffEditors([], [...resources.map(r => r.fsPath)]);
1257
1258
// Accept working set changes across all chat sessions
1259
commands.executeCommand('_chat.editSessions.accept', resources);
1260
},
1261
() => {
1262
const resourcePaths = resources.map(r => r.fsPath);
1263
const indexGroupResourcePaths = this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath);
1264
1265
// Collect added resources
1266
const addedResourceStates: Resource[] = [];
1267
for (const resource of [...this.mergeGroup.resourceStates, ...this.untrackedGroup.resourceStates, ...this.workingTreeGroup.resourceStates]) {
1268
if (resourcePaths.includes(resource.resourceUri.fsPath) && !indexGroupResourcePaths.includes(resource.resourceUri.fsPath)) {
1269
addedResourceStates.push(resource.clone(ResourceGroupType.Index));
1270
}
1271
}
1272
1273
// Add new resource(s) to index group
1274
const indexGroup = [...this.indexGroup.resourceStates, ...addedResourceStates];
1275
1276
// Remove resource(s) from merge group
1277
const mergeGroup = this.mergeGroup.resourceStates
1278
.filter(r => !resourcePaths.includes(r.resourceUri.fsPath));
1279
1280
// Remove resource(s) from working group
1281
const workingTreeGroup = this.workingTreeGroup.resourceStates
1282
.filter(r => !resourcePaths.includes(r.resourceUri.fsPath));
1283
1284
// Remove resource(s) from untracked group
1285
const untrackedGroup = this.untrackedGroup.resourceStates
1286
.filter(r => !resourcePaths.includes(r.resourceUri.fsPath));
1287
1288
return { indexGroup, mergeGroup, workingTreeGroup, untrackedGroup };
1289
});
1290
}
1291
1292
async rm(resources: Uri[]): Promise<void> {
1293
await this.run(Operation.Remove, () => this.repository.rm(resources.map(r => r.fsPath)));
1294
}
1295
1296
async stage(resource: Uri, contents: string, encoding: string): Promise<void> {
1297
await this.run(Operation.Stage, async () => {
1298
const data = await workspace.encode(contents, { encoding });
1299
await this.repository.stage(resource.fsPath, data);
1300
1301
this._onDidChangeOriginalResource.fire(resource);
1302
this.closeDiffEditors([], [...resource.fsPath]);
1303
});
1304
}
1305
1306
async revert(resources: Uri[]): Promise<void> {
1307
await this.run(
1308
Operation.RevertFiles(!this.optimisticUpdateEnabled()),
1309
async () => {
1310
await this.repository.revert('HEAD', resources.map(r => r.fsPath));
1311
for (const resource of resources) {
1312
this._onDidChangeOriginalResource.fire(resource);
1313
}
1314
this.closeDiffEditors([...resources.length !== 0 ?
1315
resources.map(r => r.fsPath) :
1316
this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath)], []);
1317
},
1318
() => {
1319
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
1320
const untrackedChanges = config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
1321
const untrackedChangesResourceGroupType = untrackedChanges === 'mixed' ? ResourceGroupType.WorkingTree : ResourceGroupType.Untracked;
1322
1323
const resourcePaths = resources.length === 0 ?
1324
this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath) : resources.map(r => r.fsPath);
1325
1326
// Collect removed resources
1327
const trackedResources: Resource[] = [];
1328
const untrackedResources: Resource[] = [];
1329
for (const resource of this.indexGroup.resourceStates) {
1330
if (resourcePaths.includes(resource.resourceUri.fsPath)) {
1331
if (resource.type === Status.INDEX_ADDED) {
1332
untrackedResources.push(resource.clone(untrackedChangesResourceGroupType));
1333
} else {
1334
trackedResources.push(resource.clone(ResourceGroupType.WorkingTree));
1335
}
1336
}
1337
}
1338
1339
// Remove resource(s) from index group
1340
const indexGroup = this.indexGroup.resourceStates
1341
.filter(r => !resourcePaths.includes(r.resourceUri.fsPath));
1342
1343
// Add resource(s) to working group
1344
const workingTreeGroup = untrackedChanges === 'mixed' ?
1345
[...this.workingTreeGroup.resourceStates, ...trackedResources, ...untrackedResources] :
1346
[...this.workingTreeGroup.resourceStates, ...trackedResources];
1347
1348
// Add resource(s) to untracked group
1349
const untrackedGroup = untrackedChanges === 'separate' ?
1350
[...this.untrackedGroup.resourceStates, ...untrackedResources] : undefined;
1351
1352
return { indexGroup, workingTreeGroup, untrackedGroup };
1353
});
1354
}
1355
1356
async commit(message: string | undefined, opts: CommitOptions = Object.create(null)): Promise<void> {
1357
const indexResources = [...this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath)];
1358
const workingGroupResources = opts.all && opts.all !== 'tracked' ?
1359
[...this.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath)] : [];
1360
1361
if (this.rebaseCommit) {
1362
await this.run(
1363
Operation.RebaseContinue,
1364
async () => {
1365
if (opts.all) {
1366
const addOpts = opts.all === 'tracked' ? { update: true } : {};
1367
await this.repository.add([], addOpts);
1368
}
1369
1370
await this.repository.rebaseContinue();
1371
await this.commitOperationCleanup(message, indexResources, workingGroupResources);
1372
},
1373
() => this.commitOperationGetOptimisticResourceGroups(opts));
1374
} else {
1375
// Set post-commit command to render the correct action button
1376
this.commitCommandCenter.postCommitCommand = opts.postCommitCommand;
1377
1378
await this.run(
1379
Operation.Commit,
1380
async () => {
1381
if (opts.all) {
1382
const addOpts = opts.all === 'tracked' ? { update: true } : {};
1383
await this.repository.add([], addOpts);
1384
}
1385
1386
delete opts.all;
1387
1388
if (opts.requireUserConfig === undefined || opts.requireUserConfig === null) {
1389
const config = workspace.getConfiguration('git', Uri.file(this.root));
1390
opts.requireUserConfig = config.get<boolean>('requireGitUserConfig');
1391
}
1392
1393
await this.repository.commit(message, opts);
1394
await this.commitOperationCleanup(message, indexResources, workingGroupResources);
1395
},
1396
() => this.commitOperationGetOptimisticResourceGroups(opts));
1397
1398
// Execute post-commit command
1399
await this.run(Operation.PostCommitCommand, async () => {
1400
await this.commitCommandCenter.executePostCommitCommand(opts.postCommitCommand);
1401
});
1402
}
1403
}
1404
1405
private async commitOperationCleanup(message: string | undefined, indexResources: string[], workingGroupResources: string[]) {
1406
if (message) {
1407
this.inputBox.value = await this.getInputTemplate();
1408
}
1409
this.closeDiffEditors(indexResources, workingGroupResources);
1410
1411
// Accept working set changes across all chat sessions
1412
const resources = indexResources.length !== 0
1413
? indexResources.map(r => Uri.file(r))
1414
: workingGroupResources.map(r => Uri.file(r));
1415
commands.executeCommand('_chat.editSessions.accept', resources);
1416
}
1417
1418
private commitOperationGetOptimisticResourceGroups(opts: CommitOptions): GitResourceGroups {
1419
let untrackedGroup: Resource[] | undefined = undefined,
1420
workingTreeGroup: Resource[] | undefined = undefined;
1421
1422
if (opts.all === 'tracked') {
1423
workingTreeGroup = this.workingTreeGroup.resourceStates
1424
.filter(r => r.type === Status.UNTRACKED);
1425
} else if (opts.all) {
1426
untrackedGroup = workingTreeGroup = [];
1427
}
1428
1429
return { indexGroup: [], mergeGroup: [], untrackedGroup, workingTreeGroup };
1430
}
1431
1432
async clean(resources: Uri[]): Promise<void> {
1433
const config = workspace.getConfiguration('git');
1434
const discardUntrackedChangesToTrash = config.get<boolean>('discardUntrackedChangesToTrash', true) && !isRemote && !isLinuxSnap;
1435
1436
await this.run(
1437
Operation.Clean(!this.optimisticUpdateEnabled()),
1438
async () => {
1439
const toClean: string[] = [];
1440
const toCheckout: string[] = [];
1441
const submodulesToUpdate: string[] = [];
1442
const resourceStates = [...this.workingTreeGroup.resourceStates, ...this.untrackedGroup.resourceStates];
1443
1444
resources.forEach(r => {
1445
const fsPath = r.fsPath;
1446
1447
for (const submodule of this.submodules) {
1448
if (path.join(this.root, submodule.path) === fsPath) {
1449
submodulesToUpdate.push(fsPath);
1450
return;
1451
}
1452
}
1453
1454
const raw = r.toString();
1455
const scmResource = find(resourceStates, sr => sr.resourceUri.toString() === raw);
1456
1457
if (!scmResource) {
1458
return;
1459
}
1460
1461
switch (scmResource.type) {
1462
case Status.UNTRACKED:
1463
case Status.IGNORED:
1464
toClean.push(fsPath);
1465
break;
1466
1467
default:
1468
toCheckout.push(fsPath);
1469
break;
1470
}
1471
});
1472
1473
if (toClean.length > 0) {
1474
if (discardUntrackedChangesToTrash) {
1475
try {
1476
// Attempt to move the first resource to the recycle bin/trash to check
1477
// if it is supported. If it fails, we show a confirmation dialog and
1478
// fall back to deletion.
1479
await workspace.fs.delete(Uri.file(toClean[0]), { useTrash: true });
1480
1481
const limiter = new Limiter<void>(5);
1482
await Promise.all(toClean.slice(1).map(fsPath => limiter.queue(
1483
async () => await workspace.fs.delete(Uri.file(fsPath), { useTrash: true }))));
1484
} catch {
1485
const message = isWindows
1486
? l10n.t('Failed to delete using the Recycle Bin. Do you want to permanently delete instead?')
1487
: l10n.t('Failed to delete using the Trash. Do you want to permanently delete instead?');
1488
const primaryAction = toClean.length === 1
1489
? l10n.t('Delete File')
1490
: l10n.t('Delete All {0} Files', resources.length);
1491
1492
const result = await window.showWarningMessage(message, { modal: true }, primaryAction);
1493
if (result === primaryAction) {
1494
// Delete permanently
1495
await this.repository.clean(toClean);
1496
}
1497
}
1498
} else {
1499
await this.repository.clean(toClean);
1500
}
1501
}
1502
1503
if (toCheckout.length > 0) {
1504
try {
1505
await this.repository.checkout('', toCheckout);
1506
} catch (err) {
1507
if (err.gitErrorCode !== GitErrorCodes.BranchNotYetBorn) {
1508
throw err;
1509
}
1510
}
1511
}
1512
1513
if (submodulesToUpdate.length > 0) {
1514
await this.repository.updateSubmodules(submodulesToUpdate);
1515
}
1516
1517
this.closeDiffEditors([], [...toClean, ...toCheckout]);
1518
},
1519
() => {
1520
const resourcePaths = resources.map(r => r.fsPath);
1521
1522
// Remove resource(s) from working group
1523
const workingTreeGroup = this.workingTreeGroup.resourceStates
1524
.filter(r => !resourcePaths.includes(r.resourceUri.fsPath));
1525
1526
// Remove resource(s) from untracked group
1527
const untrackedGroup = this.untrackedGroup.resourceStates
1528
.filter(r => !resourcePaths.includes(r.resourceUri.fsPath));
1529
1530
return { workingTreeGroup, untrackedGroup };
1531
});
1532
}
1533
1534
closeDiffEditors(indexResources: string[] | undefined, workingTreeResources: string[] | undefined, ignoreSetting = false): void {
1535
const config = workspace.getConfiguration('git', Uri.file(this.root));
1536
if (!config.get<boolean>('closeDiffOnOperation', false) && !ignoreSetting) { return; }
1537
1538
function checkTabShouldClose(input: TabInputTextDiff | TabInputNotebookDiff) {
1539
if (input.modified.scheme === 'git' && (indexResources === undefined || indexResources.some(r => pathEquals(r, input.modified.fsPath)))) {
1540
// Index
1541
return true;
1542
}
1543
if (input.modified.scheme === 'file' && input.original.scheme === 'git' && (workingTreeResources === undefined || workingTreeResources.some(r => pathEquals(r, input.modified.fsPath)))) {
1544
// Working Tree
1545
return true;
1546
}
1547
return false;
1548
}
1549
1550
const diffEditorTabsToClose = window.tabGroups.all
1551
.flatMap(g => g.tabs)
1552
.filter(({ input }) => {
1553
if (input instanceof TabInputTextDiff || input instanceof TabInputNotebookDiff) {
1554
return checkTabShouldClose(input);
1555
} else if (input instanceof TabInputTextMultiDiff) {
1556
return input.textDiffs.every(checkTabShouldClose);
1557
}
1558
return false;
1559
});
1560
1561
// Close editors
1562
window.tabGroups.close(diffEditorTabsToClose, true);
1563
}
1564
1565
async branch(name: string, _checkout: boolean, _ref?: string): Promise<void> {
1566
await this.run(Operation.Branch, () => this.repository.branch(name, _checkout, _ref));
1567
}
1568
1569
async deleteBranch(name: string, force?: boolean): Promise<void> {
1570
return this.run(Operation.DeleteBranch, async () => {
1571
await this.repository.deleteBranch(name, force);
1572
await this.repository.config('unset', 'local', `branch.${name}.vscode-merge-base`);
1573
});
1574
}
1575
1576
async renameBranch(name: string): Promise<void> {
1577
await this.run(Operation.RenameBranch, () => this.repository.renameBranch(name));
1578
}
1579
1580
@throttle
1581
async fastForwardBranch(name: string): Promise<void> {
1582
// Get branch details
1583
const branch = await this.getBranch(name);
1584
if (!branch.upstream?.remote || !branch.upstream?.name || !branch.name) {
1585
return;
1586
}
1587
1588
try {
1589
// Fast-forward the branch if possible
1590
const options = { remote: branch.upstream.remote, ref: `${branch.upstream.name}:${branch.name}` };
1591
await this.run(Operation.Fetch(true), async () => this.repository.fetch(options));
1592
} catch (err) {
1593
if (err.gitErrorCode === GitErrorCodes.BranchFastForwardRejected) {
1594
return;
1595
}
1596
1597
throw err;
1598
}
1599
}
1600
1601
async cherryPick(commitHash: string): Promise<void> {
1602
await this.run(Operation.CherryPick, () => this.repository.cherryPick(commitHash));
1603
}
1604
1605
async cherryPickAbort(): Promise<void> {
1606
await this.run(Operation.CherryPick, () => this.repository.cherryPickAbort());
1607
}
1608
1609
async move(from: string, to: string): Promise<void> {
1610
await this.run(Operation.Move, () => this.repository.move(from, to));
1611
}
1612
1613
async getBranch(name: string): Promise<Branch> {
1614
return await this.run(Operation.GetBranch, () => this.repository.getBranch(name));
1615
}
1616
1617
async getBranches(query: BranchQuery = {}, cancellationToken?: CancellationToken): Promise<Ref[]> {
1618
return await this.run(Operation.GetBranches, async () => {
1619
const refs = await this.getRefs(query, cancellationToken);
1620
return refs.filter(value => value.type === RefType.Head || (value.type === RefType.RemoteHead && query.remote));
1621
});
1622
}
1623
1624
@sequentialize
1625
async getBranchBase(ref: string): Promise<Branch | undefined> {
1626
const branch = await this.getBranch(ref);
1627
1628
// Git config
1629
const mergeBaseConfigKey = `branch.${branch.name}.vscode-merge-base`;
1630
1631
try {
1632
const mergeBase = await this.getConfig(mergeBaseConfigKey);
1633
const branchFromConfig = mergeBase !== '' ? await this.getBranch(mergeBase) : undefined;
1634
1635
// There was a brief period of time when we would consider local branches as a valid
1636
// merge base. Since then we have fixed the issue and only remote branches can be used
1637
// as a merge base so we are adding an additional check.
1638
if (branchFromConfig && branchFromConfig.remote) {
1639
return branchFromConfig;
1640
}
1641
} catch (err) { }
1642
1643
// Reflog
1644
const branchFromReflog = await this.getBranchBaseFromReflog(ref);
1645
1646
let branchFromReflogUpstream: Branch | undefined = undefined;
1647
1648
if (branchFromReflog?.type === RefType.RemoteHead) {
1649
branchFromReflogUpstream = branchFromReflog;
1650
} else if (branchFromReflog?.type === RefType.Head) {
1651
branchFromReflogUpstream = await this.getUpstreamBranch(branchFromReflog);
1652
}
1653
1654
if (branchFromReflogUpstream) {
1655
await this.setConfig(mergeBaseConfigKey, `${branchFromReflogUpstream.remote}/${branchFromReflogUpstream.name}`);
1656
return branchFromReflogUpstream;
1657
}
1658
1659
// Default branch
1660
const defaultBranch = await this.getDefaultBranch();
1661
if (defaultBranch) {
1662
await this.setConfig(mergeBaseConfigKey, `${defaultBranch.remote}/${defaultBranch.name}`);
1663
return defaultBranch;
1664
}
1665
1666
return undefined;
1667
}
1668
1669
private async getBranchBaseFromReflog(ref: string): Promise<Branch | undefined> {
1670
try {
1671
const reflogEntries = await this.repository.reflog(ref, 'branch: Created from *.');
1672
if (reflogEntries.length !== 1) {
1673
return undefined;
1674
}
1675
1676
// Branch created from an explicit branch
1677
const match = reflogEntries[0].match(/branch: Created from (?<name>.*)$/);
1678
if (match && match.length === 2 && match[1] !== 'HEAD') {
1679
return await this.getBranch(match[1]);
1680
}
1681
1682
// Branch created from HEAD
1683
const headReflogEntries = await this.repository.reflog('HEAD', `checkout: moving from .* to ${ref.replace('refs/heads/', '')}`);
1684
if (headReflogEntries.length === 0) {
1685
return undefined;
1686
}
1687
1688
const match2 = headReflogEntries[headReflogEntries.length - 1].match(/checkout: moving from ([^\s]+)\s/);
1689
if (match2 && match2.length === 2) {
1690
return await this.getBranch(match2[1]);
1691
}
1692
1693
}
1694
catch (err) { }
1695
1696
return undefined;
1697
}
1698
1699
private async getDefaultBranch(): Promise<Branch | undefined> {
1700
const defaultRemote = this.getDefaultRemote();
1701
if (!defaultRemote) {
1702
return undefined;
1703
}
1704
1705
try {
1706
const defaultBranch = await this.repository.getDefaultBranch(defaultRemote.name);
1707
return defaultBranch;
1708
}
1709
catch (err) {
1710
this.logger.warn(`[Repository][getDefaultBranch] Failed to get default branch details: ${err.message}.`);
1711
return undefined;
1712
}
1713
}
1714
1715
private async getUpstreamBranch(branch: Branch): Promise<Branch | undefined> {
1716
if (!branch.upstream) {
1717
return undefined;
1718
}
1719
1720
try {
1721
const upstreamBranch = await this.getBranch(`refs/remotes/${branch.upstream.remote}/${branch.upstream.name}`);
1722
return upstreamBranch;
1723
}
1724
catch (err) {
1725
this.logger.warn(`[Repository][getUpstreamBranch] Failed to get branch details for 'refs/remotes/${branch.upstream.remote}/${branch.upstream.name}': ${err.message}.`);
1726
return undefined;
1727
}
1728
}
1729
1730
async getRefs(query: RefQuery = {}, cancellationToken?: CancellationToken): Promise<(Ref | Branch)[]> {
1731
const config = workspace.getConfiguration('git');
1732
let defaultSort = config.get<'alphabetically' | 'committerdate'>('branchSortOrder');
1733
if (defaultSort !== 'alphabetically' && defaultSort !== 'committerdate') {
1734
defaultSort = 'alphabetically';
1735
}
1736
1737
query = { ...query, sort: query?.sort ?? defaultSort };
1738
return await this.run(Operation.GetRefs, () => this.repository.getRefs(query, cancellationToken));
1739
}
1740
1741
async getWorktrees(): Promise<Worktree[]> {
1742
return await this.run(Operation.GetWorktrees, () => this.repository.getWorktrees());
1743
}
1744
1745
async getRemoteRefs(remote: string, opts?: { heads?: boolean; tags?: boolean }): Promise<Ref[]> {
1746
return await this.run(Operation.GetRemoteRefs, () => this.repository.getRemoteRefs(remote, opts));
1747
}
1748
1749
async setBranchUpstream(name: string, upstream: string): Promise<void> {
1750
await this.run(Operation.SetBranchUpstream, () => this.repository.setBranchUpstream(name, upstream));
1751
}
1752
1753
async merge(ref: string): Promise<void> {
1754
await this.run(Operation.Merge, () => this.repository.merge(ref));
1755
}
1756
1757
async mergeAbort(): Promise<void> {
1758
await this.run(Operation.MergeAbort, async () => await this.repository.mergeAbort());
1759
}
1760
1761
async rebase(branch: string): Promise<void> {
1762
await this.run(Operation.Rebase, () => this.repository.rebase(branch));
1763
}
1764
1765
async tag(options: { name: string; message?: string; ref?: string }): Promise<void> {
1766
await this.run(Operation.Tag, () => this.repository.tag(options));
1767
}
1768
1769
async deleteTag(name: string): Promise<void> {
1770
await this.run(Operation.DeleteTag, () => this.repository.deleteTag(name));
1771
}
1772
1773
async addWorktree(options: { path: string; commitish: string; branch?: string }): Promise<void> {
1774
await this.run(Operation.Worktree, () => this.repository.addWorktree(options));
1775
}
1776
1777
async deleteWorktree(path: string, options?: { force?: boolean }): Promise<void> {
1778
await this.run(Operation.DeleteWorktree, () => this.repository.deleteWorktree(path, options));
1779
}
1780
1781
async deleteRemoteRef(remoteName: string, refName: string, options?: { force?: boolean }): Promise<void> {
1782
await this.run(Operation.DeleteRemoteRef, () => this.repository.deleteRemoteRef(remoteName, refName, options));
1783
}
1784
1785
async checkout(treeish: string, opts?: { detached?: boolean; pullBeforeCheckout?: boolean }): Promise<void> {
1786
const refLabel = opts?.detached ? getCommitShortHash(Uri.file(this.root), treeish) : treeish;
1787
1788
await this.run(Operation.Checkout(refLabel),
1789
async () => {
1790
if (opts?.pullBeforeCheckout && !opts?.detached) {
1791
try {
1792
await this.fastForwardBranch(treeish);
1793
}
1794
catch (err) {
1795
// noop
1796
}
1797
}
1798
1799
await this.repository.checkout(treeish, [], opts);
1800
});
1801
}
1802
1803
async checkoutTracking(treeish: string, opts: { detached?: boolean } = {}): Promise<void> {
1804
const refLabel = opts.detached ? getCommitShortHash(Uri.file(this.root), treeish) : treeish;
1805
await this.run(Operation.CheckoutTracking(refLabel), () => this.repository.checkout(treeish, [], { ...opts, track: true }));
1806
}
1807
1808
async findTrackingBranches(upstreamRef: string): Promise<Branch[]> {
1809
return await this.run(Operation.FindTrackingBranches, () => this.repository.findTrackingBranches(upstreamRef));
1810
}
1811
1812
async getCommit(ref: string): Promise<Commit> {
1813
return await this.repository.getCommit(ref);
1814
}
1815
1816
async showCommit(ref: string): Promise<string> {
1817
return await this.run(Operation.Show, () => this.repository.showCommit(ref));
1818
}
1819
1820
async getEmptyTree(): Promise<string> {
1821
if (!this._EMPTY_TREE) {
1822
const result = await this.repository.exec(['hash-object', '-t', 'tree', '/dev/null']);
1823
this._EMPTY_TREE = result.stdout.trim();
1824
}
1825
1826
return this._EMPTY_TREE;
1827
}
1828
1829
async reset(treeish: string, hard?: boolean): Promise<void> {
1830
await this.run(Operation.Reset, () => this.repository.reset(treeish, hard));
1831
}
1832
1833
async deleteRef(ref: string): Promise<void> {
1834
await this.run(Operation.DeleteRef, () => this.repository.deleteRef(ref));
1835
}
1836
1837
getDefaultRemote(): Remote | undefined {
1838
if (this.remotes.length === 0) {
1839
return undefined;
1840
}
1841
1842
return this.remotes.find(r => r.name === 'origin') ?? this.remotes[0];
1843
}
1844
1845
async addRemote(name: string, url: string): Promise<void> {
1846
await this.run(Operation.Remote, () => this.repository.addRemote(name, url));
1847
}
1848
1849
async removeRemote(name: string): Promise<void> {
1850
await this.run(Operation.Remote, () => this.repository.removeRemote(name));
1851
}
1852
1853
async renameRemote(name: string, newName: string): Promise<void> {
1854
await this.run(Operation.Remote, () => this.repository.renameRemote(name, newName));
1855
}
1856
1857
@throttle
1858
async fetchDefault(options: { silent?: boolean } = {}): Promise<void> {
1859
await this._fetch({ silent: options.silent });
1860
}
1861
1862
@throttle
1863
async fetchPrune(): Promise<void> {
1864
await this._fetch({ prune: true });
1865
}
1866
1867
@throttle
1868
async fetchAll(options: { silent?: boolean } = {}, cancellationToken?: CancellationToken): Promise<void> {
1869
await this._fetch({ all: true, silent: options.silent, cancellationToken });
1870
}
1871
1872
async fetch(options: FetchOptions): Promise<void> {
1873
await this._fetch(options);
1874
}
1875
1876
private async _fetch(options: { remote?: string; ref?: string; all?: boolean; prune?: boolean; depth?: number; silent?: boolean; cancellationToken?: CancellationToken } = {}): Promise<void> {
1877
if (!options.prune) {
1878
const config = workspace.getConfiguration('git', Uri.file(this.root));
1879
const prune = config.get<boolean>('pruneOnFetch');
1880
options.prune = prune;
1881
}
1882
1883
await this.run(Operation.Fetch(options.silent !== true), async () => this.repository.fetch(options));
1884
}
1885
1886
@throttle
1887
async pullWithRebase(head: Branch | undefined): Promise<void> {
1888
let remote: string | undefined;
1889
let branch: string | undefined;
1890
1891
if (head && head.name && head.upstream) {
1892
remote = head.upstream.remote;
1893
branch = `${head.upstream.name}`;
1894
}
1895
1896
return this.pullFrom(true, remote, branch);
1897
}
1898
1899
@throttle
1900
async pull(head?: Branch, unshallow?: boolean): Promise<void> {
1901
let remote: string | undefined;
1902
let branch: string | undefined;
1903
1904
if (head && head.name && head.upstream) {
1905
remote = head.upstream.remote;
1906
branch = `${head.upstream.name}`;
1907
}
1908
1909
return this.pullFrom(false, remote, branch, unshallow);
1910
}
1911
1912
async pullFrom(rebase?: boolean, remote?: string, branch?: string, unshallow?: boolean): Promise<void> {
1913
await this.run(Operation.Pull, async () => {
1914
await this.maybeAutoStash(async () => {
1915
const config = workspace.getConfiguration('git', Uri.file(this.root));
1916
const autoStash = config.get<boolean>('autoStash');
1917
const fetchOnPull = config.get<boolean>('fetchOnPull');
1918
const tags = config.get<boolean>('pullTags');
1919
1920
// When fetchOnPull is enabled, fetch all branches when pulling
1921
if (fetchOnPull) {
1922
await this.fetchAll();
1923
}
1924
1925
if (await this.checkIfMaybeRebased(this.HEAD?.name)) {
1926
await this._pullAndHandleTagConflict(rebase, remote, branch, { unshallow, tags, autoStash });
1927
}
1928
});
1929
});
1930
}
1931
1932
private async _pullAndHandleTagConflict(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> {
1933
try {
1934
await this.repository.pull(rebase, remote, branch, options);
1935
}
1936
catch (err) {
1937
if (err.gitErrorCode !== GitErrorCodes.TagConflict) {
1938
throw err;
1939
}
1940
1941
// Handle tag(s) conflict
1942
if (await this.handleTagConflict(remote, err.stderr)) {
1943
await this.repository.pull(rebase, remote, branch, options);
1944
}
1945
}
1946
}
1947
1948
@throttle
1949
async push(head: Branch, forcePushMode?: ForcePushMode): Promise<void> {
1950
let remote: string | undefined;
1951
let branch: string | undefined;
1952
1953
if (head && head.name && head.upstream) {
1954
remote = head.upstream.remote;
1955
branch = `${head.name}:${head.upstream.name}`;
1956
}
1957
1958
await this.run(Operation.Push, () => this._push(remote, branch, undefined, undefined, forcePushMode));
1959
}
1960
1961
async pushTo(remote?: string, name?: string, setUpstream = false, forcePushMode?: ForcePushMode): Promise<void> {
1962
await this.run(Operation.Push, () => this._push(remote, name, setUpstream, undefined, forcePushMode));
1963
}
1964
1965
async pushFollowTags(remote?: string, forcePushMode?: ForcePushMode): Promise<void> {
1966
await this.run(Operation.Push, () => this._push(remote, undefined, false, true, forcePushMode));
1967
}
1968
1969
async pushTags(remote?: string, forcePushMode?: ForcePushMode): Promise<void> {
1970
await this.run(Operation.Push, () => this._push(remote, undefined, false, false, forcePushMode, true));
1971
}
1972
1973
async blame(path: string): Promise<string> {
1974
return await this.run(Operation.Blame(true), () => this.repository.blame(path));
1975
}
1976
1977
async blame2(path: string, ref?: string): Promise<BlameInformation[] | undefined> {
1978
return await this.run(Operation.Blame(false), () => this.repository.blame2(path, ref));
1979
}
1980
1981
@throttle
1982
sync(head: Branch, rebase: boolean): Promise<void> {
1983
return this._sync(head, rebase);
1984
}
1985
1986
private async _sync(head: Branch, rebase: boolean): Promise<void> {
1987
let remoteName: string | undefined;
1988
let pullBranch: string | undefined;
1989
let pushBranch: string | undefined;
1990
1991
if (head.name && head.upstream) {
1992
remoteName = head.upstream.remote;
1993
pullBranch = `${head.upstream.name}`;
1994
pushBranch = `${head.name}:${head.upstream.name}`;
1995
}
1996
1997
await this.run(Operation.Sync, async () => {
1998
await this.maybeAutoStash(async () => {
1999
const config = workspace.getConfiguration('git', Uri.file(this.root));
2000
const autoStash = config.get<boolean>('autoStash');
2001
const fetchOnPull = config.get<boolean>('fetchOnPull');
2002
const tags = config.get<boolean>('pullTags');
2003
const followTags = config.get<boolean>('followTagsWhenSync');
2004
const supportCancellation = config.get<boolean>('supportCancellation');
2005
2006
const fn = async (cancellationToken?: CancellationToken) => {
2007
// When fetchOnPull is enabled, fetch all branches when pulling
2008
if (fetchOnPull) {
2009
await this.fetchAll({}, cancellationToken);
2010
}
2011
2012
if (await this.checkIfMaybeRebased(this.HEAD?.name)) {
2013
await this._pullAndHandleTagConflict(rebase, remoteName, pullBranch, { tags, cancellationToken, autoStash });
2014
}
2015
};
2016
2017
if (supportCancellation) {
2018
const opts: ProgressOptions = {
2019
location: ProgressLocation.Notification,
2020
title: l10n.t('Syncing. Cancelling may cause serious damages to the repository'),
2021
cancellable: true
2022
};
2023
2024
await window.withProgress(opts, (_, token) => fn(token));
2025
} else {
2026
await fn();
2027
}
2028
2029
const remote = this.remotes.find(r => r.name === remoteName);
2030
2031
if (remote && remote.isReadOnly) {
2032
return;
2033
}
2034
2035
const shouldPush = this.HEAD && (typeof this.HEAD.ahead === 'number' ? this.HEAD.ahead > 0 : true);
2036
2037
if (shouldPush) {
2038
await this._push(remoteName, pushBranch, false, followTags);
2039
}
2040
});
2041
});
2042
}
2043
2044
private async checkIfMaybeRebased(currentBranch?: string) {
2045
const config = workspace.getConfiguration('git');
2046
const shouldIgnore = config.get<boolean>('ignoreRebaseWarning') === true;
2047
2048
if (shouldIgnore) {
2049
return true;
2050
}
2051
2052
const maybeRebased = await this.run(Operation.Log(true), async () => {
2053
try {
2054
const result = await this.repository.exec(['log', '--oneline', '--cherry', `${currentBranch ?? ''}...${currentBranch ?? ''}@{upstream}`, '--']);
2055
if (result.exitCode) {
2056
return false;
2057
}
2058
2059
return /^=/.test(result.stdout);
2060
} catch {
2061
return false;
2062
}
2063
});
2064
2065
if (!maybeRebased) {
2066
return true;
2067
}
2068
2069
const always = { title: l10n.t('Always Pull') };
2070
const pull = { title: l10n.t('Pull') };
2071
const cancel = { title: l10n.t('Don\'t Pull') };
2072
const result = await window.showWarningMessage(
2073
currentBranch
2074
? l10n.t('It looks like the current branch "{0}" might have been rebased. Are you sure you still want to pull into it?', currentBranch)
2075
: l10n.t('It looks like the current branch might have been rebased. Are you sure you still want to pull into it?'),
2076
always, pull, cancel
2077
);
2078
2079
if (result === pull) {
2080
return true;
2081
}
2082
2083
if (result === always) {
2084
await config.update('ignoreRebaseWarning', true, true);
2085
2086
return true;
2087
}
2088
2089
return false;
2090
}
2091
2092
async show(ref: string, filePath: string): Promise<string> {
2093
return await this.run(Operation.Show, async () => {
2094
try {
2095
const content = await this.repository.buffer(ref, filePath);
2096
return await workspace.decode(content, { uri: Uri.file(filePath) });
2097
} catch (err) {
2098
if (err.gitErrorCode === GitErrorCodes.WrongCase) {
2099
const gitFilePath = await this.repository.getGitFilePath(ref, filePath);
2100
const content = await this.repository.buffer(ref, gitFilePath);
2101
return await workspace.decode(content, { uri: Uri.file(filePath) });
2102
}
2103
2104
throw err;
2105
}
2106
});
2107
}
2108
2109
async buffer(ref: string, filePath: string): Promise<Buffer> {
2110
return this.run(Operation.Show, () => this.repository.buffer(ref, filePath));
2111
}
2112
2113
getObjectFiles(ref: string): Promise<LsTreeElement[]> {
2114
return this.run(Operation.GetObjectFiles, () => this.repository.lstree(ref));
2115
}
2116
2117
getObjectDetails(ref: string, path: string): Promise<{ mode: string; object: string; size: number }> {
2118
return this.run(Operation.GetObjectDetails, () => this.repository.getObjectDetails(ref, path));
2119
}
2120
2121
detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }> {
2122
return this.run(Operation.Show, () => this.repository.detectObjectType(object));
2123
}
2124
2125
async apply(patch: string, reverse?: boolean): Promise<void> {
2126
return await this.run(Operation.Apply, () => this.repository.apply(patch, reverse));
2127
}
2128
2129
async getStashes(): Promise<Stash[]> {
2130
return this.run(Operation.Stash, () => this.repository.getStashes());
2131
}
2132
2133
async createStash(message?: string, includeUntracked?: boolean, staged?: boolean): Promise<void> {
2134
const indexResources = [...this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath)];
2135
const workingGroupResources = [
2136
...!staged ? this.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath) : [],
2137
...includeUntracked ? this.untrackedGroup.resourceStates.map(r => r.resourceUri.fsPath) : []];
2138
2139
return await this.run(Operation.Stash, async () => {
2140
await this.repository.createStash(message, includeUntracked, staged);
2141
this.closeDiffEditors(indexResources, workingGroupResources);
2142
});
2143
}
2144
2145
async popStash(index?: number): Promise<void> {
2146
return await this.run(Operation.Stash, () => this.repository.popStash(index));
2147
}
2148
2149
async dropStash(index?: number): Promise<void> {
2150
return await this.run(Operation.Stash, () => this.repository.dropStash(index));
2151
}
2152
2153
async applyStash(index?: number): Promise<void> {
2154
return await this.run(Operation.Stash, () => this.repository.applyStash(index));
2155
}
2156
2157
async showStash(index: number): Promise<Change[] | undefined> {
2158
return await this.run(Operation.Stash, () => this.repository.showStash(index));
2159
}
2160
2161
async getCommitTemplate(): Promise<string> {
2162
return await this.run(Operation.GetCommitTemplate, async () => this.repository.getCommitTemplate());
2163
}
2164
2165
async ignore(files: Uri[]): Promise<void> {
2166
return await this.run(Operation.Ignore, async () => {
2167
const ignoreFile = `${this.repository.root}${path.sep}.gitignore`;
2168
const textToAppend = files
2169
.map(uri => relativePath(this.repository.root, uri.fsPath)
2170
.replace(/\\|\[/g, match => match === '\\' ? '/' : `\\${match}`))
2171
.join('\n');
2172
2173
const document = await new Promise(c => fs.exists(ignoreFile, c))
2174
? await workspace.openTextDocument(ignoreFile)
2175
: await workspace.openTextDocument(Uri.file(ignoreFile).with({ scheme: 'untitled' }));
2176
2177
await window.showTextDocument(document);
2178
2179
const edit = new WorkspaceEdit();
2180
const lastLine = document.lineAt(document.lineCount - 1);
2181
const text = lastLine.isEmptyOrWhitespace ? `${textToAppend}\n` : `\n${textToAppend}\n`;
2182
2183
edit.insert(document.uri, lastLine.range.end, text);
2184
await workspace.applyEdit(edit);
2185
await document.save();
2186
});
2187
}
2188
2189
async rebaseAbort(): Promise<void> {
2190
await this.run(Operation.RebaseAbort, async () => await this.repository.rebaseAbort());
2191
}
2192
2193
checkIgnore(filePaths: string[]): Promise<Set<string>> {
2194
return this.run(Operation.CheckIgnore, () => {
2195
return new Promise<Set<string>>((resolve, reject) => {
2196
2197
filePaths = filePaths
2198
.filter(filePath => isDescendant(this.root, filePath));
2199
2200
if (filePaths.length === 0) {
2201
// nothing left
2202
return resolve(new Set<string>());
2203
}
2204
2205
// https://git-scm.com/docs/git-check-ignore#git-check-ignore--z
2206
const child = this.repository.stream(['check-ignore', '-v', '-z', '--stdin'], { stdio: [null, null, null] });
2207
child.stdin!.end(filePaths.join('\0'), 'utf8');
2208
2209
const onExit = (exitCode: number) => {
2210
if (exitCode === 1) {
2211
// nothing ignored
2212
resolve(new Set<string>());
2213
} else if (exitCode === 0) {
2214
resolve(new Set<string>(this.parseIgnoreCheck(data)));
2215
} else {
2216
if (/ is in submodule /.test(stderr)) {
2217
reject(new GitError({ stdout: data, stderr, exitCode, gitErrorCode: GitErrorCodes.IsInSubmodule }));
2218
} else {
2219
reject(new GitError({ stdout: data, stderr, exitCode }));
2220
}
2221
}
2222
};
2223
2224
let data = '';
2225
const onStdoutData = (raw: string) => {
2226
data += raw;
2227
};
2228
2229
child.stdout!.setEncoding('utf8');
2230
child.stdout!.on('data', onStdoutData);
2231
2232
let stderr: string = '';
2233
child.stderr!.setEncoding('utf8');
2234
child.stderr!.on('data', raw => stderr += raw);
2235
2236
child.on('error', reject);
2237
child.on('exit', onExit);
2238
});
2239
});
2240
}
2241
2242
// Parses output of `git check-ignore -v -z` and returns only those paths
2243
// that are actually ignored by git.
2244
// Matches to a negative pattern (starting with '!') are filtered out.
2245
// See also https://git-scm.com/docs/git-check-ignore#_output.
2246
private parseIgnoreCheck(raw: string): string[] {
2247
const ignored = [];
2248
const elements = raw.split('\0');
2249
for (let i = 0; i < elements.length; i += 4) {
2250
const pattern = elements[i + 2];
2251
const path = elements[i + 3];
2252
if (pattern && !pattern.startsWith('!')) {
2253
ignored.push(path);
2254
}
2255
}
2256
return ignored;
2257
}
2258
2259
private async _push(remote?: string, refspec?: string, setUpstream = false, followTags = false, forcePushMode?: ForcePushMode, tags = false): Promise<void> {
2260
try {
2261
await this.repository.push(remote, refspec, setUpstream, followTags, forcePushMode, tags);
2262
} catch (err) {
2263
if (!remote || !refspec) {
2264
throw err;
2265
}
2266
2267
const repository = new ApiRepository(this);
2268
const remoteObj = repository.state.remotes.find(r => r.name === remote);
2269
2270
if (!remoteObj) {
2271
throw err;
2272
}
2273
2274
for (const handler of this.pushErrorHandlerRegistry.getPushErrorHandlers()) {
2275
if (await handler.handlePushError(repository, remoteObj, refspec, err)) {
2276
return;
2277
}
2278
}
2279
2280
throw err;
2281
}
2282
}
2283
2284
private async run<T>(
2285
operation: Operation,
2286
runOperation: () => Promise<T> = () => Promise.resolve<any>(null),
2287
getOptimisticResourceGroups: () => GitResourceGroups | undefined = () => undefined): Promise<T> {
2288
2289
if (this.state !== RepositoryState.Idle) {
2290
throw new Error('Repository not initialized');
2291
}
2292
2293
let error: any = null;
2294
2295
this._operations.start(operation);
2296
this._onRunOperation.fire(operation.kind);
2297
2298
try {
2299
const result = await this.retryRun(operation, runOperation);
2300
2301
if (!operation.readOnly) {
2302
await this.updateModelState(this.optimisticUpdateEnabled() ? getOptimisticResourceGroups() : undefined);
2303
}
2304
2305
return result;
2306
} catch (err) {
2307
error = err;
2308
2309
if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) {
2310
this.state = RepositoryState.Disposed;
2311
}
2312
2313
if (!operation.readOnly) {
2314
await this.updateModelState();
2315
}
2316
2317
throw err;
2318
} finally {
2319
this._operations.end(operation);
2320
this._onDidRunOperation.fire({ operation: operation, error });
2321
}
2322
}
2323
2324
private async retryRun<T>(operation: Operation, runOperation: () => Promise<T> = () => Promise.resolve<any>(null)): Promise<T> {
2325
let attempt = 0;
2326
2327
while (true) {
2328
try {
2329
attempt++;
2330
return await runOperation();
2331
} catch (err) {
2332
const shouldRetry = attempt <= 10 && (
2333
(err.gitErrorCode === GitErrorCodes.RepositoryIsLocked)
2334
|| (operation.retry && (err.gitErrorCode === GitErrorCodes.CantLockRef || err.gitErrorCode === GitErrorCodes.CantRebaseMultipleBranches))
2335
);
2336
2337
if (shouldRetry) {
2338
// quatratic backoff
2339
await timeout(Math.pow(attempt, 2) * 50);
2340
} else {
2341
throw err;
2342
}
2343
}
2344
}
2345
}
2346
2347
private static KnownHugeFolderNames = ['node_modules'];
2348
2349
private async findKnownHugeFolderPathsToIgnore(): Promise<string[]> {
2350
const folderPaths: string[] = [];
2351
2352
for (const folderName of Repository.KnownHugeFolderNames) {
2353
const folderPath = path.join(this.repository.root, folderName);
2354
2355
if (await new Promise<boolean>(c => fs.exists(folderPath, c))) {
2356
folderPaths.push(folderPath);
2357
}
2358
}
2359
2360
const ignored = await this.checkIgnore(folderPaths);
2361
2362
return folderPaths.filter(p => !ignored.has(p));
2363
}
2364
2365
private async updateModelState(optimisticResourcesGroups?: GitResourceGroups) {
2366
this.updateModelStateCancellationTokenSource?.cancel();
2367
2368
this.updateModelStateCancellationTokenSource = new CancellationTokenSource();
2369
await this._updateModelState(optimisticResourcesGroups, this.updateModelStateCancellationTokenSource.token);
2370
}
2371
2372
private async _updateModelState(optimisticResourcesGroups?: GitResourceGroups, cancellationToken?: CancellationToken): Promise<void> {
2373
try {
2374
// Optimistically update resource groups
2375
if (optimisticResourcesGroups) {
2376
this._updateResourceGroupsState(optimisticResourcesGroups);
2377
}
2378
2379
const [HEAD, remotes, submodules, worktrees, rebaseCommit, mergeInProgress, cherryPickInProgress, commitTemplate] =
2380
await Promise.all([
2381
this.repository.getHEADRef(),
2382
this.repository.getRemotes(),
2383
this.repository.getSubmodules(),
2384
this.repository.getWorktrees(),
2385
this.getRebaseCommit(),
2386
this.isMergeInProgress(),
2387
this.isCherryPickInProgress(),
2388
this.getInputTemplate()]);
2389
2390
// Reset the list of unpublished commits if HEAD has
2391
// changed (ex: checkout, fetch, pull, push, publish, etc.).
2392
// The list of unpublished commits will be computed lazily
2393
// on demand.
2394
if (this.HEAD?.name !== HEAD?.name ||
2395
this.HEAD?.commit !== HEAD?.commit ||
2396
this.HEAD?.ahead !== HEAD?.ahead ||
2397
this.HEAD?.upstream !== HEAD?.upstream) {
2398
this.unpublishedCommits = undefined;
2399
}
2400
2401
this._HEAD = HEAD;
2402
this._remotes = remotes!;
2403
this._submodules = submodules!;
2404
this._worktrees = worktrees!;
2405
this.rebaseCommit = rebaseCommit;
2406
this.mergeInProgress = mergeInProgress;
2407
this.cherryPickInProgress = cherryPickInProgress;
2408
2409
this._sourceControl.commitTemplate = commitTemplate;
2410
2411
// Execute cancellable long-running operation
2412
const [resourceGroups, refs] =
2413
await Promise.all([
2414
this.getStatus(cancellationToken),
2415
this.getRefs({}, cancellationToken)]);
2416
2417
this._refs = refs;
2418
this._updateResourceGroupsState(resourceGroups);
2419
2420
this._onDidChangeStatus.fire();
2421
}
2422
catch (err) {
2423
if (err instanceof CancellationError) {
2424
return;
2425
}
2426
2427
throw err;
2428
}
2429
}
2430
2431
private _updateResourceGroupsState(resourcesGroups: GitResourceGroups): void {
2432
// set resource groups
2433
if (resourcesGroups.indexGroup) { this.indexGroup.resourceStates = resourcesGroups.indexGroup; }
2434
if (resourcesGroups.mergeGroup) { this.mergeGroup.resourceStates = resourcesGroups.mergeGroup; }
2435
if (resourcesGroups.untrackedGroup) { this.untrackedGroup.resourceStates = resourcesGroups.untrackedGroup; }
2436
if (resourcesGroups.workingTreeGroup) { this.workingTreeGroup.resourceStates = resourcesGroups.workingTreeGroup; }
2437
2438
// clear worktree migrating flag once all conflicts are resolved
2439
if (this._isWorktreeMigrating && resourcesGroups.mergeGroup && resourcesGroups.mergeGroup.length === 0) {
2440
this._isWorktreeMigrating = false;
2441
}
2442
2443
// set count badge
2444
this.setCountBadge();
2445
}
2446
2447
private async getStatus(cancellationToken?: CancellationToken): Promise<GitResourceGroups> {
2448
if (cancellationToken && cancellationToken.isCancellationRequested) {
2449
throw new CancellationError();
2450
}
2451
2452
const scopedConfig = workspace.getConfiguration('git', Uri.file(this.repository.root));
2453
const untrackedChanges = scopedConfig.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
2454
const ignoreSubmodules = scopedConfig.get<boolean>('ignoreSubmodules');
2455
2456
const limit = scopedConfig.get<number>('statusLimit', 10000);
2457
const similarityThreshold = scopedConfig.get<number>('similarityThreshold', 50);
2458
2459
const start = new Date().getTime();
2460
const { status, statusLength, didHitLimit } = await this.repository.getStatus({ limit, ignoreSubmodules, similarityThreshold, untrackedChanges, cancellationToken });
2461
const totalTime = new Date().getTime() - start;
2462
2463
this.isRepositoryHuge = didHitLimit ? { limit } : false;
2464
2465
if (didHitLimit) {
2466
/* __GDPR__
2467
"statusLimit" : {
2468
"owner": "lszomoru",
2469
"ignoreSubmodules": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Setting indicating whether submodules are ignored" },
2470
"limit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Setting indicating the limit of status entries" },
2471
"statusLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of status entries" },
2472
"totalTime": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of ms the operation took" }
2473
}
2474
*/
2475
this.telemetryReporter.sendTelemetryEvent('statusLimit', { ignoreSubmodules: String(ignoreSubmodules) }, { limit, statusLength, totalTime });
2476
}
2477
2478
if (totalTime > 5000) {
2479
/* __GDPR__
2480
"statusSlow" : {
2481
"owner": "digitarald",
2482
"comment": "Reports when git status is slower than 5s",
2483
"expiration": "1.73",
2484
"ignoreSubmodules": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Setting indicating whether submodules are ignored" },
2485
"didHitLimit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Total number of status entries" },
2486
"didWarnAboutLimit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "True when the user was warned about slow git status" },
2487
"statusLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of status entries" },
2488
"totalTime": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of ms the operation took" }
2489
}
2490
*/
2491
this.telemetryReporter.sendTelemetryEvent('statusSlow', { ignoreSubmodules: String(ignoreSubmodules), didHitLimit: String(didHitLimit), didWarnAboutLimit: String(this.didWarnAboutLimit) }, { statusLength, totalTime });
2492
}
2493
2494
// Triggers or clears any validation warning
2495
this._sourceControl.inputBox.validateInput = this._sourceControl.inputBox.validateInput;
2496
2497
const config = workspace.getConfiguration('git');
2498
const shouldIgnore = config.get<boolean>('ignoreLimitWarning') === true;
2499
const useIcons = !config.get<boolean>('decorations.enabled', true);
2500
2501
if (didHitLimit && !shouldIgnore && !this.didWarnAboutLimit) {
2502
const knownHugeFolderPaths = await this.findKnownHugeFolderPathsToIgnore();
2503
const gitWarn = l10n.t('The git repository at "{0}" has too many active changes, only a subset of Git features will be enabled.', this.repository.root);
2504
const neverAgain = { title: l10n.t('Don\'t Show Again') };
2505
2506
if (knownHugeFolderPaths.length > 0) {
2507
const folderPath = knownHugeFolderPaths[0];
2508
const folderName = path.basename(folderPath);
2509
2510
const addKnown = l10n.t('Would you like to add "{0}" to .gitignore?', folderName);
2511
const yes = { title: l10n.t('Yes') };
2512
const no = { title: l10n.t('No') };
2513
2514
window.showWarningMessage(`${gitWarn} ${addKnown}`, yes, no, neverAgain).then(result => {
2515
if (result === yes) {
2516
this.ignore([Uri.file(folderPath)]);
2517
} else {
2518
if (result === neverAgain) {
2519
config.update('ignoreLimitWarning', true, false);
2520
}
2521
2522
this.didWarnAboutLimit = true;
2523
}
2524
});
2525
} else {
2526
const ok = { title: l10n.t('OK') };
2527
window.showWarningMessage(gitWarn, ok, neverAgain).then(result => {
2528
if (result === neverAgain) {
2529
config.update('ignoreLimitWarning', true, false);
2530
}
2531
2532
this.didWarnAboutLimit = true;
2533
});
2534
}
2535
}
2536
2537
const indexGroup: Resource[] = [],
2538
mergeGroup: Resource[] = [],
2539
untrackedGroup: Resource[] = [],
2540
workingTreeGroup: Resource[] = [];
2541
2542
status.forEach(raw => {
2543
const uri = Uri.file(path.join(this.repository.root, raw.path));
2544
const renameUri = raw.rename
2545
? Uri.file(path.join(this.repository.root, raw.rename))
2546
: undefined;
2547
2548
switch (raw.x + raw.y) {
2549
case '??': switch (untrackedChanges) {
2550
case 'mixed': return workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.UNTRACKED, useIcons, undefined, this.kind));
2551
case 'separate': return untrackedGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Untracked, uri, Status.UNTRACKED, useIcons));
2552
default: return undefined;
2553
}
2554
case '!!': switch (untrackedChanges) {
2555
case 'mixed': return workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.IGNORED, useIcons, undefined, this.kind));
2556
case 'separate': return untrackedGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Untracked, uri, Status.IGNORED, useIcons));
2557
default: return undefined;
2558
}
2559
case 'DD': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.BOTH_DELETED, useIcons));
2560
case 'AU': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.ADDED_BY_US, useIcons));
2561
case 'UD': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.DELETED_BY_THEM, useIcons));
2562
case 'UA': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.ADDED_BY_THEM, useIcons));
2563
case 'DU': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.DELETED_BY_US, useIcons));
2564
case 'AA': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.BOTH_ADDED, useIcons));
2565
case 'UU': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.BOTH_MODIFIED, useIcons));
2566
}
2567
2568
switch (raw.x) {
2569
case 'M': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_MODIFIED, useIcons, undefined, this.kind)); break;
2570
case 'A': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_ADDED, useIcons, undefined, this.kind)); break;
2571
case 'D': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_DELETED, useIcons, undefined, this.kind)); break;
2572
case 'R': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_RENAMED, useIcons, renameUri, this.kind)); break;
2573
case 'C': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_COPIED, useIcons, renameUri, this.kind)); break;
2574
}
2575
2576
switch (raw.y) {
2577
case 'M': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.MODIFIED, useIcons, renameUri, this.kind)); break;
2578
case 'D': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.DELETED, useIcons, renameUri, this.kind)); break;
2579
case 'A': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.INTENT_TO_ADD, useIcons, renameUri, this.kind)); break;
2580
case 'R': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.INTENT_TO_RENAME, useIcons, renameUri, this.kind)); break;
2581
case 'T': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.TYPE_CHANGED, useIcons, renameUri, this.kind)); break;
2582
}
2583
2584
return undefined;
2585
});
2586
2587
return { indexGroup, mergeGroup, untrackedGroup, workingTreeGroup };
2588
}
2589
2590
private setCountBadge(): void {
2591
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
2592
const countBadge = config.get<'all' | 'tracked' | 'off'>('countBadge');
2593
const untrackedChanges = config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
2594
2595
let count =
2596
this.mergeGroup.resourceStates.length +
2597
this.indexGroup.resourceStates.length +
2598
this.workingTreeGroup.resourceStates.length;
2599
2600
switch (countBadge) {
2601
case 'off': count = 0; break;
2602
case 'tracked':
2603
if (untrackedChanges === 'mixed') {
2604
count -= this.workingTreeGroup.resourceStates.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED).length;
2605
}
2606
break;
2607
case 'all':
2608
if (untrackedChanges === 'separate') {
2609
count += this.untrackedGroup.resourceStates.length;
2610
}
2611
break;
2612
}
2613
2614
this._sourceControl.count = count;
2615
}
2616
2617
private async getRebaseCommit(): Promise<Commit | undefined> {
2618
const rebaseHeadPath = path.join(this.repository.root, '.git', 'REBASE_HEAD');
2619
const rebaseApplyPath = path.join(this.repository.root, '.git', 'rebase-apply');
2620
const rebaseMergePath = path.join(this.repository.root, '.git', 'rebase-merge');
2621
2622
try {
2623
const [rebaseApplyExists, rebaseMergePathExists, rebaseHead] = await Promise.all([
2624
new Promise<boolean>(c => fs.exists(rebaseApplyPath, c)),
2625
new Promise<boolean>(c => fs.exists(rebaseMergePath, c)),
2626
new Promise<string>((c, e) => fs.readFile(rebaseHeadPath, 'utf8', (err, result) => err ? e(err) : c(result)))
2627
]);
2628
if (!rebaseApplyExists && !rebaseMergePathExists) {
2629
return undefined;
2630
}
2631
return await this.getCommit(rebaseHead.trim());
2632
} catch (err) {
2633
return undefined;
2634
}
2635
}
2636
2637
private isMergeInProgress(): Promise<boolean> {
2638
const mergeHeadPath = path.join(this.repository.root, '.git', 'MERGE_HEAD');
2639
return new Promise<boolean>(resolve => fs.exists(mergeHeadPath, resolve));
2640
}
2641
2642
private isCherryPickInProgress(): Promise<boolean> {
2643
const cherryPickHeadPath = path.join(this.repository.root, '.git', 'CHERRY_PICK_HEAD');
2644
return new Promise<boolean>(resolve => fs.exists(cherryPickHeadPath, resolve));
2645
}
2646
2647
private async maybeAutoStash<T>(runOperation: () => Promise<T>): Promise<T> {
2648
const config = workspace.getConfiguration('git', Uri.file(this.root));
2649
const shouldAutoStash = config.get<boolean>('autoStash')
2650
&& this.repository.git.compareGitVersionTo('2.27.0') < 0
2651
&& (this.indexGroup.resourceStates.length > 0
2652
|| this.workingTreeGroup.resourceStates.some(
2653
r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED));
2654
2655
if (!shouldAutoStash) {
2656
return await runOperation();
2657
}
2658
2659
await this.repository.createStash(undefined, true);
2660
try {
2661
const result = await runOperation();
2662
return result;
2663
} finally {
2664
await this.repository.popStash();
2665
}
2666
}
2667
2668
private onFileChange(_uri: Uri): void {
2669
const config = workspace.getConfiguration('git');
2670
const autorefresh = config.get<boolean>('autorefresh');
2671
2672
if (!autorefresh) {
2673
this.logger.trace('[Repository][onFileChange] Skip running git status because autorefresh setting is disabled.');
2674
return;
2675
}
2676
2677
if (this.isRepositoryHuge) {
2678
this.logger.trace('[Repository][onFileChange] Skip running git status because repository is huge.');
2679
return;
2680
}
2681
2682
if (!this.operations.isIdle()) {
2683
this.logger.trace('[Repository][onFileChange] Skip running git status because an operation is running.');
2684
return;
2685
}
2686
2687
this.eventuallyUpdateWhenIdleAndWait();
2688
}
2689
2690
@debounce(1000)
2691
private eventuallyUpdateWhenIdleAndWait(): void {
2692
this.updateWhenIdleAndWait();
2693
}
2694
2695
@throttle
2696
private async updateWhenIdleAndWait(): Promise<void> {
2697
await this.whenIdleAndFocused();
2698
await this.status();
2699
await timeout(5000);
2700
}
2701
2702
async whenIdleAndFocused(): Promise<void> {
2703
while (true) {
2704
if (!this.operations.isIdle()) {
2705
await eventToPromise(this.onDidRunOperation);
2706
continue;
2707
}
2708
2709
if (!window.state.focused) {
2710
const onDidFocusWindow = filterEvent(window.onDidChangeWindowState, e => e.focused);
2711
await eventToPromise(onDidFocusWindow);
2712
continue;
2713
}
2714
2715
return;
2716
}
2717
}
2718
2719
get headLabel(): string {
2720
const HEAD = this.HEAD;
2721
2722
if (!HEAD) {
2723
return '';
2724
}
2725
2726
const head = HEAD.name || (HEAD.commit || '').substr(0, 8);
2727
2728
return head
2729
+ (this.workingTreeGroup.resourceStates.length + this.untrackedGroup.resourceStates.length > 0 ? '*' : '')
2730
+ (this.indexGroup.resourceStates.length > 0 ? '+' : '')
2731
+ (this.mergeInProgress || !!this.rebaseCommit ? '!' : '');
2732
}
2733
2734
get syncLabel(): string {
2735
if (!this.HEAD
2736
|| !this.HEAD.name
2737
|| !this.HEAD.commit
2738
|| !this.HEAD.upstream
2739
|| !(this.HEAD.ahead || this.HEAD.behind)
2740
) {
2741
return '';
2742
}
2743
2744
const remoteName = this.HEAD && this.HEAD.remote || this.HEAD.upstream.remote;
2745
const remote = this.remotes.find(r => r.name === remoteName);
2746
2747
if (remote && remote.isReadOnly) {
2748
return `${this.HEAD.behind}↓`;
2749
}
2750
2751
return `${this.HEAD.behind}↓ ${this.HEAD.ahead}↑`;
2752
}
2753
2754
get syncTooltip(): string {
2755
if (!this.HEAD
2756
|| !this.HEAD.name
2757
|| !this.HEAD.commit
2758
|| !this.HEAD.upstream
2759
|| !(this.HEAD.ahead || this.HEAD.behind)
2760
) {
2761
return l10n.t('Synchronize Changes');
2762
}
2763
2764
const remoteName = this.HEAD && this.HEAD.remote || this.HEAD.upstream.remote;
2765
const remote = this.remotes.find(r => r.name === remoteName);
2766
2767
if ((remote && remote.isReadOnly) || !this.HEAD.ahead) {
2768
return l10n.t('Pull {0} commits from {1}/{2}', this.HEAD.behind!, this.HEAD.upstream.remote, this.HEAD.upstream.name);
2769
} else if (!this.HEAD.behind) {
2770
return l10n.t('Push {0} commits to {1}/{2}', this.HEAD.ahead, this.HEAD.upstream.remote, this.HEAD.upstream.name);
2771
} else {
2772
return l10n.t('Pull {0} and push {1} commits between {2}/{3}', this.HEAD.behind, this.HEAD.ahead, this.HEAD.upstream.remote, this.HEAD.upstream.name);
2773
}
2774
}
2775
2776
private updateInputBoxPlaceholder(): void {
2777
const branchName = this.headShortName;
2778
2779
if (branchName) {
2780
// '{0}' will be replaced by the corresponding key-command later in the process, which is why it needs to stay.
2781
this._sourceControl.inputBox.placeholder = l10n.t('Message ({0} to commit on "{1}")', '{0}', branchName);
2782
} else {
2783
this._sourceControl.inputBox.placeholder = l10n.t('Message ({0} to commit)');
2784
}
2785
}
2786
2787
private updateBranchProtectionMatchers(root: Uri): void {
2788
this.branchProtection.clear();
2789
2790
for (const provider of this.branchProtectionProviderRegistry.getBranchProtectionProviders(root)) {
2791
for (const { remote, rules } of provider.provideBranchProtection()) {
2792
const matchers: BranchProtectionMatcher[] = [];
2793
2794
for (const rule of rules) {
2795
const include = rule.include && rule.include.length !== 0 ? picomatch(rule.include) : undefined;
2796
const exclude = rule.exclude && rule.exclude.length !== 0 ? picomatch(rule.exclude) : undefined;
2797
2798
if (include || exclude) {
2799
matchers.push({ include, exclude });
2800
}
2801
}
2802
2803
if (matchers.length !== 0) {
2804
this.branchProtection.set(remote, matchers);
2805
}
2806
}
2807
}
2808
2809
this._onDidChangeBranchProtection.fire();
2810
}
2811
2812
private optimisticUpdateEnabled(): boolean {
2813
const config = workspace.getConfiguration('git', Uri.file(this.root));
2814
return config.get<boolean>('optimisticUpdate') === true;
2815
}
2816
2817
private async handleTagConflict(remote: string | undefined, raw: string): Promise<boolean> {
2818
// Ensure there is a remote
2819
remote = remote ?? this.HEAD?.upstream?.remote;
2820
if (!remote) {
2821
throw new Error('Unable to resolve tag conflict due to missing remote.');
2822
}
2823
2824
// Extract tag names from message
2825
const tags: string[] = [];
2826
for (const match of raw.matchAll(/^ ! \[rejected\]\s+([^\s]+)\s+->\s+([^\s]+)\s+\(would clobber existing tag\)$/gm)) {
2827
if (match.length === 3) {
2828
tags.push(match[1]);
2829
}
2830
}
2831
if (tags.length === 0) {
2832
throw new Error(`Unable to extract tag names from error message: ${raw}`);
2833
}
2834
2835
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
2836
const replaceTagsWhenPull = config.get<boolean>('replaceTagsWhenPull', false) === true;
2837
2838
if (!replaceTagsWhenPull) {
2839
// Notification
2840
const replaceLocalTags = l10n.t('Replace Local Tag(s)');
2841
const replaceLocalTagsAlways = l10n.t('Always Replace Local Tag(s)');
2842
const message = l10n.t('Unable to pull from remote repository due to conflicting tag(s): {0}. Would you like to resolve the conflict by replacing the local tag(s)?', tags.join(', '));
2843
const choice = await window.showErrorMessage(message, { modal: true }, replaceLocalTags, replaceLocalTagsAlways);
2844
2845
if (choice !== replaceLocalTags && choice !== replaceLocalTagsAlways) {
2846
return false;
2847
}
2848
2849
if (choice === replaceLocalTagsAlways) {
2850
await config.update('replaceTagsWhenPull', true, true);
2851
}
2852
}
2853
2854
// Force fetch tags
2855
await this.repository.fetchTags({ remote, tags, force: true });
2856
return true;
2857
}
2858
2859
public isBranchProtected(branch = this.HEAD): boolean {
2860
if (branch?.name) {
2861
// Default branch protection (settings)
2862
const defaultBranchProtectionMatcher = this.branchProtection.get('');
2863
if (defaultBranchProtectionMatcher?.length === 1 &&
2864
defaultBranchProtectionMatcher[0].include &&
2865
defaultBranchProtectionMatcher[0].include(branch.name)) {
2866
return true;
2867
}
2868
2869
if (branch.upstream?.remote) {
2870
// Branch protection (contributed)
2871
const remoteBranchProtectionMatcher = this.branchProtection.get(branch.upstream.remote);
2872
if (remoteBranchProtectionMatcher && remoteBranchProtectionMatcher?.length !== 0) {
2873
return remoteBranchProtectionMatcher.some(matcher => {
2874
const include = matcher.include ? matcher.include(branch.name!) : true;
2875
const exclude = matcher.exclude ? matcher.exclude(branch.name!) : false;
2876
2877
return include && !exclude;
2878
});
2879
}
2880
}
2881
}
2882
2883
return false;
2884
}
2885
2886
async getUnpublishedCommits(): Promise<Set<string>> {
2887
if (this.unpublishedCommits) {
2888
return this.unpublishedCommits;
2889
}
2890
2891
if (!this.HEAD?.name) {
2892
this.unpublishedCommits = new Set<string>();
2893
return this.unpublishedCommits;
2894
}
2895
2896
if (this.HEAD.upstream) {
2897
// Upstream
2898
if (this.HEAD.ahead === 0) {
2899
this.unpublishedCommits = new Set<string>();
2900
} else {
2901
const ref1 = `${this.HEAD.upstream.remote}/${this.HEAD.upstream.name}`;
2902
const ref2 = this.HEAD.name;
2903
2904
const revList = await this.repository.revList(ref1, ref2);
2905
this.unpublishedCommits = new Set<string>(revList);
2906
}
2907
} else if (this.historyProvider.currentHistoryItemBaseRef) {
2908
// Base
2909
const ref1 = this.historyProvider.currentHistoryItemBaseRef.id;
2910
const ref2 = this.HEAD.name;
2911
2912
const revList = await this.repository.revList(ref1, ref2);
2913
this.unpublishedCommits = new Set<string>(revList);
2914
} else {
2915
this.unpublishedCommits = new Set<string>();
2916
}
2917
2918
return this.unpublishedCommits;
2919
}
2920
2921
dispose(): void {
2922
this.disposables = dispose(this.disposables);
2923
}
2924
}
2925
2926
export class StagedResourceQuickDiffProvider implements QuickDiffProvider {
2927
readonly label = l10n.t('Git Local Changes (Index)');
2928
2929
constructor(
2930
private readonly _repository: Repository,
2931
private readonly logger: LogOutputChannel
2932
) { }
2933
2934
async provideOriginalResource(uri: Uri): Promise<Uri | undefined> {
2935
this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource: ${uri.toString()}`);
2936
2937
if (uri.scheme !== 'file') {
2938
this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is not a file: ${uri.scheme}`);
2939
return undefined;
2940
}
2941
2942
// Ignore symbolic links
2943
const stat = await workspace.fs.stat(uri);
2944
if ((stat.type & FileType.SymbolicLink) !== 0) {
2945
this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is a symbolic link: ${uri.toString()}`);
2946
return undefined;
2947
}
2948
2949
// Ignore resources that are not in the index group
2950
if (!this._repository.indexGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) {
2951
this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Resource is not part of a index group: ${uri.toString()}`);
2952
return undefined;
2953
}
2954
2955
const originalResource = toGitUri(uri, 'HEAD', { replaceFileExtension: true });
2956
this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Original resource: ${originalResource.toString()}`);
2957
return originalResource;
2958
}
2959
}
2960
2961