Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/fileSystemProvider.ts
3316 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, Uri, Disposable, Event, EventEmitter, window, FileSystemProvider, FileChangeEvent, FileStat, FileType, FileChangeType, FileSystemError, LogOutputChannel } from 'vscode';
7
import { debounce, throttle } from './decorators';
8
import { fromGitUri, toGitUri } from './uri';
9
import { Model, ModelChangeEvent, OriginalResourceChangeEvent } from './model';
10
import { filterEvent, eventToPromise, isDescendant, pathEquals, EmptyDisposable } from './util';
11
import { Repository } from './repository';
12
13
interface CacheRow {
14
uri: Uri;
15
timestamp: number;
16
}
17
18
const THREE_MINUTES = 1000 * 60 * 3;
19
const FIVE_MINUTES = 1000 * 60 * 5;
20
21
function sanitizeRef(ref: string, path: string, submoduleOf: string | undefined, repository: Repository): string {
22
if (ref === '~') {
23
const fileUri = Uri.file(path);
24
const uriString = fileUri.toString();
25
const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
26
return indexStatus ? '' : 'HEAD';
27
}
28
29
if (/^~\d$/.test(ref)) {
30
return `:${ref[1]}`;
31
}
32
33
// Submodule HEAD
34
if (submoduleOf && (ref === 'index' || ref === 'wt')) {
35
return 'HEAD';
36
}
37
38
return ref;
39
}
40
41
export class GitFileSystemProvider implements FileSystemProvider {
42
43
private _onDidChangeFile = new EventEmitter<FileChangeEvent[]>();
44
readonly onDidChangeFile: Event<FileChangeEvent[]> = this._onDidChangeFile.event;
45
46
private changedRepositoryRoots = new Set<string>();
47
private cache = new Map<string, CacheRow>();
48
private mtime = new Date().getTime();
49
private disposables: Disposable[] = [];
50
51
constructor(private readonly model: Model, private readonly logger: LogOutputChannel) {
52
this.disposables.push(
53
model.onDidChangeRepository(this.onDidChangeRepository, this),
54
model.onDidChangeOriginalResource(this.onDidChangeOriginalResource, this),
55
workspace.registerFileSystemProvider('git', this, { isReadonly: true, isCaseSensitive: true }),
56
);
57
58
setInterval(() => this.cleanup(), FIVE_MINUTES);
59
}
60
61
private onDidChangeRepository({ repository }: ModelChangeEvent): void {
62
this.changedRepositoryRoots.add(repository.root);
63
this.eventuallyFireChangeEvents();
64
}
65
66
private onDidChangeOriginalResource({ uri }: OriginalResourceChangeEvent): void {
67
if (uri.scheme !== 'file') {
68
return;
69
}
70
71
const diffOriginalResourceUri = toGitUri(uri, '~',);
72
const quickDiffOriginalResourceUri = toGitUri(uri, '', { replaceFileExtension: true });
73
74
this.mtime = new Date().getTime();
75
this._onDidChangeFile.fire([
76
{ type: FileChangeType.Changed, uri: diffOriginalResourceUri },
77
{ type: FileChangeType.Changed, uri: quickDiffOriginalResourceUri }
78
]);
79
}
80
81
@debounce(1100)
82
private eventuallyFireChangeEvents(): void {
83
this.fireChangeEvents();
84
}
85
86
@throttle
87
private async fireChangeEvents(): Promise<void> {
88
if (!window.state.focused) {
89
const onDidFocusWindow = filterEvent(window.onDidChangeWindowState, e => e.focused);
90
await eventToPromise(onDidFocusWindow);
91
}
92
93
const events: FileChangeEvent[] = [];
94
95
for (const { uri } of this.cache.values()) {
96
const fsPath = uri.fsPath;
97
98
for (const root of this.changedRepositoryRoots) {
99
if (isDescendant(root, fsPath)) {
100
events.push({ type: FileChangeType.Changed, uri });
101
break;
102
}
103
}
104
}
105
106
if (events.length > 0) {
107
this.mtime = new Date().getTime();
108
this._onDidChangeFile.fire(events);
109
}
110
111
this.changedRepositoryRoots.clear();
112
}
113
114
private cleanup(): void {
115
const now = new Date().getTime();
116
const cache = new Map<string, CacheRow>();
117
118
for (const row of this.cache.values()) {
119
const { path } = fromGitUri(row.uri);
120
const isOpen = workspace.textDocuments
121
.filter(d => d.uri.scheme === 'file')
122
.some(d => pathEquals(d.uri.fsPath, path));
123
124
if (isOpen || now - row.timestamp < THREE_MINUTES) {
125
cache.set(row.uri.toString(), row);
126
} else {
127
// TODO: should fire delete events?
128
}
129
}
130
131
this.cache = cache;
132
}
133
134
watch(): Disposable {
135
return EmptyDisposable;
136
}
137
138
async stat(uri: Uri): Promise<FileStat> {
139
await this.model.isInitialized;
140
141
const { submoduleOf, path, ref } = fromGitUri(uri);
142
const repository = submoduleOf ? this.model.getRepository(submoduleOf) : this.model.getRepository(uri);
143
if (!repository) {
144
this.logger.warn(`[GitFileSystemProvider][stat] Repository not found - ${uri.toString()}`);
145
throw FileSystemError.FileNotFound();
146
}
147
148
try {
149
const details = await repository.getObjectDetails(sanitizeRef(ref, path, submoduleOf, repository), path);
150
return { type: FileType.File, size: details.size, mtime: this.mtime, ctime: 0 };
151
} catch {
152
// Empty tree
153
if (ref === await repository.getEmptyTree()) {
154
this.logger.warn(`[GitFileSystemProvider][stat] Empty tree - ${uri.toString()}`);
155
return { type: FileType.File, size: 0, mtime: this.mtime, ctime: 0 };
156
}
157
158
// File does not exist in git. This could be because the file is untracked or ignored
159
this.logger.warn(`[GitFileSystemProvider][stat] File not found - ${uri.toString()}`);
160
throw FileSystemError.FileNotFound();
161
}
162
}
163
164
readDirectory(): Thenable<[string, FileType][]> {
165
throw new Error('Method not implemented.');
166
}
167
168
createDirectory(): void {
169
throw new Error('Method not implemented.');
170
}
171
172
async readFile(uri: Uri): Promise<Uint8Array> {
173
await this.model.isInitialized;
174
175
const { path, ref, submoduleOf } = fromGitUri(uri);
176
177
if (submoduleOf) {
178
const repository = this.model.getRepository(submoduleOf);
179
180
if (!repository) {
181
throw FileSystemError.FileNotFound();
182
}
183
184
const encoder = new TextEncoder();
185
186
if (ref === 'index') {
187
return encoder.encode(await repository.diffIndexWithHEAD(path));
188
} else {
189
return encoder.encode(await repository.diffWithHEAD(path));
190
}
191
}
192
193
const repository = this.model.getRepository(uri);
194
195
if (!repository) {
196
this.logger.warn(`[GitFileSystemProvider][readFile] Repository not found - ${uri.toString()}`);
197
throw FileSystemError.FileNotFound();
198
}
199
200
const timestamp = new Date().getTime();
201
const cacheValue: CacheRow = { uri, timestamp };
202
203
this.cache.set(uri.toString(), cacheValue);
204
205
try {
206
return await repository.buffer(sanitizeRef(ref, path, submoduleOf, repository), path);
207
} catch {
208
// Empty tree
209
if (ref === await repository.getEmptyTree()) {
210
this.logger.warn(`[GitFileSystemProvider][readFile] Empty tree - ${uri.toString()}`);
211
return new Uint8Array(0);
212
}
213
214
// File does not exist in git. This could be because the file is untracked or ignored
215
this.logger.warn(`[GitFileSystemProvider][readFile] File not found - ${uri.toString()}`);
216
throw FileSystemError.FileNotFound();
217
}
218
}
219
220
writeFile(): void {
221
throw new Error('Method not implemented.');
222
}
223
224
delete(): void {
225
throw new Error('Method not implemented.');
226
}
227
228
rename(): void {
229
throw new Error('Method not implemented.');
230
}
231
232
dispose(): void {
233
this.disposables.forEach(d => d.dispose());
234
}
235
}
236
237