Path: blob/main/extensions/git/src/fileSystemProvider.ts
3316 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { workspace, Uri, Disposable, Event, EventEmitter, window, FileSystemProvider, FileChangeEvent, FileStat, FileType, FileChangeType, FileSystemError, LogOutputChannel } from 'vscode';6import { debounce, throttle } from './decorators';7import { fromGitUri, toGitUri } from './uri';8import { Model, ModelChangeEvent, OriginalResourceChangeEvent } from './model';9import { filterEvent, eventToPromise, isDescendant, pathEquals, EmptyDisposable } from './util';10import { Repository } from './repository';1112interface CacheRow {13uri: Uri;14timestamp: number;15}1617const THREE_MINUTES = 1000 * 60 * 3;18const FIVE_MINUTES = 1000 * 60 * 5;1920function sanitizeRef(ref: string, path: string, submoduleOf: string | undefined, repository: Repository): string {21if (ref === '~') {22const fileUri = Uri.file(path);23const uriString = fileUri.toString();24const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);25return indexStatus ? '' : 'HEAD';26}2728if (/^~\d$/.test(ref)) {29return `:${ref[1]}`;30}3132// Submodule HEAD33if (submoduleOf && (ref === 'index' || ref === 'wt')) {34return 'HEAD';35}3637return ref;38}3940export class GitFileSystemProvider implements FileSystemProvider {4142private _onDidChangeFile = new EventEmitter<FileChangeEvent[]>();43readonly onDidChangeFile: Event<FileChangeEvent[]> = this._onDidChangeFile.event;4445private changedRepositoryRoots = new Set<string>();46private cache = new Map<string, CacheRow>();47private mtime = new Date().getTime();48private disposables: Disposable[] = [];4950constructor(private readonly model: Model, private readonly logger: LogOutputChannel) {51this.disposables.push(52model.onDidChangeRepository(this.onDidChangeRepository, this),53model.onDidChangeOriginalResource(this.onDidChangeOriginalResource, this),54workspace.registerFileSystemProvider('git', this, { isReadonly: true, isCaseSensitive: true }),55);5657setInterval(() => this.cleanup(), FIVE_MINUTES);58}5960private onDidChangeRepository({ repository }: ModelChangeEvent): void {61this.changedRepositoryRoots.add(repository.root);62this.eventuallyFireChangeEvents();63}6465private onDidChangeOriginalResource({ uri }: OriginalResourceChangeEvent): void {66if (uri.scheme !== 'file') {67return;68}6970const diffOriginalResourceUri = toGitUri(uri, '~',);71const quickDiffOriginalResourceUri = toGitUri(uri, '', { replaceFileExtension: true });7273this.mtime = new Date().getTime();74this._onDidChangeFile.fire([75{ type: FileChangeType.Changed, uri: diffOriginalResourceUri },76{ type: FileChangeType.Changed, uri: quickDiffOriginalResourceUri }77]);78}7980@debounce(1100)81private eventuallyFireChangeEvents(): void {82this.fireChangeEvents();83}8485@throttle86private async fireChangeEvents(): Promise<void> {87if (!window.state.focused) {88const onDidFocusWindow = filterEvent(window.onDidChangeWindowState, e => e.focused);89await eventToPromise(onDidFocusWindow);90}9192const events: FileChangeEvent[] = [];9394for (const { uri } of this.cache.values()) {95const fsPath = uri.fsPath;9697for (const root of this.changedRepositoryRoots) {98if (isDescendant(root, fsPath)) {99events.push({ type: FileChangeType.Changed, uri });100break;101}102}103}104105if (events.length > 0) {106this.mtime = new Date().getTime();107this._onDidChangeFile.fire(events);108}109110this.changedRepositoryRoots.clear();111}112113private cleanup(): void {114const now = new Date().getTime();115const cache = new Map<string, CacheRow>();116117for (const row of this.cache.values()) {118const { path } = fromGitUri(row.uri);119const isOpen = workspace.textDocuments120.filter(d => d.uri.scheme === 'file')121.some(d => pathEquals(d.uri.fsPath, path));122123if (isOpen || now - row.timestamp < THREE_MINUTES) {124cache.set(row.uri.toString(), row);125} else {126// TODO: should fire delete events?127}128}129130this.cache = cache;131}132133watch(): Disposable {134return EmptyDisposable;135}136137async stat(uri: Uri): Promise<FileStat> {138await this.model.isInitialized;139140const { submoduleOf, path, ref } = fromGitUri(uri);141const repository = submoduleOf ? this.model.getRepository(submoduleOf) : this.model.getRepository(uri);142if (!repository) {143this.logger.warn(`[GitFileSystemProvider][stat] Repository not found - ${uri.toString()}`);144throw FileSystemError.FileNotFound();145}146147try {148const details = await repository.getObjectDetails(sanitizeRef(ref, path, submoduleOf, repository), path);149return { type: FileType.File, size: details.size, mtime: this.mtime, ctime: 0 };150} catch {151// Empty tree152if (ref === await repository.getEmptyTree()) {153this.logger.warn(`[GitFileSystemProvider][stat] Empty tree - ${uri.toString()}`);154return { type: FileType.File, size: 0, mtime: this.mtime, ctime: 0 };155}156157// File does not exist in git. This could be because the file is untracked or ignored158this.logger.warn(`[GitFileSystemProvider][stat] File not found - ${uri.toString()}`);159throw FileSystemError.FileNotFound();160}161}162163readDirectory(): Thenable<[string, FileType][]> {164throw new Error('Method not implemented.');165}166167createDirectory(): void {168throw new Error('Method not implemented.');169}170171async readFile(uri: Uri): Promise<Uint8Array> {172await this.model.isInitialized;173174const { path, ref, submoduleOf } = fromGitUri(uri);175176if (submoduleOf) {177const repository = this.model.getRepository(submoduleOf);178179if (!repository) {180throw FileSystemError.FileNotFound();181}182183const encoder = new TextEncoder();184185if (ref === 'index') {186return encoder.encode(await repository.diffIndexWithHEAD(path));187} else {188return encoder.encode(await repository.diffWithHEAD(path));189}190}191192const repository = this.model.getRepository(uri);193194if (!repository) {195this.logger.warn(`[GitFileSystemProvider][readFile] Repository not found - ${uri.toString()}`);196throw FileSystemError.FileNotFound();197}198199const timestamp = new Date().getTime();200const cacheValue: CacheRow = { uri, timestamp };201202this.cache.set(uri.toString(), cacheValue);203204try {205return await repository.buffer(sanitizeRef(ref, path, submoduleOf, repository), path);206} catch {207// Empty tree208if (ref === await repository.getEmptyTree()) {209this.logger.warn(`[GitFileSystemProvider][readFile] Empty tree - ${uri.toString()}`);210return new Uint8Array(0);211}212213// File does not exist in git. This could be because the file is untracked or ignored214this.logger.warn(`[GitFileSystemProvider][readFile] File not found - ${uri.toString()}`);215throw FileSystemError.FileNotFound();216}217}218219writeFile(): void {220throw new Error('Method not implemented.');221}222223delete(): void {224throw new Error('Method not implemented.');225}226227rename(): void {228throw new Error('Method not implemented.');229}230231dispose(): void {232this.disposables.forEach(d => d.dispose());233}234}235236237