perf(core): cache git discovery in Project.resolve by metadata fingerprint (#58616)

This commit is contained in:
Kit Langton
2026-10-24 14:34:07 -07:00
committed by GitHub
Unverified
parent 0093fe5782
commit e573115f17
7 changed files with 330 additions and 37 deletions
+84 -12
View File
@@ -1,5 +1,6 @@
export * as Git from "./git.js"
import os from "os"
import path from "path"
import { Cause, Context, Effect, Exit, Layer, Option, Schedule, Schema } from "effect"
import { ChildProcess } from "effect/process"
@@ -80,6 +81,13 @@ export class OperationError extends Schema.TaggedError<OperationError>()("Git.Op
cause: Schema.optional(Schema.Defect()),
}) {}
/** Git could not answer: the spawn failed or it exited with an unexpected status. */
export class CommandError extends Schema.TaggedError<CommandError>()("Git.CommandError", {
args: Schema.Array(Schema.String),
exitCode: Schema.optional(Schema.Number),
cause: Schema.optional(Schema.Defect()),
}) {}
export class Worktree extends Schema.Class<Worktree>("Git.Worktree")({
directory: AbsolutePath,
kind: Schema.Literals(["main", "linked"]),
@@ -96,6 +104,11 @@ export class WorktreeError extends Schema.TaggedError<WorktreeError>()("Git.Work
export interface Interface {
readonly repo: {
readonly discover: (input: AbsolutePath) => Effect.Effect<Repository | undefined>
/**
* Spawn-free stamp of the metadata that `discover`, `remote.get` and `history.rootCommits`
* read for the `.git` entry at `dotGit`: undefined when it isn't a readable repository.
*/
readonly fingerprint: (dotGit: AbsolutePath) => Effect.Effect<string | undefined>
readonly clone: (input: {
remote: string
directory: AbsolutePath
@@ -109,13 +122,15 @@ export interface Interface {
}) => Effect.Effect<Repository, OperationError>
}
readonly remote: {
readonly get: (repository: Repository, name?: string) => Effect.Effect<string | undefined>
/** Undefined when the remote doesn't exist. */
readonly get: (repository: Repository, name?: string) => Effect.Effect<string | undefined, CommandError>
}
readonly history: {
readonly head: (repository: Repository) => Effect.Effect<string | undefined>
readonly branch: (repository: Repository) => Effect.Effect<string | undefined>
readonly defaultRemoteBranch: (repository: Repository, remote?: string) => Effect.Effect<string | undefined>
readonly rootCommits: (repository: Repository) => Effect.Effect<readonly string[]>
/** Empty when HEAD is unborn. */
readonly rootCommits: (repository: Repository) => Effect.Effect<readonly string[], CommandError>
}
readonly sync: {
readonly fetchRemotes: (repository: Repository, input?: { prune?: boolean }) => Effect.Effect<void, OperationError>
@@ -215,20 +230,77 @@ const layer = Layer.effect(
})
})
const stamp = (target: string) =>
fs.stat(target).pipe(
Effect.map((info) =>
[info.type, Option.getOrUndefined(info.ino), info.size, Option.getOrUndefined(info.mtime)?.getTime()].join(
":",
),
),
Effect.orElseSucceed(() => "missing"),
)
const readText = (target: string) => fs.readFileStringSafe(target).pipe(Effect.orElseSucceed(() => undefined))
// Covers linked worktrees (gitdir file + commondir), HEAD and the ref it points at,
// loose/packed/reftable refs, repository/worktree/global/XDG/system config, and the
// cached `opencode` project id, so external `git init`, a first commit or `git remote`
// edits change the stamp.
const fingerprint = Effect.fn("Git.repo.fingerprint")(function* (dotGit: AbsolutePath) {
const dot = yield* fs.stat(dotGit).pipe(Effect.orElseSucceed(() => undefined))
if (!dot || (dot.type !== "File" && dot.type !== "Directory")) return undefined
const gitFile = dot.type === "File" ? (yield* readText(dotGit))?.match(/^gitdir:\s*(.+)$/m)?.[1]?.trim() : ""
if (gitFile === undefined) return undefined
const gitDir = gitFile ? yield* fs.resolve(path.resolve(path.dirname(dotGit), gitFile)) : dotGit
const commondir = (yield* readText(path.join(gitDir, "commondir")))?.trim()
const commonDir = commondir ? yield* fs.resolve(path.resolve(gitDir, commondir)) : gitDir
const head = (yield* readText(path.join(gitDir, "HEAD")))?.trim()
if (head === undefined) return undefined
const ref = head.startsWith("ref:") ? head.slice(4).trim() : undefined
const home = process.env.HOME || os.homedir()
const nosystem = ["1", "true", "yes", "on"].includes((process.env.GIT_CONFIG_NOSYSTEM ?? "").toLowerCase())
const files = [
path.join(gitDir, "HEAD"),
path.join(commonDir, "config"),
path.join(commonDir, "config.worktree"),
path.join(gitDir, "config.worktree"),
path.join(commonDir, "packed-refs"),
path.join(commonDir, "reftable", "tables.list"),
path.join(commonDir, "opencode"),
...(ref ? [path.join(commonDir, ref), path.join(gitDir, ref)] : []),
process.env.GIT_CONFIG_GLOBAL || path.join(home, ".gitconfig"),
path.join(process.env.XDG_CONFIG_HOME || path.join(home, ".config"), "git", "config"),
...(nosystem ? [] : [process.env.GIT_CONFIG_SYSTEM || "/etc/gitconfig"]),
]
const stamps = yield* Effect.forEach([dotGit, ...files], stamp, { concurrency: "unbounded" })
return [gitDir, commonDir, head, ...files, ...stamps].join("\0")
})
const command = (cwd: string, args: string[]) =>
execute(cwd, proc, args).pipe(Effect.mapError((cause) => new CommandError({ args, cause })))
const remote = Effect.fn("Git.remote.get")(function* (repository: Repository, name = "origin") {
const result = yield* run(repository.worktree, proc, ["remote", "get-url", name])
if (result.exitCode !== 0) return undefined
const args = ["remote", "get-url", name]
const result = yield* command(repository.worktree, args)
// Exit 2: no such remote.
if (result.exitCode === 2) return undefined
if (result.exitCode !== 0) return yield* new CommandError({ args, exitCode: result.exitCode })
return result.text.trim() || undefined
})
const roots = Effect.fn("Git.history.rootCommits")(function* (repository: Repository) {
const result = yield* run(repository.worktree, proc, ["rev-list", "--max-parents=0", "HEAD"])
if (result.exitCode !== 0) return []
return result.text
.split("\n")
.map((item) => item.trim())
.filter(Boolean)
.toSorted()
const args = ["rev-list", "--max-parents=0", "HEAD"]
const result = yield* command(repository.worktree, args)
if (result.exitCode === 0)
return result.text
.split("\n")
.map((item) => item.trim())
.filter(Boolean)
.toSorted()
// `rev-parse -q --verify` exits 1 only when HEAD doesn't resolve, i.e. it is unborn.
const verify = yield* command(repository.worktree, ["rev-parse", "-q", "--verify", "HEAD"])
if (verify.exitCode === 1) return []
return yield* new CommandError({ args, exitCode: result.exitCode })
})
const head = Effect.fn("Git.history.head")(function* (repository: Repository) {
@@ -1291,7 +1363,7 @@ const layer = Layer.effect(
})
return Service.of({
repo: { discover, clone, create },
repo: { discover, fingerprint, clone, create },
remote: { get: remote },
history: { head, branch, defaultRemoteBranch: remoteHead, rootCommits: roots },
sync: { fetchRemotes: fetch, fetchBranch, checkoutRemoteBranch: checkout, resetHard: reset },
+49 -22
View File
@@ -1,6 +1,6 @@
export * as Project from "./project.js"
import { Context, Duration, Effect, Layer, Schedule, Schema } from "effect"
import { Cache, Context, Duration, Effect, Layer, Schedule, Schema } from "effect"
import { ChildProcess } from "effect/process"
import { and, asc, desc, eq, gte, isNotNull, isNull, lt, lte, ne, sql } from "drizzle-orm"
import path from "path"
@@ -320,6 +320,44 @@ const layer = Layer.effect(
return root ? ID.make(root) : undefined
})
// Failures resolve like dev (no remote / no root / own worktree) but aren't cached.
const gitDiscover = Effect.fnUntraced(function* (dotGit: AbsolutePath) {
const repo = yield* git.repo.discover(AbsolutePath.make(path.dirname(dotGit)))
if (!repo) return { resolved: undefined, cacheable: false }
let cacheable = true
const fallback = <A>(value: A) =>
Effect.orElseSucceed(() => {
cacheable = false
return value
})
const previous = yield* cached(repo.commonDirectory)
const id =
(yield* remote(repo).pipe(fallback(undefined))) ??
previous ??
(yield* rootCommit(repo).pipe(fallback(undefined)))
const canonical =
repo.gitDirectory === repo.commonDirectory
? repo.worktree
: yield* git.worktree.list(repo).pipe(
Effect.map((items) => items.find((item) => item.kind === "main")?.directory ?? repo.worktree),
fallback(repo.worktree),
)
const resolved: Resolved = {
previous,
id: id ?? ID.global,
directory: repo.worktree,
canonical,
vcs: { type: "git" as const, store: repo.commonDirectory },
}
return { resolved, cacheable }
})
// Keyed by `.git` path plus its metadata fingerprint, so repeat resolves spawn no git.
const gitCache = yield* Cache.make({
capacity: 512,
lookup: (key: string) => gitDiscover(AbsolutePath.make(key.slice(0, key.indexOf("\u0000")))),
})
// Mercurial identity uses the cached ID or the first root changeset; remote-derived
// identity (the git `remote()` path) is a follow-up.
const hgRoot = Effect.fnUntraced(function* (worktree: AbsolutePath) {
@@ -362,27 +400,16 @@ const layer = Layer.effect(
Effect.map((matches) => matches[0]),
Effect.orElseSucceed(() => undefined),
)
const repo =
native && path.basename(native) === ".git"
? yield* git.repo.discover(AbsolutePath.make(path.dirname(native)))
: undefined
if (repo) {
const previous = yield* cached(repo.commonDirectory)
const id = (yield* remote(repo)) ?? previous ?? (yield* rootCommit(repo))
const canonical =
repo.gitDirectory === repo.commonDirectory
? repo.worktree
: yield* git.worktree.list(repo).pipe(
Effect.map((items) => items.find((item) => item.kind === "main")?.directory ?? repo.worktree),
Effect.orElseSucceed(() => repo.worktree),
)
return yield* save({
previous,
id: id ?? ID.global,
directory: repo.worktree,
canonical,
vcs: { type: "git" as const, store: repo.commonDirectory },
})
if (native && path.basename(native) === ".git") {
const dotGit = AbsolutePath.make(native)
const fingerprint = yield* git.repo.fingerprint(dotGit)
const key = `${dotGit}\u0000${fingerprint}`
const discovered = fingerprint
? yield* Cache.get(gitCache, key).pipe(
Effect.tap((entry) => (entry.cacheable ? Effect.void : Cache.invalidate(gitCache, key))),
)
: yield* gitDiscover(dotGit)
if (discovered.resolved) return yield* save(discovered.resolved)
}
const hg = native && path.basename(native) === ".hg" ? yield* hgDiscover(AbsolutePath.make(native)) : undefined
+3 -1
View File
@@ -136,7 +136,9 @@ const layer = Layer.effect(
yield* cacheOperation(fs.ensureDir(path.dirname(localPath)), "ensure cache directory", localPath)
const existing = yield* git.repo.discover(AbsolutePath.make(localPath))
const origin = existing ? yield* git.remote.get(existing) : undefined
const origin = existing
? yield* git.remote.get(existing).pipe(Effect.orElseSucceed(() => undefined))
: undefined
const originReference = origin ? Repository.parse(origin) : undefined
// Discovery walks upward, so an enclosing repository with a
// matching origin could masquerade as the cache entry; reuse
+24
View File
@@ -0,0 +1,24 @@
import { Effect, Layer, PlatformError } from "effect"
import { ChildProcessSpawner } from "effect/process"
import { CrossSpawnSpawner } from "@opencode/util/cross-spawn-spawner"
/** Counts process spawns; `fail` rejects matching spawns with a transient error. */
export function countingSpawner(fail?: (args: readonly string[]) => boolean) {
const state = { spawns: 0 }
const node = CrossSpawnSpawner.node.replace(
CrossSpawnSpawner.node.mapLayer((base) =>
Layer.effect(
ChildProcessSpawner.ChildProcessSpawner,
Effect.map(ChildProcessSpawner.ChildProcessSpawner, (spawner) =>
ChildProcessSpawner.make((command) => {
state.spawns++
if (fail && command._tag === "StandardCommand" && fail(command.args))
return Effect.fail(PlatformError.systemError({ _tag: "Busy", module: "ChildProcess", method: "spawn" }))
return spawner.spawn(command)
}),
),
).pipe(Layer.provide(base)),
),
)
return { node, spawns: () => state.spawns }
}
+46 -1
View File
@@ -7,7 +7,7 @@ import { LayerNode } from "@opencode/util/effect/layer-node"
import { Git } from "@opencode/core/git"
import { AbsolutePath, RelativePath } from "@opencode/core/schema"
import { VcsPatch } from "@opencode/core/vcs/patch"
import { branch, commit, initRepo, read, withRemote } from "./fixture/git"
import { branch, commit, initGit, initRepo, read, withRemote } from "./fixture/git"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
@@ -103,6 +103,51 @@ describe("Git worktrees", () => {
expect((yield* git.worktree.list(repo)).some((entry) => entry.directory.endsWith("-git-worktree"))).toBe(false)
}),
)
it.live("reads no remote and no root commits from an unborn repository", () =>
Effect.gen(function* () {
const root = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
)
yield* Effect.promise(() => initGit(root.path))
const git = yield* Git.Service
const repo = yield* git.repo.discover(AbsolutePath.make(root.path))
if (!repo) throw new Error("Repository not found")
expect(yield* git.remote.get(repo)).toBeUndefined()
expect(yield* git.history.rootCommits(repo)).toEqual([])
}),
)
it.live("fingerprint changes with repository metadata, including from a linked worktree", () =>
Effect.gen(function* () {
const root = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
)
const main = path.join(root.path, "main")
const linked = path.join(root.path, "linked")
yield* Effect.promise(() => initGit(main))
const git = yield* Git.Service
const fingerprint = (dir: string) => git.repo.fingerprint(AbsolutePath.make(path.join(dir, ".git")))
expect(yield* fingerprint(root.path)).toBeUndefined()
const unborn = yield* fingerprint(main)
expect(unborn).toBeDefined()
expect(yield* fingerprint(main)).toBe(unborn)
yield* Effect.promise(() => $`git commit --allow-empty -qm root`.cwd(main).quiet())
const committed = yield* fingerprint(main)
expect(committed).not.toBe(unborn)
yield* Effect.promise(() => $`git worktree add -q ${linked}`.cwd(main).quiet())
const before = yield* fingerprint(linked)
expect(before).toBeDefined()
yield* Effect.promise(() => $`git remote add origin git@github.com:owner/repo.git`.cwd(main).quiet())
expect(yield* fingerprint(main)).not.toBe(committed)
expect(yield* fingerprint(linked)).not.toBe(before)
}),
)
})
describe("Git trees", () => {
+89 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect } from "bun:test"
import { $ } from "bun"
import fs from "fs/promises"
import path from "path"
import { Effect, Stream } from "effect"
import { Effect, Layer, Stream } from "effect"
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
import { LayerNode } from "@opencode/util/effect/layer-node"
import { Bus } from "@opencode/core/bus"
@@ -13,6 +13,7 @@ import { ProjectTable } from "@opencode/core/project/sql"
import { AbsolutePath } from "@opencode/core/schema"
import { Hash } from "@opencode/util/hash"
import { initGit } from "./fixture/git"
import { countingSpawner } from "./fixture/spawn"
import { tmpdir } from "./fixture/tmpdir"
import { testEffect } from "./lib/effect"
@@ -747,4 +748,91 @@ describe("Project.resolve", () => {
})
}),
)
const counted = (fail?: (args: readonly string[]) => boolean) => {
const counter = countingSpawner(fail)
return {
spawns: counter.spawns,
layer: AppNodeBuilder.build(LayerNode.group([Project.node, Database.node, Bus.node]), [counter.node]),
}
}
testEffect(Layer.empty).live("reuses git discovery until git metadata changes", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
const nested = path.join(tmp.path, "packages", "app")
yield* Effect.promise(async () => {
await initRepo(tmp.path, { commit: true })
await fs.mkdir(nested, { recursive: true })
})
const rootSha = Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path)))
const { layer, spawns } = counted()
yield* Effect.gen(function* () {
const project = yield* Project.Service
expect((yield* project.resolve(abs(nested))).id).toBe(rootSha)
expect(spawns()).toBeGreaterThan(0)
const before = spawns()
expect((yield* project.resolve(abs(nested))).id).toBe(rootSha)
expect((yield* project.resolve(abs(tmp.path))).id).toBe(rootSha)
expect(spawns()).toBe(before)
yield* Effect.promise(() => $`git remote add origin git@github.com:owner/repo.git`.cwd(tmp.path).quiet())
expect((yield* project.resolve(abs(nested))).id).toBe(remoteID("github.com/owner/repo"))
expect(spawns()).toBeGreaterThan(before)
}).pipe(Effect.provide(layer))
}),
)
testEffect(Layer.empty).live("does not cache a transient git failure", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
yield* Effect.promise(() => initRepo(tmp.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
let failRemote = true
const { layer } = counted((args) => {
if (!failRemote || !args.includes("remote")) return false
failRemote = false
return true
})
yield* Effect.gen(function* () {
const project = yield* Project.Service
// Falls back like dev: the remote spawn failed, so identity comes from the root commit.
expect((yield* project.resolve(abs(tmp.path))).id).toBe(
Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))),
)
expect((yield* project.resolve(abs(tmp.path))).id).toBe(remoteID("github.com/owner/repo"))
}).pipe(Effect.provide(layer))
}),
)
testEffect(Layer.empty).live("sees the first commit in a reftable repository", () =>
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
const supported = yield* Effect.promise(
async () => (await $`git init -q --ref-format=reftable ${tmp.path}`.nothrow().quiet()).exitCode === 0,
)
if (!supported) return
yield* Effect.gen(function* () {
const project = yield* Project.Service
expect((yield* project.resolve(abs(tmp.path))).id).toBe(Project.ID.global)
yield* Effect.promise(() =>
$`git -c user.name=Test -c user.email=test@opencode.test -c commit.gpgsign=false commit --allow-empty -qm root`
.cwd(tmp.path)
.quiet(),
)
expect((yield* project.resolve(abs(tmp.path))).id).toBe(
Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))),
)
}).pipe(Effect.provide(counted().layer))
}),
)
})
+35
View File
@@ -37,6 +37,7 @@ import { Expected } from "./lib/session-message"
import { testEffect } from "./lib/effect"
import { LocationServiceMap } from "@opencode/core/location-service-map"
import { initGit } from "./fixture/git"
import { countingSpawner } from "./fixture/spawn"
import { offlineModels } from "./fixture/models"
import { promptLocationNode } from "./fixture/prompt-location"
import { globalProjectNode } from "./lib/project"
@@ -131,6 +132,40 @@ describe("Session.create", () => {
),
)
testEffect(Layer.empty).live("repeat Session.create on the same location spawns no git", () =>
withTmp((directory) =>
Effect.gen(function* () {
yield* Effect.promise(() => initGit(directory))
const counter = countingSpawner()
const layer = AppNodeBuilder.build(
LayerNode.group([
Database.node,
Bus.node,
Project.node,
LocationServiceMap.node,
SessionProjector.node,
SessionStore.node,
Session.node,
]),
[
Bus.node.replace(Bus.configured({ persist: true })),
SessionExecution.node.replace(SessionExecution.noopLayer),
offlineModels,
counter.node,
],
)
yield* Effect.gen(function* () {
const sessions = yield* Session.Service
const ref = Location.Ref.make({ directory: AbsolutePath.make(directory) })
yield* sessions.create({ location: ref })
const before = counter.spawns()
yield* sessions.create({ location: ref })
expect(counter.spawns()).toBe(before)
}).pipe(Effect.provide(layer))
}),
),
)
projectIt.live("follows the directory's project identity established after creation", () =>
withTmp((directory) =>
Effect.gen(function* () {