Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/model.ts
3314 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands, LogOutputChannel, l10n, ProgressLocation, WorkspaceFolder, ThemeIcon } from 'vscode';
7
import TelemetryReporter from '@vscode/extension-telemetry';
8
import { IRepositoryResolver, Repository, RepositoryState } from './repository';
9
import { memoize, sequentialize, debounce } from './decorators';
10
import { dispose, anyEvent, filterEvent, isDescendant, pathEquals, toDisposable, eventToPromise } from './util';
11
import { Git } from './git';
12
import * as path from 'path';
13
import * as fs from 'fs';
14
import { fromGitUri } from './uri';
15
import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider, SourceControlHistoryItemDetailsProvider } from './api/git';
16
import { Askpass } from './askpass';
17
import { IPushErrorHandlerRegistry } from './pushError';
18
import { ApiRepository } from './api/api1';
19
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
20
import { IPostCommitCommandsProviderRegistry } from './postCommitCommands';
21
import { IBranchProtectionProviderRegistry } from './branchProtection';
22
import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider';
23
24
class RepositoryPick implements QuickPickItem {
25
@memoize get label(): string {
26
return path.basename(this.repository.root);
27
}
28
29
@memoize get description(): string {
30
return [this.repository.headLabel, this.repository.syncLabel]
31
.filter(l => !!l)
32
.join(' ');
33
}
34
35
@memoize get iconPath(): ThemeIcon {
36
switch (this.repository.kind) {
37
case 'submodule':
38
return new ThemeIcon('archive');
39
case 'worktree':
40
return new ThemeIcon('list-tree');
41
default:
42
return new ThemeIcon('repo');
43
}
44
}
45
46
constructor(public readonly repository: Repository, public readonly index: number) { }
47
}
48
49
export interface ModelChangeEvent {
50
repository: Repository;
51
uri: Uri;
52
}
53
54
export interface OriginalResourceChangeEvent {
55
repository: Repository;
56
uri: Uri;
57
}
58
59
interface OpenRepository extends Disposable {
60
repository: Repository;
61
}
62
63
class ClosedRepositoriesManager {
64
65
private _repositories: Set<string>;
66
get repositories(): string[] {
67
return [...this._repositories.values()];
68
}
69
70
constructor(private readonly workspaceState: Memento) {
71
this._repositories = new Set<string>(workspaceState.get<string[]>('closedRepositories', []));
72
this.onDidChangeRepositories();
73
}
74
75
addRepository(repository: string): void {
76
this._repositories.add(repository);
77
this.onDidChangeRepositories();
78
}
79
80
deleteRepository(repository: string): boolean {
81
const result = this._repositories.delete(repository);
82
if (result) {
83
this.onDidChangeRepositories();
84
}
85
86
return result;
87
}
88
89
isRepositoryClosed(repository: string): boolean {
90
return this._repositories.has(repository);
91
}
92
93
private onDidChangeRepositories(): void {
94
this.workspaceState.update('closedRepositories', [...this._repositories.values()]);
95
commands.executeCommand('setContext', 'git.closedRepositoryCount', this._repositories.size);
96
}
97
}
98
99
class ParentRepositoriesManager {
100
101
/**
102
* Key - normalized path used in user interface
103
* Value - value indicating whether the repository should be opened
104
*/
105
private _repositories = new Set<string>;
106
get repositories(): string[] {
107
return [...this._repositories.values()];
108
}
109
110
constructor(private readonly globalState: Memento) {
111
this.onDidChangeRepositories();
112
}
113
114
addRepository(repository: string): void {
115
this._repositories.add(repository);
116
this.onDidChangeRepositories();
117
}
118
119
deleteRepository(repository: string): boolean {
120
const result = this._repositories.delete(repository);
121
if (result) {
122
this.onDidChangeRepositories();
123
}
124
125
return result;
126
}
127
128
hasRepository(repository: string): boolean {
129
return this._repositories.has(repository);
130
}
131
132
openRepository(repository: string): void {
133
this.globalState.update(`parentRepository:${repository}`, true);
134
this.deleteRepository(repository);
135
}
136
137
private onDidChangeRepositories(): void {
138
commands.executeCommand('setContext', 'git.parentRepositoryCount', this._repositories.size);
139
}
140
}
141
142
class UnsafeRepositoriesManager {
143
144
/**
145
* Key - normalized path used in user interface
146
* Value - path extracted from the output of the `git status` command
147
* used when calling `git config --global --add safe.directory`
148
*/
149
private _repositories = new Map<string, string>();
150
get repositories(): string[] {
151
return [...this._repositories.keys()];
152
}
153
154
constructor() {
155
this.onDidChangeRepositories();
156
}
157
158
addRepository(repository: string, path: string): void {
159
this._repositories.set(repository, path);
160
this.onDidChangeRepositories();
161
}
162
163
deleteRepository(repository: string): boolean {
164
const result = this._repositories.delete(repository);
165
if (result) {
166
this.onDidChangeRepositories();
167
}
168
169
return result;
170
}
171
172
getRepositoryPath(repository: string): string | undefined {
173
return this._repositories.get(repository);
174
}
175
176
hasRepository(repository: string): boolean {
177
return this._repositories.has(repository);
178
}
179
180
private onDidChangeRepositories(): void {
181
commands.executeCommand('setContext', 'git.unsafeRepositoryCount', this._repositories.size);
182
}
183
}
184
185
export class Model implements IRepositoryResolver, IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry, ISourceControlHistoryItemDetailsProviderRegistry {
186
187
private _onDidOpenRepository = new EventEmitter<Repository>();
188
readonly onDidOpenRepository: Event<Repository> = this._onDidOpenRepository.event;
189
190
private _onDidCloseRepository = new EventEmitter<Repository>();
191
readonly onDidCloseRepository: Event<Repository> = this._onDidCloseRepository.event;
192
193
private _onDidChangeRepository = new EventEmitter<ModelChangeEvent>();
194
readonly onDidChangeRepository: Event<ModelChangeEvent> = this._onDidChangeRepository.event;
195
196
private _onDidChangeOriginalResource = new EventEmitter<OriginalResourceChangeEvent>();
197
readonly onDidChangeOriginalResource: Event<OriginalResourceChangeEvent> = this._onDidChangeOriginalResource.event;
198
199
private openRepositories: OpenRepository[] = [];
200
get repositories(): Repository[] { return this.openRepositories.map(r => r.repository); }
201
202
private possibleGitRepositoryPaths = new Set<string>();
203
204
private _onDidChangeState = new EventEmitter<State>();
205
readonly onDidChangeState = this._onDidChangeState.event;
206
207
private _onDidPublish = new EventEmitter<PublishEvent>();
208
readonly onDidPublish = this._onDidPublish.event;
209
210
firePublishEvent(repository: Repository, branch?: string) {
211
this._onDidPublish.fire({ repository: new ApiRepository(repository), branch: branch });
212
}
213
214
private _state: State = 'uninitialized';
215
get state(): State { return this._state; }
216
217
setState(state: State): void {
218
this._state = state;
219
this._onDidChangeState.fire(state);
220
commands.executeCommand('setContext', 'git.state', state);
221
}
222
223
@memoize
224
get isInitialized(): Promise<void> {
225
if (this._state === 'initialized') {
226
return Promise.resolve();
227
}
228
229
return eventToPromise(filterEvent(this.onDidChangeState, s => s === 'initialized')) as Promise<any>;
230
}
231
232
private remoteSourcePublishers = new Set<RemoteSourcePublisher>();
233
234
private _onDidAddRemoteSourcePublisher = new EventEmitter<RemoteSourcePublisher>();
235
readonly onDidAddRemoteSourcePublisher = this._onDidAddRemoteSourcePublisher.event;
236
237
private _onDidRemoveRemoteSourcePublisher = new EventEmitter<RemoteSourcePublisher>();
238
readonly onDidRemoveRemoteSourcePublisher = this._onDidRemoveRemoteSourcePublisher.event;
239
240
private postCommitCommandsProviders = new Set<PostCommitCommandsProvider>();
241
242
private _onDidChangePostCommitCommandsProviders = new EventEmitter<void>();
243
readonly onDidChangePostCommitCommandsProviders = this._onDidChangePostCommitCommandsProviders.event;
244
245
private branchProtectionProviders = new Map<string, Set<BranchProtectionProvider>>();
246
247
private _onDidChangeBranchProtectionProviders = new EventEmitter<Uri>();
248
readonly onDidChangeBranchProtectionProviders = this._onDidChangeBranchProtectionProviders.event;
249
250
private pushErrorHandlers = new Set<PushErrorHandler>();
251
private historyItemDetailsProviders = new Set<SourceControlHistoryItemDetailsProvider>();
252
253
private _unsafeRepositoriesManager: UnsafeRepositoriesManager;
254
get unsafeRepositories(): string[] {
255
return this._unsafeRepositoriesManager.repositories;
256
}
257
258
private _parentRepositoriesManager: ParentRepositoriesManager;
259
get parentRepositories(): string[] {
260
return this._parentRepositoriesManager.repositories;
261
}
262
263
private _closedRepositoriesManager: ClosedRepositoriesManager;
264
get closedRepositories(): string[] {
265
return [...this._closedRepositoriesManager.repositories];
266
}
267
268
/**
269
* We maintain a map containing both the path and the canonical path of the
270
* workspace folders. We are doing this as `git.exe` expands the symbolic links
271
* while there are scenarios in which VS Code does not.
272
*
273
* Key - path of the workspace folder
274
* Value - canonical path of the workspace folder
275
*/
276
private _workspaceFolders = new Map<string, string>();
277
278
private disposables: Disposable[] = [];
279
280
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, readonly workspaceState: Memento, private logger: LogOutputChannel, private telemetryReporter: TelemetryReporter) {
281
// Repositories managers
282
this._closedRepositoriesManager = new ClosedRepositoriesManager(workspaceState);
283
this._parentRepositoriesManager = new ParentRepositoriesManager(globalState);
284
this._unsafeRepositoriesManager = new UnsafeRepositoriesManager();
285
286
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
287
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
288
window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, this.disposables);
289
workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables);
290
291
const fsWatcher = workspace.createFileSystemWatcher('**');
292
this.disposables.push(fsWatcher);
293
294
const onWorkspaceChange = anyEvent(fsWatcher.onDidChange, fsWatcher.onDidCreate, fsWatcher.onDidDelete);
295
const onGitRepositoryChange = filterEvent(onWorkspaceChange, uri => /\/\.git/.test(uri.path));
296
const onPossibleGitRepositoryChange = filterEvent(onGitRepositoryChange, uri => !this.getRepository(uri));
297
onPossibleGitRepositoryChange(this.onPossibleGitRepositoryChange, this, this.disposables);
298
299
this.setState('uninitialized');
300
this.doInitialScan().finally(() => this.setState('initialized'));
301
}
302
303
private async doInitialScan(): Promise<void> {
304
this.logger.info('[Model][doInitialScan] Initial repository scan started');
305
306
const config = workspace.getConfiguration('git');
307
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
308
const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt');
309
310
this.logger.trace(`[Model][doInitialScan] Settings: autoRepositoryDetection=${autoRepositoryDetection}, openRepositoryInParentFolders=${parentRepositoryConfig}`);
311
312
// Initial repository scan function
313
const initialScanFn = () => Promise.all([
314
this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] }),
315
this.onDidChangeVisibleTextEditors(window.visibleTextEditors),
316
this.scanWorkspaceFolders()
317
]);
318
319
if (config.get<boolean>('showProgress', true)) {
320
await window.withProgress({ location: ProgressLocation.SourceControl }, initialScanFn);
321
} else {
322
await initialScanFn();
323
}
324
325
if (this.parentRepositories.length !== 0 &&
326
parentRepositoryConfig === 'prompt') {
327
// Parent repositories notification
328
this.showParentRepositoryNotification();
329
} else if (this.unsafeRepositories.length !== 0) {
330
// Unsafe repositories notification
331
this.showUnsafeRepositoryNotification();
332
}
333
334
/* __GDPR__
335
"git.repositoryInitialScan" : {
336
"owner": "lszomoru",
337
"autoRepositoryDetection": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Setting that controls the initial repository scan" },
338
"repositoryCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of repositories opened during initial repository scan" }
339
}
340
*/
341
this.telemetryReporter.sendTelemetryEvent('git.repositoryInitialScan', { autoRepositoryDetection: String(autoRepositoryDetection) }, { repositoryCount: this.openRepositories.length });
342
this.logger.info(`[Model][doInitialScan] Initial repository scan completed - repositories (${this.repositories.length}), closed repositories (${this.closedRepositories.length}), parent repositories (${this.parentRepositories.length}), unsafe repositories (${this.unsafeRepositories.length})`);
343
}
344
345
/**
346
* Scans each workspace folder, looking for git repositories. By
347
* default it scans one level deep but that can be changed using
348
* the git.repositoryScanMaxDepth setting.
349
*/
350
private async scanWorkspaceFolders(): Promise<void> {
351
try {
352
const config = workspace.getConfiguration('git');
353
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
354
355
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') {
356
return;
357
}
358
359
await Promise.all((workspace.workspaceFolders || []).map(async folder => {
360
const root = folder.uri.fsPath;
361
this.logger.trace(`[Model][scanWorkspaceFolders] Workspace folder: ${root}`);
362
363
// Workspace folder children
364
const repositoryScanMaxDepth = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<number>('repositoryScanMaxDepth', 1);
365
const repositoryScanIgnoredFolders = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<string[]>('repositoryScanIgnoredFolders', []);
366
367
const subfolders = new Set(await this.traverseWorkspaceFolder(root, repositoryScanMaxDepth, repositoryScanIgnoredFolders));
368
369
// Repository scan folders
370
const scanPaths = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<string[]>('scanRepositories') || [];
371
this.logger.trace(`[Model][scanWorkspaceFolders] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`);
372
373
for (const scanPath of scanPaths) {
374
if (scanPath === '.git') {
375
this.logger.trace('[Model][scanWorkspaceFolders] \'.git\' not supported in \'git.scanRepositories\' setting.');
376
continue;
377
}
378
379
if (path.isAbsolute(scanPath)) {
380
const notSupportedMessage = l10n.t('Absolute paths not supported in "git.scanRepositories" setting.');
381
this.logger.warn(`[Model][scanWorkspaceFolders] ${notSupportedMessage}`);
382
console.warn(notSupportedMessage);
383
continue;
384
}
385
386
subfolders.add(path.join(root, scanPath));
387
}
388
389
this.logger.trace(`[Model][scanWorkspaceFolders] Workspace scan sub folders: [${[...subfolders].join(', ')}]`);
390
await Promise.all([...subfolders].map(f => this.openRepository(f)));
391
}));
392
}
393
catch (err) {
394
this.logger.warn(`[Model][scanWorkspaceFolders] Error: ${err}`);
395
}
396
}
397
398
private async traverseWorkspaceFolder(workspaceFolder: string, maxDepth: number, repositoryScanIgnoredFolders: string[]): Promise<string[]> {
399
const result: string[] = [];
400
const foldersToTravers = [{ path: workspaceFolder, depth: 0 }];
401
402
while (foldersToTravers.length > 0) {
403
const currentFolder = foldersToTravers.shift()!;
404
405
const children: fs.Dirent[] = [];
406
try {
407
children.push(...await fs.promises.readdir(currentFolder.path, { withFileTypes: true }));
408
409
if (currentFolder.depth !== 0) {
410
result.push(currentFolder.path);
411
}
412
}
413
catch (err) {
414
this.logger.warn(`[Model][traverseWorkspaceFolder] Unable to read workspace folder '${currentFolder.path}': ${err}`);
415
continue;
416
}
417
418
if (currentFolder.depth < maxDepth || maxDepth === -1) {
419
const childrenFolders = children
420
.filter(dirent =>
421
dirent.isDirectory() && dirent.name !== '.git' &&
422
!repositoryScanIgnoredFolders.find(f => pathEquals(dirent.name, f)))
423
.map(dirent => path.join(currentFolder.path, dirent.name));
424
425
foldersToTravers.push(...childrenFolders.map(folder => {
426
return { path: folder, depth: currentFolder.depth + 1 };
427
}));
428
}
429
}
430
431
return result;
432
}
433
434
private onPossibleGitRepositoryChange(uri: Uri): void {
435
const config = workspace.getConfiguration('git');
436
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
437
438
if (autoRepositoryDetection === false) {
439
return;
440
}
441
442
this.eventuallyScanPossibleGitRepository(uri.fsPath.replace(/\.git.*$/, ''));
443
}
444
445
private eventuallyScanPossibleGitRepository(path: string) {
446
this.possibleGitRepositoryPaths.add(path);
447
this.eventuallyScanPossibleGitRepositories();
448
}
449
450
@debounce(500)
451
private eventuallyScanPossibleGitRepositories(): void {
452
for (const path of this.possibleGitRepositoryPaths) {
453
this.openRepository(path, false, true);
454
}
455
456
this.possibleGitRepositoryPaths.clear();
457
}
458
459
private async onDidChangeWorkspaceFolders({ added, removed }: WorkspaceFoldersChangeEvent): Promise<void> {
460
try {
461
const possibleRepositoryFolders = added
462
.filter(folder => !this.getOpenRepository(folder.uri));
463
464
const activeRepositoriesList = window.visibleTextEditors
465
.map(editor => this.getRepository(editor.document.uri))
466
.filter(repository => !!repository) as Repository[];
467
468
const activeRepositories = new Set<Repository>(activeRepositoriesList);
469
const openRepositoriesToDispose = removed
470
.map(folder => this.getOpenRepository(folder.uri))
471
.filter(r => !!r)
472
.filter(r => !activeRepositories.has(r!.repository))
473
.filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[];
474
475
openRepositoriesToDispose.forEach(r => r.dispose());
476
this.logger.trace(`[Model][onDidChangeWorkspaceFolders] Workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
477
await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath)));
478
}
479
catch (err) {
480
this.logger.warn(`[Model][onDidChangeWorkspaceFolders] Error: ${err}`);
481
}
482
}
483
484
private onDidChangeConfiguration(): void {
485
const possibleRepositoryFolders = (workspace.workspaceFolders || [])
486
.filter(folder => workspace.getConfiguration('git', folder.uri).get<boolean>('enabled') === true)
487
.filter(folder => !this.getOpenRepository(folder.uri));
488
489
const openRepositoriesToDispose = this.openRepositories
490
.map(repository => ({ repository, root: Uri.file(repository.repository.root) }))
491
.filter(({ root }) => workspace.getConfiguration('git', root).get<boolean>('enabled') !== true)
492
.map(({ repository }) => repository);
493
494
this.logger.trace(`[Model][onDidChangeConfiguration] Workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
495
possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath));
496
openRepositoriesToDispose.forEach(r => r.dispose());
497
}
498
499
private async onDidChangeVisibleTextEditors(editors: readonly TextEditor[]): Promise<void> {
500
try {
501
if (!workspace.isTrusted) {
502
this.logger.trace('[Model][onDidChangeVisibleTextEditors] Workspace is not trusted.');
503
return;
504
}
505
506
const config = workspace.getConfiguration('git');
507
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
508
509
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') {
510
return;
511
}
512
513
await Promise.all(editors.map(async editor => {
514
const uri = editor.document.uri;
515
516
if (uri.scheme !== 'file') {
517
return;
518
}
519
520
const repository = this.getRepository(uri);
521
522
if (repository) {
523
this.logger.trace(`[Model][onDidChangeVisibleTextEditors] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`);
524
return;
525
}
526
527
this.logger.trace(`[Model][onDidChangeVisibleTextEditors] Open repository for editor resource ${uri.fsPath}`);
528
await this.openRepository(path.dirname(uri.fsPath));
529
}));
530
}
531
catch (err) {
532
this.logger.warn(`[Model][onDidChangeVisibleTextEditors] Error: ${err}`);
533
}
534
}
535
536
private onDidChangeActiveTextEditor(): void {
537
const textEditor = window.activeTextEditor;
538
539
if (textEditor === undefined) {
540
commands.executeCommand('setContext', 'git.activeResourceHasUnstagedChanges', false);
541
commands.executeCommand('setContext', 'git.activeResourceHasStagedChanges', false);
542
return;
543
}
544
545
const repository = this.getRepository(textEditor.document.uri);
546
if (!repository) {
547
commands.executeCommand('setContext', 'git.activeResourceHasUnstagedChanges', false);
548
commands.executeCommand('setContext', 'git.activeResourceHasStagedChanges', false);
549
return;
550
}
551
552
const indexResource = repository.indexGroup.resourceStates
553
.find(resource => pathEquals(resource.resourceUri.fsPath, textEditor.document.uri.fsPath));
554
const workingTreeResource = repository.workingTreeGroup.resourceStates
555
.find(resource => pathEquals(resource.resourceUri.fsPath, textEditor.document.uri.fsPath));
556
557
commands.executeCommand('setContext', 'git.activeResourceHasStagedChanges', indexResource !== undefined);
558
commands.executeCommand('setContext', 'git.activeResourceHasUnstagedChanges', workingTreeResource !== undefined);
559
}
560
561
@sequentialize
562
async openRepository(repoPath: string, openIfClosed = false, openIfParent = false): Promise<void> {
563
this.logger.trace(`[Model][openRepository] Repository: ${repoPath}`);
564
const existingRepository = await this.getRepositoryExact(repoPath);
565
if (existingRepository) {
566
this.logger.trace(`[Model][openRepository] Repository for path ${repoPath} already exists: ${existingRepository.root}`);
567
return;
568
}
569
570
const config = workspace.getConfiguration('git', Uri.file(repoPath));
571
const enabled = config.get<boolean>('enabled') === true;
572
573
if (!enabled) {
574
this.logger.trace('[Model][openRepository] Git is not enabled');
575
return;
576
}
577
578
if (!workspace.isTrusted) {
579
// Check if the folder is a bare repo: if it has a file named HEAD && `rev-parse --show -cdup` is empty
580
try {
581
fs.accessSync(path.join(repoPath, 'HEAD'), fs.constants.F_OK);
582
const result = await this.git.exec(repoPath, ['-C', repoPath, 'rev-parse', '--show-cdup']);
583
if (result.stderr.trim() === '' && result.stdout.trim() === '') {
584
this.logger.trace(`[Model][openRepository] Bare repository: ${repoPath}`);
585
return;
586
}
587
} catch {
588
// If this throw, we should be good to open the repo (e.g. HEAD doesn't exist)
589
}
590
}
591
592
try {
593
const { repositoryRoot, unsafeRepositoryMatch } = await this.getRepositoryRoot(repoPath);
594
this.logger.trace(`[Model][openRepository] Repository root for path ${repoPath} is: ${repositoryRoot}`);
595
596
const existingRepository = await this.getRepositoryExact(repositoryRoot);
597
if (existingRepository) {
598
this.logger.trace(`[Model][openRepository] Repository for path ${repositoryRoot} already exists: ${existingRepository.root}`);
599
return;
600
}
601
602
if (this.shouldRepositoryBeIgnored(repositoryRoot)) {
603
this.logger.trace(`[Model][openRepository] Repository for path ${repositoryRoot} is ignored`);
604
return;
605
}
606
607
// Handle git repositories that are in parent folders
608
const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt');
609
if (parentRepositoryConfig !== 'always' && this.globalState.get<boolean>(`parentRepository:${repositoryRoot}`) !== true) {
610
const isRepositoryOutsideWorkspace = await this.isRepositoryOutsideWorkspace(repositoryRoot);
611
if (!openIfParent && isRepositoryOutsideWorkspace) {
612
this.logger.trace(`[Model][openRepository] Repository in parent folder: ${repositoryRoot}`);
613
614
if (!this._parentRepositoriesManager.hasRepository(repositoryRoot)) {
615
// Show a notification if the parent repository is opened after the initial scan
616
if (this.state === 'initialized' && parentRepositoryConfig === 'prompt') {
617
this.showParentRepositoryNotification();
618
}
619
620
this._parentRepositoriesManager.addRepository(repositoryRoot);
621
}
622
623
return;
624
}
625
}
626
627
// Handle unsafe repositories
628
if (unsafeRepositoryMatch && unsafeRepositoryMatch.length === 3) {
629
this.logger.trace(`[Model][openRepository] Unsafe repository: ${repositoryRoot}`);
630
631
// Show a notification if the unsafe repository is opened after the initial scan
632
if (this._state === 'initialized' && !this._unsafeRepositoriesManager.hasRepository(repositoryRoot)) {
633
this.showUnsafeRepositoryNotification();
634
}
635
636
this._unsafeRepositoriesManager.addRepository(repositoryRoot, unsafeRepositoryMatch[2]);
637
638
return;
639
}
640
641
// Handle repositories that were closed by the user
642
if (!openIfClosed && this._closedRepositoriesManager.isRepositoryClosed(repositoryRoot)) {
643
this.logger.trace(`[Model][openRepository] Repository for path ${repositoryRoot} is closed`);
644
return;
645
}
646
647
// Open repository
648
const [dotGit, repositoryRootRealPath] = await Promise.all([this.git.getRepositoryDotGit(repositoryRoot), this.getRepositoryRootRealPath(repositoryRoot)]);
649
const gitRepository = this.git.open(repositoryRoot, repositoryRootRealPath, dotGit, this.logger);
650
const repository = new Repository(gitRepository, this, this, this, this, this, this, this.globalState, this.logger, this.telemetryReporter);
651
652
this.open(repository);
653
this._closedRepositoriesManager.deleteRepository(repository.root);
654
655
this.logger.info(`[Model][openRepository] Opened repository (path): ${repository.root}`);
656
this.logger.info(`[Model][openRepository] Opened repository (real path): ${repository.rootRealPath ?? repository.root}`);
657
this.logger.info(`[Model][openRepository] Opened repository (kind): ${gitRepository.kind}`);
658
659
// Do not await this, we want SCM
660
// to know about the repo asap
661
repository.status();
662
} catch (err) {
663
// noop
664
this.logger.trace(`[Model][openRepository] Opening repository for path='${repoPath}' failed. Error:${err}`);
665
}
666
}
667
668
async openParentRepository(repoPath: string): Promise<void> {
669
this._parentRepositoriesManager.openRepository(repoPath);
670
await this.openRepository(repoPath);
671
}
672
673
private async getRepositoryRoot(repoPath: string): Promise<{ repositoryRoot: string; unsafeRepositoryMatch: RegExpMatchArray | null }> {
674
try {
675
const rawRoot = await this.git.getRepositoryRoot(repoPath);
676
677
// This can happen whenever `path` has the wrong case sensitivity in case
678
// insensitive file systems https://github.com/microsoft/vscode/issues/33498
679
return { repositoryRoot: Uri.file(rawRoot).fsPath, unsafeRepositoryMatch: null };
680
} catch (err) {
681
// Handle unsafe repository
682
const unsafeRepositoryMatch = /^fatal: detected dubious ownership in repository at \'([^']+)\'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec(err.stderr);
683
if (unsafeRepositoryMatch && unsafeRepositoryMatch.length === 3) {
684
return { repositoryRoot: path.normalize(unsafeRepositoryMatch[1]), unsafeRepositoryMatch };
685
}
686
687
throw err;
688
}
689
}
690
691
private async getRepositoryRootRealPath(repositoryRoot: string): Promise<string | undefined> {
692
try {
693
const repositoryRootRealPath = await fs.promises.realpath(repositoryRoot);
694
return !pathEquals(repositoryRoot, repositoryRootRealPath) ? repositoryRootRealPath : undefined;
695
} catch (err) {
696
this.logger.warn(`[Model][getRepositoryRootRealPath] Failed to get repository realpath for "${repositoryRoot}": ${err}`);
697
return undefined;
698
}
699
}
700
701
private shouldRepositoryBeIgnored(repositoryRoot: string): boolean {
702
const config = workspace.getConfiguration('git');
703
const ignoredRepos = config.get<string[]>('ignoredRepositories') || [];
704
705
for (const ignoredRepo of ignoredRepos) {
706
if (path.isAbsolute(ignoredRepo)) {
707
if (pathEquals(ignoredRepo, repositoryRoot)) {
708
return true;
709
}
710
} else {
711
for (const folder of workspace.workspaceFolders || []) {
712
if (pathEquals(path.join(folder.uri.fsPath, ignoredRepo), repositoryRoot)) {
713
return true;
714
}
715
}
716
}
717
}
718
719
return false;
720
}
721
722
private open(repository: Repository): void {
723
this.logger.trace(`[Model][open] Repository: ${repository.root}`);
724
725
const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed);
726
const disappearListener = onDidDisappearRepository(() => dispose());
727
const disposeParentListener = repository.sourceControl.onDidDisposeParent(() => dispose());
728
const changeListener = repository.onDidChangeRepository(uri => this._onDidChangeRepository.fire({ repository, uri }));
729
const originalResourceChangeListener = repository.onDidChangeOriginalResource(uri => this._onDidChangeOriginalResource.fire({ repository, uri }));
730
731
const shouldDetectSubmodules = workspace
732
.getConfiguration('git', Uri.file(repository.root))
733
.get<boolean>('detectSubmodules') as boolean;
734
735
const submodulesLimit = workspace
736
.getConfiguration('git', Uri.file(repository.root))
737
.get<number>('detectSubmodulesLimit') as number;
738
739
const shouldDetectWorktrees = workspace
740
.getConfiguration('git', Uri.file(repository.root))
741
.get<boolean>('detectWorktrees') as boolean;
742
743
const worktreesLimit = workspace
744
.getConfiguration('git', Uri.file(repository.root))
745
.get<number>('detectWorktreesLimit') as number;
746
747
const checkForSubmodules = () => {
748
if (!shouldDetectSubmodules) {
749
this.logger.trace('[Model][open] Automatic detection of git submodules is not enabled.');
750
return;
751
}
752
753
if (repository.submodules.length > submodulesLimit) {
754
window.showWarningMessage(l10n.t('The "{0}" repository has {1} submodules which won\'t be opened automatically. You can still open each one individually by opening a file within.', path.basename(repository.root), repository.submodules.length));
755
statusListener.dispose();
756
}
757
758
repository.submodules
759
.slice(0, submodulesLimit)
760
.map(r => path.join(repository.root, r.path))
761
.forEach(p => {
762
this.logger.trace(`[Model][open] Opening submodule: '${p}'`);
763
this.eventuallyScanPossibleGitRepository(p);
764
});
765
};
766
767
const checkForWorktrees = () => {
768
if (!shouldDetectWorktrees) {
769
this.logger.trace('[Model][open] Automatic detection of git worktrees is not enabled.');
770
return;
771
}
772
773
if (repository.kind === 'worktree') {
774
this.logger.trace('[Model][open] Automatic detection of git worktrees is not skipped.');
775
return;
776
}
777
778
if (repository.worktrees.length > worktreesLimit) {
779
window.showWarningMessage(l10n.t('The "{0}" repository has {1} worktrees which won\'t be opened automatically. You can still open each one individually by opening a file within.', path.basename(repository.root), repository.worktrees.length));
780
statusListener.dispose();
781
}
782
783
repository.worktrees
784
.slice(0, worktreesLimit)
785
.forEach(w => {
786
this.logger.trace(`[Model][open] Opening worktree: '${w.path}'`);
787
this.eventuallyScanPossibleGitRepository(w.path);
788
});
789
};
790
791
const updateMergeChanges = () => {
792
// set mergeChanges context
793
const mergeChanges: Uri[] = [];
794
for (const { repository } of this.openRepositories.values()) {
795
for (const state of repository.mergeGroup.resourceStates) {
796
mergeChanges.push(state.resourceUri);
797
}
798
}
799
commands.executeCommand('setContext', 'git.mergeChanges', mergeChanges);
800
};
801
802
const statusListener = repository.onDidRunGitStatus(() => {
803
checkForSubmodules();
804
checkForWorktrees();
805
updateMergeChanges();
806
this.onDidChangeActiveTextEditor();
807
});
808
checkForSubmodules();
809
checkForWorktrees();
810
this.onDidChangeActiveTextEditor();
811
812
const updateOperationInProgressContext = () => {
813
let operationInProgress = false;
814
for (const { repository } of this.openRepositories.values()) {
815
if (repository.operations.shouldDisableCommands()) {
816
operationInProgress = true;
817
}
818
}
819
820
commands.executeCommand('setContext', 'operationInProgress', operationInProgress);
821
};
822
823
const operationEvent = anyEvent(repository.onDidRunOperation as Event<any>, repository.onRunOperation as Event<any>);
824
const operationListener = operationEvent(() => updateOperationInProgressContext());
825
updateOperationInProgressContext();
826
827
const dispose = () => {
828
disappearListener.dispose();
829
disposeParentListener.dispose();
830
changeListener.dispose();
831
originalResourceChangeListener.dispose();
832
statusListener.dispose();
833
operationListener.dispose();
834
repository.dispose();
835
836
this.openRepositories = this.openRepositories.filter(e => e !== openRepository);
837
this._onDidCloseRepository.fire(repository);
838
};
839
840
const openRepository = { repository, dispose };
841
this.openRepositories.push(openRepository);
842
updateMergeChanges();
843
this._onDidOpenRepository.fire(repository);
844
}
845
846
close(repository: Repository): void {
847
const openRepository = this.getOpenRepository(repository);
848
849
if (!openRepository) {
850
return;
851
}
852
853
this.logger.info(`[Model][close] Repository: ${repository.root}`);
854
this._closedRepositoriesManager.addRepository(openRepository.repository.root);
855
856
openRepository.dispose();
857
}
858
859
async pickRepository(repositoryFilter?: ('repository' | 'submodule' | 'worktree')[]): Promise<Repository | undefined> {
860
if (this.openRepositories.length === 0) {
861
throw new Error(l10n.t('There are no available repositories'));
862
}
863
864
const repositories = this.openRepositories
865
.filter(r => !repositoryFilter || repositoryFilter.includes(r.repository.kind));
866
867
if (repositories.length === 0) {
868
throw new Error(l10n.t('There are no available repositories matching the filter'));
869
} else if (repositories.length === 1) {
870
return repositories[0].repository;
871
}
872
873
const active = window.activeTextEditor;
874
const picks = repositories.map((e, index) => new RepositoryPick(e.repository, index));
875
const repository = active && this.getRepository(active.document.fileName);
876
const index = picks.findIndex(pick => pick.repository === repository);
877
878
// Move repository pick containing the active text editor to appear first
879
if (index > -1) {
880
picks.unshift(...picks.splice(index, 1));
881
}
882
883
const placeHolder = l10n.t('Choose a repository');
884
const pick = await window.showQuickPick(picks, { placeHolder });
885
886
return pick && pick.repository;
887
}
888
889
getRepository(sourceControl: SourceControl): Repository | undefined;
890
getRepository(resourceGroup: SourceControlResourceGroup): Repository | undefined;
891
getRepository(path: string): Repository | undefined;
892
getRepository(resource: Uri): Repository | undefined;
893
getRepository(hint: any): Repository | undefined {
894
const liveRepository = this.getOpenRepository(hint);
895
return liveRepository && liveRepository.repository;
896
}
897
898
private async getRepositoryExact(repoPath: string): Promise<Repository | undefined> {
899
// Use the repository path
900
const openRepository = this.openRepositories
901
.find(r => pathEquals(r.repository.root, repoPath));
902
903
if (openRepository) {
904
return openRepository.repository;
905
}
906
907
try {
908
// Use the repository real path
909
const repoPathRealPath = await fs.promises.realpath(repoPath, { encoding: 'utf8' });
910
const openRepositoryRealPath = this.openRepositories
911
.find(r => pathEquals(r.repository.rootRealPath ?? r.repository.root, repoPathRealPath));
912
913
return openRepositoryRealPath?.repository;
914
} catch (err) {
915
this.logger.warn(`[Model][getRepositoryExact] Failed to get repository realpath for: "${repoPath}". Error:${err}`);
916
return undefined;
917
}
918
}
919
920
private getOpenRepository(repository: Repository): OpenRepository | undefined;
921
private getOpenRepository(sourceControl: SourceControl): OpenRepository | undefined;
922
private getOpenRepository(resourceGroup: SourceControlResourceGroup): OpenRepository | undefined;
923
private getOpenRepository(path: string): OpenRepository | undefined;
924
private getOpenRepository(resource: Uri): OpenRepository | undefined;
925
private getOpenRepository(hint: any): OpenRepository | undefined {
926
if (!hint) {
927
return undefined;
928
}
929
930
if (hint instanceof Repository) {
931
return this.openRepositories.filter(r => r.repository === hint)[0];
932
}
933
934
if (hint instanceof ApiRepository) {
935
hint = hint.rootUri;
936
}
937
938
if (typeof hint === 'string') {
939
hint = Uri.file(hint);
940
}
941
942
if (hint instanceof Uri) {
943
let resourcePath: string;
944
945
if (hint.scheme === 'git') {
946
resourcePath = fromGitUri(hint).path;
947
} else {
948
resourcePath = hint.fsPath;
949
}
950
951
outer:
952
for (const liveRepository of this.openRepositories.sort((a, b) => b.repository.root.length - a.repository.root.length)) {
953
if (!isDescendant(liveRepository.repository.root, resourcePath)) {
954
continue;
955
}
956
957
for (const submodule of liveRepository.repository.submodules) {
958
const submoduleRoot = path.join(liveRepository.repository.root, submodule.path);
959
960
if (isDescendant(submoduleRoot, resourcePath)) {
961
continue outer;
962
}
963
}
964
965
return liveRepository;
966
}
967
968
return undefined;
969
}
970
971
for (const liveRepository of this.openRepositories) {
972
const repository = liveRepository.repository;
973
974
if (hint === repository.sourceControl) {
975
return liveRepository;
976
}
977
978
if (hint === repository.mergeGroup || hint === repository.indexGroup || hint === repository.workingTreeGroup || hint === repository.untrackedGroup) {
979
return liveRepository;
980
}
981
}
982
983
return undefined;
984
}
985
986
getRepositoryForSubmodule(submoduleUri: Uri): Repository | undefined {
987
for (const repository of this.repositories) {
988
for (const submodule of repository.submodules) {
989
const submodulePath = path.join(repository.root, submodule.path);
990
991
if (submodulePath === submoduleUri.fsPath) {
992
return repository;
993
}
994
}
995
}
996
997
return undefined;
998
}
999
1000
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable {
1001
this.remoteSourcePublishers.add(publisher);
1002
this._onDidAddRemoteSourcePublisher.fire(publisher);
1003
1004
return toDisposable(() => {
1005
this.remoteSourcePublishers.delete(publisher);
1006
this._onDidRemoveRemoteSourcePublisher.fire(publisher);
1007
});
1008
}
1009
1010
getRemoteSourcePublishers(): RemoteSourcePublisher[] {
1011
return [...this.remoteSourcePublishers.values()];
1012
}
1013
1014
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable {
1015
const providerDisposables: Disposable[] = [];
1016
1017
this.branchProtectionProviders.set(root.toString(), (this.branchProtectionProviders.get(root.toString()) ?? new Set()).add(provider));
1018
providerDisposables.push(provider.onDidChangeBranchProtection(uri => this._onDidChangeBranchProtectionProviders.fire(uri)));
1019
1020
this._onDidChangeBranchProtectionProviders.fire(root);
1021
1022
return toDisposable(() => {
1023
const providers = this.branchProtectionProviders.get(root.toString());
1024
1025
if (providers && providers.has(provider)) {
1026
providers.delete(provider);
1027
this.branchProtectionProviders.set(root.toString(), providers);
1028
this._onDidChangeBranchProtectionProviders.fire(root);
1029
}
1030
1031
dispose(providerDisposables);
1032
});
1033
}
1034
1035
getBranchProtectionProviders(root: Uri): BranchProtectionProvider[] {
1036
return [...(this.branchProtectionProviders.get(root.toString()) ?? new Set()).values()];
1037
}
1038
1039
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable {
1040
this.postCommitCommandsProviders.add(provider);
1041
this._onDidChangePostCommitCommandsProviders.fire();
1042
1043
return toDisposable(() => {
1044
this.postCommitCommandsProviders.delete(provider);
1045
this._onDidChangePostCommitCommandsProviders.fire();
1046
});
1047
}
1048
1049
getPostCommitCommandsProviders(): PostCommitCommandsProvider[] {
1050
return [...this.postCommitCommandsProviders.values()];
1051
}
1052
1053
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
1054
return this.askpass.registerCredentialsProvider(provider);
1055
}
1056
1057
registerPushErrorHandler(handler: PushErrorHandler): Disposable {
1058
this.pushErrorHandlers.add(handler);
1059
return toDisposable(() => this.pushErrorHandlers.delete(handler));
1060
}
1061
1062
getPushErrorHandlers(): PushErrorHandler[] {
1063
return [...this.pushErrorHandlers];
1064
}
1065
1066
registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable {
1067
this.historyItemDetailsProviders.add(provider);
1068
return toDisposable(() => this.historyItemDetailsProviders.delete(provider));
1069
}
1070
1071
getSourceControlHistoryItemDetailsProviders(): SourceControlHistoryItemDetailsProvider[] {
1072
return [...this.historyItemDetailsProviders];
1073
}
1074
1075
getUnsafeRepositoryPath(repository: string): string | undefined {
1076
return this._unsafeRepositoriesManager.getRepositoryPath(repository);
1077
}
1078
1079
deleteUnsafeRepository(repository: string): boolean {
1080
return this._unsafeRepositoriesManager.deleteRepository(repository);
1081
}
1082
1083
private async isRepositoryOutsideWorkspace(repositoryPath: string): Promise<boolean> {
1084
const workspaceFolders = (workspace.workspaceFolders || [])
1085
.filter(folder => folder.uri.scheme === 'file');
1086
1087
if (workspaceFolders.length === 0) {
1088
return true;
1089
}
1090
1091
// The repository path may be a canonical path or it may contain a symbolic link so we have
1092
// to match it against the workspace folders and the canonical paths of the workspace folders
1093
const workspaceFolderPaths = new Set<string | undefined>([
1094
...workspaceFolders.map(folder => folder.uri.fsPath),
1095
...await Promise.all(workspaceFolders.map(folder => this.getWorkspaceFolderRealPath(folder)))
1096
]);
1097
1098
return !Array.from(workspaceFolderPaths).some(folder => folder && (pathEquals(folder, repositoryPath) || isDescendant(folder, repositoryPath)));
1099
}
1100
1101
private async getWorkspaceFolderRealPath(workspaceFolder: WorkspaceFolder): Promise<string | undefined> {
1102
let result = this._workspaceFolders.get(workspaceFolder.uri.fsPath);
1103
1104
if (!result) {
1105
try {
1106
result = await fs.promises.realpath(workspaceFolder.uri.fsPath, { encoding: 'utf8' });
1107
this._workspaceFolders.set(workspaceFolder.uri.fsPath, result);
1108
} catch (err) {
1109
// noop - Workspace folder does not exist
1110
this.logger.trace(`[Model][getWorkspaceFolderRealPath] Failed to resolve workspace folder "${workspaceFolder.uri.fsPath}". Error:${err}`);
1111
}
1112
}
1113
1114
return result;
1115
}
1116
1117
private async showParentRepositoryNotification(): Promise<void> {
1118
const message = this.parentRepositories.length === 1 ?
1119
l10n.t('A git repository was found in the parent folders of the workspace or the open file(s). Would you like to open the repository?') :
1120
l10n.t('Git repositories were found in the parent folders of the workspace or the open file(s). Would you like to open the repositories?');
1121
1122
const yes = l10n.t('Yes');
1123
const always = l10n.t('Always');
1124
const never = l10n.t('Never');
1125
1126
const choice = await window.showInformationMessage(message, yes, always, never);
1127
if (choice === yes) {
1128
// Open Parent Repositories
1129
commands.executeCommand('git.openRepositoriesInParentFolders');
1130
} else if (choice === always || choice === never) {
1131
// Update setting
1132
const config = workspace.getConfiguration('git');
1133
await config.update('openRepositoryInParentFolders', choice === always ? 'always' : 'never', true);
1134
1135
if (choice === always) {
1136
for (const parentRepository of this.parentRepositories) {
1137
await this.openParentRepository(parentRepository);
1138
}
1139
}
1140
}
1141
}
1142
1143
private async showUnsafeRepositoryNotification(): Promise<void> {
1144
// If no repositories are open, we will use a welcome view to inform the user
1145
// that a potentially unsafe repository was found so we do not have to show
1146
// the notification
1147
if (this.repositories.length === 0) {
1148
return;
1149
}
1150
1151
const message = this.unsafeRepositories.length === 1 ?
1152
l10n.t('The git repository in the current folder is potentially unsafe as the folder is owned by someone other than the current user.') :
1153
l10n.t('The git repositories in the current folder are potentially unsafe as the folders are owned by someone other than the current user.');
1154
1155
const manageUnsafeRepositories = l10n.t('Manage Unsafe Repositories');
1156
const learnMore = l10n.t('Learn More');
1157
1158
const choice = await window.showErrorMessage(message, manageUnsafeRepositories, learnMore);
1159
if (choice === manageUnsafeRepositories) {
1160
// Manage Unsafe Repositories
1161
commands.executeCommand('git.manageUnsafeRepositories');
1162
} else if (choice === learnMore) {
1163
// Learn More
1164
commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-git-unsafe-repository'));
1165
}
1166
}
1167
1168
disposeRepository(repository: Repository): void {
1169
const openRepository = this.getOpenRepository(repository);
1170
if (!openRepository) {
1171
return;
1172
}
1173
1174
this.logger.info(`[Model][disposeRepository] Repository: ${repository.root}`);
1175
openRepository.dispose();
1176
}
1177
1178
dispose(): void {
1179
const openRepositories = [...this.openRepositories];
1180
openRepositories.forEach(r => r.dispose());
1181
this.openRepositories = [];
1182
1183
this.possibleGitRepositoryPaths.clear();
1184
this.disposables = dispose(this.disposables);
1185
}
1186
}
1187
1188