Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 43 additions & 32 deletions src/FileSystemWatcherManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,27 @@
* GPL-3.0-only. See LICENSE.md in the project root for license details.
*/

import { Disposable, Uri, WorkspaceFolder, WorkspaceFoldersChangeEvent, window } from 'vscode'
import { FSWatcher, existsSync, watch } from 'fs'
import { Disposable, WorkspaceFolder, WorkspaceFoldersChangeEvent, window } from 'vscode'
import { existsSync, WatchEventType } from 'fs'
import { PathWatcher, WatcherCallback } from './Foundation/PathWatcher'
import { join } from 'path'

type CallbackFunction = (event: Uri) => void
type RepoWatcherCallback = (event: WatchEventType, filename: string) => void

// https://github.com/Microsoft/vscode/issues/3025
export default class implements Disposable {
private callback: CallbackFunction
private watchers: Map<string, FSWatcher> = new Map() as Map<string, FSWatcher>
private callback: RepoWatcherCallback
private watchers: Map<string, PathWatcher[]> = new Map() as Map<string, PathWatcher[]>

/**
* Creates a new watcher.
*
* @param repos the repositories to watch
* @param callback the callback to run when detecting changes
*/
constructor(repos: string[], callback: CallbackFunction) {
constructor(repos: string[], callback: RepoWatcherCallback) {
this.callback = callback
repos.forEach((directory) => { this.registerProjectWatcher(directory) })
repos.forEach((directory) => { this.registerProjectWatchers(directory) })
}

/**
Expand All @@ -33,67 +34,77 @@ export default class implements Disposable {
public configure(directoryChanges: WorkspaceFoldersChangeEvent): void {
directoryChanges.added.forEach((changedDirectory: WorkspaceFolder) => {
const directory = changedDirectory.uri.fsPath
this.registerProjectWatcher(directory)
this.registerProjectWatchers(directory)
})

directoryChanges.removed.forEach((changedDirectory: WorkspaceFolder) => {
const directory = changedDirectory.uri.fsPath
this.removeProjectWatcher(directory)
this.removeProjectWatchers(directory)
})
}

/**
* Disposes this object.
* @see Disposable.dispose()
*/
public dispose(): void {
for (const path of this.watchers.keys()) {
this.removeProjectWatcher(path)
this.removeProjectWatchers(path)
}
}

/**
* Registers a new project directory watcher.
* Registers the project directory watchers.
*
* @param projectPath the directory path
*/
private registerProjectWatcher(projectPath: string): void {
private registerProjectWatchers(projectPath: string): void {
global.dbg(`[FSWatch] Watch ${projectPath} ...`)
if (this.watchers.has(projectPath)) {
const pathToMonitor = join(projectPath, '.git', 'refs')
if (!existsSync(pathToMonitor)) {
return
}

const pathToMonitor = join(projectPath, '.git', 'refs')
this.registerPathWatcher(pathToMonitor, projectPath)
}

if (!existsSync(pathToMonitor)) {
/**
* Creates a FS Watcher for a directory.
* @param pathToMonitor the path to monitor
* @param projectPath the path to use in the callback
*/
private registerPathWatcher(pathToMonitor: string, projectPath: string) {
const watchers = this.watchers.get(projectPath)
?? this.watchers.set(projectPath, []).get(projectPath)! // eslint-disable-line @typescript-eslint/no-non-null-assertion

if (watchers.length && watchers.find((watcher) => watcher.for(pathToMonitor))) {
return
}

try {
const watcher = watch(pathToMonitor, (event: string, filename) => {
// Create the watcher callback which will review if a reaction is needed
// based on the changed (stash) file.
const callback: WatcherCallback = (event, filename) => {
if (filename?.includes('stash')) {
this.callback(Uri.file(projectPath))
this.callback(event, projectPath)
}
})
}

this.watchers.set(projectPath, watcher)
watchers.push(PathWatcher.watch(pathToMonitor, callback))
}
catch (error) {
const msg = `Unable to create a stashes monitor for ${pathToMonitor}.`
+ ' This may happen on NFS or if the path is a link.'
+ ' See the console for details'
console.error(msg)
console.error(error)
void window.showErrorMessage(`Unable to a create a stashes monitor for
${projectPath}. This may happen on NFS or if the path is a link`)
void window.showErrorMessage(msg)
}
}

/**
* Removes an active project directory watcher.
*
* @param path the directory path
*/
private removeProjectWatcher(path: string): void {
if (this.watchers.has(path)) {
global.dbg(`[FSWatch] Stop watching ${path} ...`)
this.watchers.get(path)?.close()
this.watchers.delete(path)
}
private removeProjectWatchers(path: string): void {
global.dbg(`[FSWatch] Stop watching ${path} ...`)
this.watchers.get(path)?.forEach((watcher) => { watcher.dispose() })
this.watchers.delete(path)
}
}
44 changes: 44 additions & 0 deletions src/Foundation/PathWatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) Arturo Rodríguez V.
* GPL-3.0-only. See LICENSE.md in the project root for license details.
*/

import { FSWatcher, WatchListener, watch } from 'fs'

/**
* The callback used on PathWatcher events.
*/
export type WatcherCallback = WatchListener<string>

/**
* A class wrapping a FSWatcher adding a path property to identify the watched directory.
*/
export class PathWatcher {
protected constructor(
public path: string,
public watcher: FSWatcher,
) { }

/**
* Creates a watcher for the given file.
*/
public static watch(path: string, callback: WatcherCallback) {
return new PathWatcher(path, watch(path, (event, filename) => {
callback(event, filename)
}))
}

/**
* Indicates if the watcher if for the specified path.
*/
public for(path: string): boolean {
return this.path === path
}

/**
* Disposes the watcher by removing every listener and closing it.
*/
public dispose(): void {
this.watcher.removeAllListeners().close()
}
}
2 changes: 1 addition & 1 deletion src/StashNode/NodeContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export default class NodeContainer {
* @param eagerLoadStashes indicates if children should be preloaded
*/
public async getRepositories(eagerLoadStashes: boolean): Promise<RepositoryNode[]> {
const paths = await this.gitWorkspace.getRepositories(false)
const paths = await this.gitWorkspace.getRepositories()

const repositoryNodes: RepositoryNode[] = []
for (const repositoryPath of paths) {
Expand Down
5 changes: 4 additions & 1 deletion src/StashNode/NodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ export default class NodeFactory {
// May be undefined if the directory is not part of the workspace,
// this happens on upper directories by negative search depth setting.
const wsFolder = workspace.getWorkspaceFolder(Uri.file(path))
// In a root dir, the received path is contained in the workspace's path.
const isRoot = wsFolder?.uri.fsPath.includes(path) ?? false

return new RepositoryNode(
dirname(path),
basename(path),
wsFolder?.name,
// wsFolder is defined when isRoot is true but TS cannot infer that so add a ?
isRoot ? wsFolder?.name : undefined,
)
}

Expand Down
6 changes: 3 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ export async function activate(context: ExtensionContext): Promise<void> {

const watcherManager = new FileSystemWatcherManager(
repos,
(projectDirectory: Uri) => {
global.dbg(`[watch] Reloading explorer (${projectDirectory.fsPath})...`)
treeProvider.reload('update', projectDirectory)
(event, projectDirectory) => {
global.dbg(`[watch] Reloading explorer (${projectDirectory})...`)
treeProvider.reload('update', Uri.file(projectDirectory))
},
)
global.dbg('[boot] FS Watcher created')
Expand Down