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