From 29c90725aeebd9176c0d8749db030adab256771f Mon Sep 17 00:00:00 2001 From: Jonathan van Rij Date: Tue, 16 Jun 2026 11:42:23 +0200 Subject: [PATCH] adds correct git watcher --- src/main/index.ts | 51 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 2052648..1c6ce04 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,4 +1,5 @@ -import { join, sep } from 'node:path' +import { join, resolve, sep } from 'node:path' +import { readFileSync, statSync } from 'node:fs' import { app, dialog, shell, BrowserWindow, ipcMain, Menu } from 'electron' import { watch, type FSWatcher } from 'chokidar' import { addRecentProject, getName, getRecentProjects, getRoot, openDialog, setRoot } from './project' @@ -18,7 +19,9 @@ const WATCH_IGNORE = new Set([ let watcher: FSWatcher | null = null let configWatcher: FSWatcher | null = null +let gitWatcher: FSWatcher | null = null let watchTimer: ReturnType | null = null +let gitTimer: ReturnType | null = null function broadcast(channel: string): void { for (const w of BrowserWindow.getAllWindows()) w.webContents.send(channel) @@ -34,6 +37,47 @@ function startConfigWatcher(): void { configWatcher.on('add', reload).on('change', reload).on('unlink', reload) } +/** Resolve the real git directory for a project root. Normally this is + * `/.git`, but for worktrees/submodules `.git` is a file containing + * `gitdir: ` pointing at the actual directory. */ +function gitDir(root: string): string { + const dot = join(root, '.git') + try { + if (statSync(dot).isFile()) { + const m = readFileSync(dot, 'utf8').trim().match(/^gitdir:\s*(.+)$/) + if (m) return resolve(root, m[1].trim()) + } + } catch { /* not a worktree, or .git missing */ } + return dot +} + +/** The main watcher ignores `.git`, so branch switches / commits / staging + * done by external tools (Sublime Merge, the CLI, …) would never refresh the + * git column. Watch the few git files that mark those events so the renderer + * re-reads status: HEAD (branch switch), index (staging), refs/heads + logs + * (commits), MERGE_HEAD (in-progress merge). */ +function startGitWatcher(): void { + if (gitWatcher) { gitWatcher.close(); gitWatcher = null } + const root = getRoot() + if (!root) return + const dir = gitDir(root) + gitWatcher = watch( + [ + join(dir, 'HEAD'), + join(dir, 'index'), + join(dir, 'refs', 'heads'), + join(dir, 'logs', 'HEAD'), + join(dir, 'MERGE_HEAD'), + ], + { ignoreInitial: true }, + ) + const ping = (): void => { + if (gitTimer) clearTimeout(gitTimer) + gitTimer = setTimeout(() => broadcast('project:changed'), 200) + } + gitWatcher.on('add', ping).on('change', ping).on('unlink', ping).on('addDir', ping).on('unlinkDir', ping) +} + /** Show the folder picker; if a new folder is chosen, switch the project * (config + watchers). Returns whether the project changed. */ async function openFolderFlow(win: BrowserWindow | null): Promise { @@ -42,6 +86,7 @@ async function openFolderFlow(win: BrowserWindow | null): Promise { await resolveConfig(next) startWatcher() startConfigWatcher() + startGitWatcher() return true } @@ -95,7 +140,7 @@ function registerIpc(): void { ipcMain.handle('project:openPath', async (_e, path: string) => { setRoot(path) const r = getRoot() - if (r) { await resolveConfig(r); startWatcher(); startConfigWatcher() } + if (r) { await resolveConfig(r); startWatcher(); startConfigWatcher(); startGitWatcher() } broadcast('project:changed') return { root: getRoot(), name: getName() } }) @@ -185,6 +230,7 @@ app.whenReady().then(async () => { await addRecentProject(initialRoot) startWatcher() startConfigWatcher() + startGitWatcher() } createWindow() @@ -196,6 +242,7 @@ app.whenReady().then(async () => { app.on('window-all-closed', () => { if (watcher) { watcher.close(); watcher = null } if (configWatcher) { configWatcher.close(); configWatcher = null } + if (gitWatcher) { gitWatcher.close(); gitWatcher = null } killAllPtys() if (!isMac) app.quit() })