Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/repositoryCache.ts
4772 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 { LogOutputChannel, Memento, workspace } from 'vscode';
7
import { LRUCache } from './cache';
8
import { Remote } from './api/git';
9
import { isDescendant } from './util';
10
11
export interface RepositoryCacheInfo {
12
workspacePath: string; // path of the workspace folder or workspace file
13
}
14
15
function isRepositoryCacheInfo(obj: unknown): obj is RepositoryCacheInfo {
16
if (!obj || typeof obj !== 'object') {
17
return false;
18
}
19
const rec = obj as Record<string, unknown>;
20
return typeof rec.workspacePath === 'string';
21
}
22
23
export class RepositoryCache {
24
25
private static readonly STORAGE_KEY = 'git.repositoryCache';
26
private static readonly MAX_REPO_ENTRIES = 30; // Max repositories tracked
27
private static readonly MAX_FOLDER_ENTRIES = 10; // Max folders per repository
28
29
private normalizeRepoUrl(url: string): string {
30
try {
31
const trimmed = url.trim();
32
return trimmed.replace(/(?:\.git)?\/*$/i, '');
33
} catch {
34
return url;
35
}
36
}
37
38
// Outer LRU: repoUrl -> inner LRU (folderPathOrWorkspaceFile -> RepositoryCacheInfo).
39
private readonly lru = new LRUCache<string, LRUCache<string, RepositoryCacheInfo>>(RepositoryCache.MAX_REPO_ENTRIES);
40
41
constructor(public readonly _globalState: Memento, private readonly _logger: LogOutputChannel) {
42
this.load();
43
}
44
45
// Exposed for testing
46
protected get _workspaceFile() {
47
return workspace.workspaceFile;
48
}
49
50
// Exposed for testing
51
protected get _workspaceFolders() {
52
return workspace.workspaceFolders;
53
}
54
55
/**
56
* Associate a repository remote URL with a local workspace folder or workspace file.
57
* Re-associating bumps recency and persists the updated LRU state.
58
* @param repoUrl Remote repository URL (e.g. https://github.com/owner/repo.git)
59
* @param rootPath Root path of the local repo clone.
60
*/
61
set(repoUrl: string, rootPath: string): void {
62
const key = this.normalizeRepoUrl(repoUrl);
63
let foldersLru = this.lru.get(key);
64
if (!foldersLru) {
65
foldersLru = new LRUCache<string, RepositoryCacheInfo>(RepositoryCache.MAX_FOLDER_ENTRIES);
66
}
67
const folderPathOrWorkspaceFile: string | undefined = this._findWorkspaceForRepo(rootPath);
68
if (!folderPathOrWorkspaceFile) {
69
return;
70
}
71
72
foldersLru.set(folderPathOrWorkspaceFile, {
73
workspacePath: folderPathOrWorkspaceFile
74
}); // touch entry
75
this.lru.set(key, foldersLru);
76
this.save();
77
}
78
79
private _findWorkspaceForRepo(rootPath: string): string | undefined {
80
// If the current workspace is a workspace file, use that. Otherwise, find the workspace folder that contains the rootUri
81
let folderPathOrWorkspaceFile: string | undefined;
82
try {
83
if (this._workspaceFile) {
84
folderPathOrWorkspaceFile = this._workspaceFile.fsPath;
85
} else if (this._workspaceFolders && this._workspaceFolders.length) {
86
const sorted = [...this._workspaceFolders].sort((a, b) => b.uri.fsPath.length - a.uri.fsPath.length);
87
for (const folder of sorted) {
88
const folderPath = folder.uri.fsPath;
89
if (isDescendant(folderPath, rootPath) || isDescendant(rootPath, folderPath)) {
90
folderPathOrWorkspaceFile = folderPath;
91
break;
92
}
93
}
94
}
95
return folderPathOrWorkspaceFile;
96
} catch {
97
return;
98
}
99
100
}
101
102
update(addedRemotes: Remote[], removedRemotes: Remote[], rootPath: string): void {
103
for (const remote of removedRemotes) {
104
const url = remote.fetchUrl;
105
if (!url) {
106
continue;
107
}
108
const relatedWorkspace = this._findWorkspaceForRepo(rootPath);
109
if (relatedWorkspace) {
110
this.delete(url, relatedWorkspace);
111
}
112
}
113
114
for (const remote of addedRemotes) {
115
const url = remote.fetchUrl;
116
if (!url) {
117
continue;
118
}
119
this.set(url, rootPath);
120
}
121
}
122
123
/**
124
* We should possibly support converting between ssh remotes and http remotes.
125
*/
126
get(repoUrl: string): RepositoryCacheInfo[] | undefined {
127
const key = this.normalizeRepoUrl(repoUrl);
128
const inner = this.lru.get(key);
129
return inner ? Array.from(inner.values()) : undefined;
130
}
131
132
delete(repoUrl: string, folderPathOrWorkspaceFile: string) {
133
const key = this.normalizeRepoUrl(repoUrl);
134
const inner = this.lru.get(key);
135
if (!inner) {
136
return;
137
}
138
if (!inner.remove(folderPathOrWorkspaceFile)) {
139
return;
140
}
141
if (inner.size === 0) {
142
this.lru.remove(key);
143
} else {
144
// Re-set to bump outer LRU recency after modification
145
this.lru.set(key, inner);
146
}
147
this.save();
148
}
149
150
private load(): void {
151
try {
152
const raw = this._globalState.get<[string, [string, RepositoryCacheInfo][]][]>(RepositoryCache.STORAGE_KEY);
153
if (!Array.isArray(raw)) {
154
return;
155
}
156
for (const [repo, storedFolders] of raw) {
157
if (typeof repo !== 'string' || !Array.isArray(storedFolders)) {
158
continue;
159
}
160
const inner = new LRUCache<string, RepositoryCacheInfo>(RepositoryCache.MAX_FOLDER_ENTRIES);
161
for (const entry of storedFolders) {
162
if (!Array.isArray(entry) || entry.length !== 2) {
163
continue;
164
}
165
const [folderPath, info] = entry;
166
if (typeof folderPath !== 'string' || !isRepositoryCacheInfo(info)) {
167
continue;
168
}
169
170
inner.set(folderPath, info);
171
}
172
if (inner.size) {
173
this.lru.set(repo, inner);
174
}
175
}
176
177
} catch {
178
this._logger.warn('[CachedRepositories][load] Failed to load cached repositories from global state.');
179
}
180
}
181
182
private save(): void {
183
// Serialize as [repoUrl, [folderPathOrWorkspaceFile, RepositoryCacheInfo][]] preserving outer LRU order.
184
const serialized: [string, [string, RepositoryCacheInfo][]][] = [];
185
for (const [repo, inner] of this.lru) {
186
const folders: [string, RepositoryCacheInfo][] = [];
187
for (const [folder, info] of inner) {
188
folders.push([folder, info]);
189
}
190
serialized.push([repo, folders]);
191
}
192
void this._globalState.update(RepositoryCache.STORAGE_KEY, serialized);
193
}
194
}
195
196