mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-04 11:46:38 +00:00
feat: restore git-backed review modes (#20845)
This commit is contained in:
128
packages/opencode/test/git/git.test.ts
Normal file
128
packages/opencode/test/git/git.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { $ } from "bun"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { ManagedRuntime } from "effect"
|
||||
import { Git } from "../../src/git"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
|
||||
const weird = process.platform === "win32" ? "space file.txt" : "tab\tfile.txt"
|
||||
|
||||
async function withGit<T>(body: (rt: ManagedRuntime.ManagedRuntime<Git.Service, never>) => Promise<T>) {
|
||||
const rt = ManagedRuntime.make(Git.defaultLayer)
|
||||
try {
|
||||
return await body(rt)
|
||||
} finally {
|
||||
await rt.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
describe("Git", () => {
|
||||
test("branch() returns current branch name", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await withGit(async (rt) => {
|
||||
const branch = await rt.runPromise(Git.Service.use((git) => git.branch(tmp.path)))
|
||||
expect(branch).toBeDefined()
|
||||
expect(typeof branch).toBe("string")
|
||||
})
|
||||
})
|
||||
|
||||
test("branch() returns undefined for non-git directories", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await withGit(async (rt) => {
|
||||
const branch = await rt.runPromise(Git.Service.use((git) => git.branch(tmp.path)))
|
||||
expect(branch).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
test("branch() returns undefined for detached HEAD", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
const hash = (await $`git rev-parse HEAD`.cwd(tmp.path).quiet().text()).trim()
|
||||
await $`git checkout --detach ${hash}`.cwd(tmp.path).quiet()
|
||||
|
||||
await withGit(async (rt) => {
|
||||
const branch = await rt.runPromise(Git.Service.use((git) => git.branch(tmp.path)))
|
||||
expect(branch).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
test("defaultBranch() uses init.defaultBranch when available", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await $`git branch -M trunk`.cwd(tmp.path).quiet()
|
||||
await $`git config init.defaultBranch trunk`.cwd(tmp.path).quiet()
|
||||
|
||||
await withGit(async (rt) => {
|
||||
const branch = await rt.runPromise(Git.Service.use((git) => git.defaultBranch(tmp.path)))
|
||||
expect(branch?.name).toBe("trunk")
|
||||
expect(branch?.ref).toBe("trunk")
|
||||
})
|
||||
})
|
||||
|
||||
test("status() handles special filenames", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await fs.writeFile(path.join(tmp.path, weird), "hello\n", "utf-8")
|
||||
|
||||
await withGit(async (rt) => {
|
||||
const status = await rt.runPromise(Git.Service.use((git) => git.status(tmp.path)))
|
||||
expect(status).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
file: weird,
|
||||
status: "added",
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("diff(), stats(), and mergeBase() parse tracked changes", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await $`git branch -M main`.cwd(tmp.path).quiet()
|
||||
await fs.writeFile(path.join(tmp.path, weird), "before\n", "utf-8")
|
||||
await $`git add .`.cwd(tmp.path).quiet()
|
||||
await $`git commit --no-gpg-sign -m "add file"`.cwd(tmp.path).quiet()
|
||||
await $`git checkout -b feature/test`.cwd(tmp.path).quiet()
|
||||
await fs.writeFile(path.join(tmp.path, weird), "after\n", "utf-8")
|
||||
|
||||
await withGit(async (rt) => {
|
||||
const [base, diff, stats] = await Promise.all([
|
||||
rt.runPromise(Git.Service.use((git) => git.mergeBase(tmp.path, "main"))),
|
||||
rt.runPromise(Git.Service.use((git) => git.diff(tmp.path, "HEAD"))),
|
||||
rt.runPromise(Git.Service.use((git) => git.stats(tmp.path, "HEAD"))),
|
||||
])
|
||||
|
||||
expect(base).toBeTruthy()
|
||||
expect(diff).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
file: weird,
|
||||
status: "modified",
|
||||
}),
|
||||
]),
|
||||
)
|
||||
expect(stats).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
file: weird,
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("show() returns empty text for binary blobs", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await fs.writeFile(path.join(tmp.path, "bin.dat"), new Uint8Array([0, 1, 2, 3]))
|
||||
await $`git add .`.cwd(tmp.path).quiet()
|
||||
await $`git commit --no-gpg-sign -m "add binary"`.cwd(tmp.path).quiet()
|
||||
|
||||
await withGit(async (rt) => {
|
||||
const text = await rt.runPromise(Git.Service.use((git) => git.show(tmp.path, "HEAD", "bin.dat")))
|
||||
expect(text).toBe("")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -8,8 +8,13 @@ import { Instance } from "../../src/project/instance"
|
||||
import { GlobalBus } from "../../src/bus/global"
|
||||
import { Vcs } from "../../src/project/vcs"
|
||||
|
||||
// Skip in CI — native @parcel/watcher binding needed
|
||||
const describeVcs = FileWatcher.hasNativeBinding() && !process.env.CI ? describe : describe.skip
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function withVcs(directory: string, body: () => Promise<void>) {
|
||||
return Instance.provide({
|
||||
directory,
|
||||
@@ -22,8 +27,20 @@ async function withVcs(directory: string, body: () => Promise<void>) {
|
||||
})
|
||||
}
|
||||
|
||||
type BranchEvent = { directory?: string; payload: { type: string; properties: { branch?: string } } }
|
||||
function withVcsOnly(directory: string, body: () => Promise<void>) {
|
||||
return Instance.provide({
|
||||
directory,
|
||||
fn: async () => {
|
||||
Vcs.init()
|
||||
await body()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type BranchEvent = { directory?: string; payload: { type: string; properties: { branch?: string } } }
|
||||
const weird = process.platform === "win32" ? "space file.txt" : "tab\tfile.txt"
|
||||
|
||||
/** Wait for a Vcs.Event.BranchUpdated event on GlobalBus, with retry polling as fallback */
|
||||
function nextBranchUpdate(directory: string, timeout = 10_000) {
|
||||
return new Promise<string | undefined>((resolve, reject) => {
|
||||
let settled = false
|
||||
@@ -49,6 +66,10 @@ function nextBranchUpdate(directory: string, timeout = 10_000) {
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describeVcs("Vcs", () => {
|
||||
afterEach(async () => {
|
||||
await Instance.disposeAll()
|
||||
@@ -82,11 +103,7 @@ describeVcs("Vcs", () => {
|
||||
const pending = nextBranchUpdate(tmp.path)
|
||||
|
||||
const head = path.join(tmp.path, ".git", "HEAD")
|
||||
await fs.writeFile(
|
||||
head,
|
||||
`ref: refs/heads/${branch}
|
||||
`,
|
||||
)
|
||||
await fs.writeFile(head, `ref: refs/heads/${branch}\n`)
|
||||
|
||||
const updated = await pending
|
||||
expect(updated).toBe(branch)
|
||||
@@ -102,11 +119,7 @@ describeVcs("Vcs", () => {
|
||||
const pending = nextBranchUpdate(tmp.path)
|
||||
|
||||
const head = path.join(tmp.path, ".git", "HEAD")
|
||||
await fs.writeFile(
|
||||
head,
|
||||
`ref: refs/heads/${branch}
|
||||
`,
|
||||
)
|
||||
await fs.writeFile(head, `ref: refs/heads/${branch}\n`)
|
||||
|
||||
await pending
|
||||
const current = await Vcs.branch()
|
||||
@@ -114,3 +127,102 @@ describeVcs("Vcs", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("Vcs diff", () => {
|
||||
afterEach(async () => {
|
||||
await Instance.disposeAll()
|
||||
})
|
||||
|
||||
test("defaultBranch() falls back to main", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await $`git branch -M main`.cwd(tmp.path).quiet()
|
||||
|
||||
await withVcsOnly(tmp.path, async () => {
|
||||
const branch = await Vcs.defaultBranch()
|
||||
expect(branch).toBe("main")
|
||||
})
|
||||
})
|
||||
|
||||
test("defaultBranch() uses init.defaultBranch when available", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await $`git branch -M trunk`.cwd(tmp.path).quiet()
|
||||
await $`git config init.defaultBranch trunk`.cwd(tmp.path).quiet()
|
||||
|
||||
await withVcsOnly(tmp.path, async () => {
|
||||
const branch = await Vcs.defaultBranch()
|
||||
expect(branch).toBe("trunk")
|
||||
})
|
||||
})
|
||||
|
||||
test("detects current branch from the active worktree", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await using wt = await tmpdir()
|
||||
await $`git branch -M main`.cwd(tmp.path).quiet()
|
||||
const dir = path.join(wt.path, "feature")
|
||||
await $`git worktree add -b feature/test ${dir} HEAD`.cwd(tmp.path).quiet()
|
||||
|
||||
await withVcsOnly(dir, async () => {
|
||||
const [branch, base] = await Promise.all([Vcs.branch(), Vcs.defaultBranch()])
|
||||
expect(branch).toBe("feature/test")
|
||||
expect(base).toBe("main")
|
||||
})
|
||||
})
|
||||
|
||||
test("diff('git') returns uncommitted changes", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "original\n", "utf-8")
|
||||
await $`git add .`.cwd(tmp.path).quiet()
|
||||
await $`git commit --no-gpg-sign -m "add file"`.cwd(tmp.path).quiet()
|
||||
await fs.writeFile(path.join(tmp.path, "file.txt"), "changed\n", "utf-8")
|
||||
|
||||
await withVcsOnly(tmp.path, async () => {
|
||||
const diff = await Vcs.diff("git")
|
||||
expect(diff).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
file: "file.txt",
|
||||
status: "modified",
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("diff('git') handles special filenames", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await fs.writeFile(path.join(tmp.path, weird), "hello\n", "utf-8")
|
||||
|
||||
await withVcsOnly(tmp.path, async () => {
|
||||
const diff = await Vcs.diff("git")
|
||||
expect(diff).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
file: weird,
|
||||
status: "added",
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("diff('branch') returns changes against default branch", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
await $`git branch -M main`.cwd(tmp.path).quiet()
|
||||
await $`git checkout -b feature/test`.cwd(tmp.path).quiet()
|
||||
await fs.writeFile(path.join(tmp.path, "branch.txt"), "hello\n", "utf-8")
|
||||
await $`git add .`.cwd(tmp.path).quiet()
|
||||
await $`git commit --no-gpg-sign -m "branch file"`.cwd(tmp.path).quiet()
|
||||
|
||||
await withVcsOnly(tmp.path, async () => {
|
||||
const diff = await Vcs.diff("branch")
|
||||
expect(diff).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
file: "branch.txt",
|
||||
status: "added",
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user