Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 5 additions & 1 deletion packages/app/e2e/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export async function createTestProject() {
await fs.writeFile(path.join(root, "README.md"), "# e2e\n")

execSync("git init", { cwd: root, stdio: "ignore" })
execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" })
execSync("git add -A", { cwd: root, stdio: "ignore" })
execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', {
cwd: root,
Expand All @@ -207,7 +208,10 @@ export async function createTestProject() {
}

export async function cleanupTestProject(directory: string) {
await fs.rm(directory, { recursive: true, force: true }).catch(() => undefined)
try {
execSync("git fsmonitor--daemon stop", { cwd: directory, stdio: "ignore" })
} catch {}
await fs.rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => undefined)
}

export function sessionIDFromUrl(url: string) {
Expand Down
40 changes: 26 additions & 14 deletions packages/opencode/src/file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ export namespace File {
const project = Instance.project
if (project.vcs !== "git") return []

const diffOutput = await $`git -c core.quotepath=false diff --numstat HEAD`
const diffOutput = await $`git -c core.fsmonitor=false -c core.quotepath=false diff --numstat HEAD`
.cwd(Instance.directory)
.quiet()
.nothrow()
Expand All @@ -439,11 +439,12 @@ export namespace File {
}
}

const untrackedOutput = await $`git -c core.quotepath=false ls-files --others --exclude-standard`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
const untrackedOutput =
await $`git -c core.fsmonitor=false -c core.quotepath=false ls-files --others --exclude-standard`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()

if (untrackedOutput.trim()) {
const untrackedFiles = untrackedOutput.trim().split("\n")
Expand All @@ -464,11 +465,12 @@ export namespace File {
}

// Get deleted files
const deletedOutput = await $`git -c core.quotepath=false diff --name-only --diff-filter=D HEAD`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
const deletedOutput =
await $`git -c core.fsmonitor=false -c core.quotepath=false diff --name-only --diff-filter=D HEAD`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()

if (deletedOutput.trim()) {
const deletedFiles = deletedOutput.trim().split("\n")
Expand Down Expand Up @@ -539,10 +541,20 @@ export namespace File {
const content = (await Filesystem.readText(full).catch(() => "")).trim()

if (project.vcs === "git") {
let diff = await $`git diff ${file}`.cwd(Instance.directory).quiet().nothrow().text()
if (!diff.trim()) diff = await $`git diff --staged ${file}`.cwd(Instance.directory).quiet().nothrow().text()
let diff = await $`git -c core.fsmonitor=false diff ${file}`.cwd(Instance.directory).quiet().nothrow().text()
if (!diff.trim()) {
diff = await $`git -c core.fsmonitor=false diff --staged ${file}`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
}
if (diff.trim()) {
const original = await $`git show HEAD:${file}`.cwd(Instance.directory).quiet().nothrow().text()
const original = await $`git -c core.fsmonitor=false show HEAD:${file}`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
const patch = structuredPatch(file, file, original, content, "old", "new", {
context: Infinity,
ignoreWhitespace: true,
Expand Down
9 changes: 8 additions & 1 deletion packages/opencode/src/worktree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,11 @@ export namespace Worktree {
throw new RemoveFailedError({ message: message || "Failed to remove git worktree directory" })
})

const stop = async (target: string) => {
if (!(await exists(target))) return
await $`git fsmonitor--daemon stop`.quiet().nothrow().cwd(target)
}

const list = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree)
if (list.exitCode !== 0) {
throw new RemoveFailedError({ message: errorText(list) || "Failed to read git worktrees" })
Expand All @@ -484,11 +489,13 @@ export namespace Worktree {
if (!entry?.path) {
const directoryExists = await exists(directory)
if (directoryExists) {
await stop(directory)
await clean(directory)
}
return true
}

await stop(entry.path)
const removed = await $`git worktree remove --force ${entry.path}`.quiet().nothrow().cwd(Instance.worktree)
if (removed.exitCode !== 0) {
const next = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree)
Expand Down Expand Up @@ -637,7 +644,7 @@ export namespace Worktree {
throw new ResetFailedError({ message: errorText(subClean) || "Failed to clean submodules" })
}

const status = await $`git status --porcelain=v1`.quiet().nothrow().cwd(worktreePath)
const status = await $`git -c core.fsmonitor=false status --porcelain=v1`.quiet().nothrow().cwd(worktreePath)
if (status.exitCode !== 0) {
throw new ResetFailedError({ message: errorText(status) || "Failed to read git status" })
}
Expand Down
62 changes: 62 additions & 0 deletions packages/opencode/test/file/fsmonitor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { $ } from "bun"
import { describe, expect, test } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { File } from "../../src/file"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"

const wintest = process.platform === "win32" ? test : test.skip

describe("file fsmonitor", () => {
wintest("status does not start fsmonitor for readonly git checks", async () => {
await using tmp = await tmpdir({ git: true })
const target = path.join(tmp.path, "tracked.txt")

await fs.writeFile(target, "base\n")
await $`git add tracked.txt`.cwd(tmp.path).quiet()
await $`git commit -m init`.cwd(tmp.path).quiet()
await $`git config core.fsmonitor true`.cwd(tmp.path).quiet()
await $`git fsmonitor--daemon stop`.cwd(tmp.path).quiet().nothrow()
await fs.writeFile(target, "next\n")
await fs.writeFile(path.join(tmp.path, "new.txt"), "new\n")

const before = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow()
expect(before.exitCode).not.toBe(0)

await Instance.provide({
directory: tmp.path,
fn: async () => {
await File.status()
},
})

const after = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow()
expect(after.exitCode).not.toBe(0)
})

wintest("read does not start fsmonitor for git diffs", async () => {
await using tmp = await tmpdir({ git: true })
const target = path.join(tmp.path, "tracked.txt")

await fs.writeFile(target, "base\n")
await $`git add tracked.txt`.cwd(tmp.path).quiet()
await $`git commit -m init`.cwd(tmp.path).quiet()
await $`git config core.fsmonitor true`.cwd(tmp.path).quiet()
await $`git fsmonitor--daemon stop`.cwd(tmp.path).quiet().nothrow()
await fs.writeFile(target, "next\n")

const before = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow()
expect(before.exitCode).not.toBe(0)

await Instance.provide({
directory: tmp.path,
fn: async () => {
await File.read("tracked.txt")
},
})

const after = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow()
expect(after.exitCode).not.toBe(0)
})
})
26 changes: 26 additions & 0 deletions packages/opencode/test/fixture/fixture.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { $ } from "bun"
import { describe, expect, test } from "bun:test"
import fs from "fs/promises"
import { tmpdir } from "./fixture"

describe("tmpdir", () => {
test("disables fsmonitor for git fixtures", async () => {
await using tmp = await tmpdir({ git: true })

const value = (await $`git config core.fsmonitor`.cwd(tmp.path).quiet().text()).trim()
expect(value).toBe("false")
})

test("removes directories on dispose", async () => {
const tmp = await tmpdir({ git: true })
const dir = tmp.path

await tmp[Symbol.asyncDispose]()

const exists = await fs
.stat(dir)
.then(() => true)
.catch(() => false)
expect(exists).toBe(false)
})
})
30 changes: 28 additions & 2 deletions packages/opencode/test/fixture/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@ function sanitizePath(p: string): string {
return p.replace(/\0/g, "")
}

function exists(dir: string) {
return fs
.stat(dir)
.then(() => true)
.catch(() => false)
}

function clean(dir: string) {
return fs.rm(dir, {
recursive: true,
force: true,
maxRetries: 5,
retryDelay: 100,
})
}

async function stop(dir: string) {
if (!(await exists(dir))) return
await $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow()
}

type TmpDirOptions<T> = {
git?: boolean
config?: Partial<Config.Info>
Expand All @@ -20,6 +41,7 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
await fs.mkdir(dirpath, { recursive: true })
if (options?.git) {
await $`git init`.cwd(dirpath).quiet()
await $`git config core.fsmonitor false`.cwd(dirpath).quiet()
await $`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath).quiet()
}
if (options?.config) {
Expand All @@ -35,8 +57,12 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
const realpath = sanitizePath(await fs.realpath(dirpath))
const result = {
[Symbol.asyncDispose]: async () => {
await options?.dispose?.(dirpath)
// await fs.rm(dirpath, { recursive: true, force: true })
try {
await options?.dispose?.(realpath)
} finally {
Comment thread
Hona marked this conversation as resolved.
Outdated
if (options?.git) await stop(realpath)
await clean(realpath)
}
},
path: realpath,
extra: extra as T,
Expand Down
31 changes: 31 additions & 0 deletions packages/opencode/test/project/worktree-remove.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Worktree } from "../../src/worktree"
import { Filesystem } from "../../src/util/filesystem"
import { tmpdir } from "../fixture/fixture"

const wintest = process.platform === "win32" ? test : test.skip

describe("Worktree.remove", () => {
test("continues when git remove exits non-zero after detaching", async () => {
await using tmp = await tmpdir({ git: true })
Expand Down Expand Up @@ -62,4 +64,33 @@ describe("Worktree.remove", () => {
const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow()
expect(ref.exitCode).not.toBe(0)
})

wintest("stops fsmonitor before removing a worktree", async () => {
await using tmp = await tmpdir({ git: true })
const root = tmp.path
const name = `remove-fsmonitor-${Date.now().toString(36)}`
const branch = `opencode/${name}`
const dir = path.join(root, "..", name)

await $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet()
await $`git reset --hard`.cwd(dir).quiet()
await $`git config core.fsmonitor true`.cwd(dir).quiet()
await $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow()
await Bun.write(path.join(dir, "tracked.txt"), "next\n")
await $`git diff`.cwd(dir).quiet()

const before = await $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow()
expect(before.exitCode).toBe(0)

const ok = await Instance.provide({
directory: root,
fn: () => Worktree.remove({ directory: dir }),
})

expect(ok).toBe(true)
expect(await Filesystem.exists(dir)).toBe(false)

const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow()
expect(ref.exitCode).not.toBe(0)
})
})
Loading