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