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