This commit is contained in:
@@ -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 { app, dialog, shell, BrowserWindow, ipcMain, Menu } from 'electron'
|
||||||
import { watch, type FSWatcher } from 'chokidar'
|
import { watch, type FSWatcher } from 'chokidar'
|
||||||
import { addRecentProject, getName, getRecentProjects, getRoot, openDialog, setRoot } from './project'
|
import { addRecentProject, getName, getRecentProjects, getRoot, openDialog, setRoot } from './project'
|
||||||
@@ -18,7 +19,9 @@ const WATCH_IGNORE = new Set([
|
|||||||
|
|
||||||
let watcher: FSWatcher | null = null
|
let watcher: FSWatcher | null = null
|
||||||
let configWatcher: FSWatcher | null = null
|
let configWatcher: FSWatcher | null = null
|
||||||
|
let gitWatcher: FSWatcher | null = null
|
||||||
let watchTimer: ReturnType<typeof setTimeout> | null = null
|
let watchTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let gitTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
function broadcast(channel: string): void {
|
function broadcast(channel: string): void {
|
||||||
for (const w of BrowserWindow.getAllWindows()) w.webContents.send(channel)
|
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)
|
configWatcher.on('add', reload).on('change', reload).on('unlink', reload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolve the real git directory for a project root. Normally this is
|
||||||
|
* `<root>/.git`, but for worktrees/submodules `.git` is a file containing
|
||||||
|
* `gitdir: <path>` 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
|
/** Show the folder picker; if a new folder is chosen, switch the project
|
||||||
* (config + watchers). Returns whether the project changed. */
|
* (config + watchers). Returns whether the project changed. */
|
||||||
async function openFolderFlow(win: BrowserWindow | null): Promise<boolean> {
|
async function openFolderFlow(win: BrowserWindow | null): Promise<boolean> {
|
||||||
@@ -42,6 +86,7 @@ async function openFolderFlow(win: BrowserWindow | null): Promise<boolean> {
|
|||||||
await resolveConfig(next)
|
await resolveConfig(next)
|
||||||
startWatcher()
|
startWatcher()
|
||||||
startConfigWatcher()
|
startConfigWatcher()
|
||||||
|
startGitWatcher()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +140,7 @@ function registerIpc(): void {
|
|||||||
ipcMain.handle('project:openPath', async (_e, path: string) => {
|
ipcMain.handle('project:openPath', async (_e, path: string) => {
|
||||||
setRoot(path)
|
setRoot(path)
|
||||||
const r = getRoot()
|
const r = getRoot()
|
||||||
if (r) { await resolveConfig(r); startWatcher(); startConfigWatcher() }
|
if (r) { await resolveConfig(r); startWatcher(); startConfigWatcher(); startGitWatcher() }
|
||||||
broadcast('project:changed')
|
broadcast('project:changed')
|
||||||
return { root: getRoot(), name: getName() }
|
return { root: getRoot(), name: getName() }
|
||||||
})
|
})
|
||||||
@@ -185,6 +230,7 @@ app.whenReady().then(async () => {
|
|||||||
await addRecentProject(initialRoot)
|
await addRecentProject(initialRoot)
|
||||||
startWatcher()
|
startWatcher()
|
||||||
startConfigWatcher()
|
startConfigWatcher()
|
||||||
|
startGitWatcher()
|
||||||
}
|
}
|
||||||
createWindow()
|
createWindow()
|
||||||
|
|
||||||
@@ -196,6 +242,7 @@ app.whenReady().then(async () => {
|
|||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (watcher) { watcher.close(); watcher = null }
|
if (watcher) { watcher.close(); watcher = null }
|
||||||
if (configWatcher) { configWatcher.close(); configWatcher = null }
|
if (configWatcher) { configWatcher.close(); configWatcher = null }
|
||||||
|
if (gitWatcher) { gitWatcher.close(); gitWatcher = null }
|
||||||
killAllPtys()
|
killAllPtys()
|
||||||
if (!isMac) app.quit()
|
if (!isMac) app.quit()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user