diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 537f5264631..1fb46be52f7 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -17,6 +17,8 @@ import { git } from "@/util/git" const SUBSCRIBE_TIMEOUT_MS = 10_000 +const GIT_STATUS_CHANGE_DEBOUNCE = 200 + declare const OPENCODE_LIBC: string | undefined export namespace FileWatcher { @@ -30,6 +32,7 @@ export namespace FileWatcher { event: z.union([z.literal("add"), z.literal("change"), z.literal("unlink")]), }), ), + GitStatusChanged: BusEvent.define("git.status.changed", z.object({ directory: z.string() })), } const watcher = lazy((): typeof import("@parcel/watcher") | undefined => { @@ -108,6 +111,32 @@ export namespace FileWatcher { } } + if (Instance.project.vcs === "git") { + const result = await git(["rev-parse", "--absolute-git-dir"], { + cwd: Instance.directory, + }) + if (result.exitCode === 0 && result.text().trim()) { + const gitDir = result.text().trim() + const directory = path.dirname(gitDir) + let debounceTimer: ReturnType | undefined + const handleGitStatusChange: ParcelWatcher.SubscribeCallback = (err) => { + if (err) return + if (debounceTimer) clearTimeout(debounceTimer) + debounceTimer = setTimeout(() => { + Bus.publish(Event.GitStatusChanged, { directory: directory }) + }, GIT_STATUS_CHANGE_DEBOUNCE) + } + const pending = w.subscribe(gitDir, handleGitStatusChange, { ignore: [], backend }) + const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => { + log.error("failed to subscribe to .git change", { error: err, cwd: Instance.directory }) + if (debounceTimer) clearTimeout(debounceTimer) + pending.then((s) => s.unsubscribe()).catch(() => {}) + return undefined + }) + if (sub) subs.push(sub) + } + } + return { subs } }, async (state) => { diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index 34d59054314..8d0e87caca4 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -6,6 +6,9 @@ import { Log } from "@/util/log" import { Instance } from "./instance" import { FileWatcher } from "@/file/watcher" import { git } from "@/util/git" +import { Session } from "@/session" +import { Storage } from "@/storage/storage" +import { Snapshot } from "@/snapshot" const log = Log.create({ service: "vcs" }) @@ -41,7 +44,11 @@ export namespace Vcs { const state = Instance.state( async () => { if (Instance.project.vcs !== "git") { - return { branch: async () => undefined, unsubscribe: undefined } + return { + branch: async () => undefined, + unsubscribe: undefined, + unsubscribeGitStatus: undefined, + } } let current = await currentBranch() log.info("initialized", { branch: current }) @@ -56,13 +63,43 @@ export namespace Vcs { } }) + const unsubscribeGitStatus = Bus.subscribe(FileWatcher.Event.GitStatusChanged, async (evt) => { + const directory = evt.properties.directory + log.info("git status change", { directory: directory }) + + const changedFiles = await Snapshot.diffFull("HEAD", "", path.join(directory, ".git")) + const sessions = [...Session.list({ directory: directory })] + for (const s of sessions) { + try { + await Storage.write(["session_diff", s.id], changedFiles) + await Session.setSummary({ + sessionID: s.id, + summary: { + additions: changedFiles.reduce((sum, f) => sum + f.additions, 0), + deletions: changedFiles.reduce((sum, f) => sum + f.deletions, 0), + files: changedFiles.length, + }, + }) + Bus.publish(Session.Event.Diff, { sessionID: s.id, diff: changedFiles }) + } catch (e) { + log.error("failed to handle git status change", { + error: e, + sessionID: s.id, + directory: directory, + }) + } + } + }) + return { branch: async () => current, unsubscribe, + unsubscribeGitStatus, } }, async (state) => { state.unsubscribe?.() + state.unsubscribeGitStatus?.() }, ) diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 848f2694e01..487d4bcdb1b 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -16,7 +16,7 @@ export namespace Snapshot { const prune = "7.days" function args(git: string, cmd: string[]) { - return ["--git-dir", git, "--work-tree", Instance.worktree, ...cmd] + return ["--git-dir", git, "--work-tree", Instance.worktree, ...cmd.filter(Boolean)] } export function init() { @@ -265,8 +265,10 @@ export namespace Snapshot { ref: "FileDiff", }) export type FileDiff = z.infer - export async function diffFull(from: string, to: string): Promise { - const git = gitdir() + export async function diffFull(from: string, to: string, git?: string): Promise { + if (! git) { + git = gitdir() + } const result: FileDiff[] = [] const status = new Map()