UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

223 lines • 10.2 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { configValues } from "./fs.js"; import { GIT_DIR } from "./constants.js"; import { FileType, PathSpec } from "../../../../types.js"; import { InvalidGitFileError, NotARepositoryError, NotAWorkTreeError, NoWorkingDirectoryError, WorkTreeChdirError, } from "./errors.js"; import { readFile, readOptional, statOf } from "./io.js"; import { gitBool } from "./util.js"; const GITDIR_PREFIX = 'gitdir:'; const COMMON_DIR = 'commondir'; const DEC = new TextDecoder('utf-8', { fatal: false }); /** * Read a `.git` file and return the directory it points at. * * A `.git` that is a file rather than a directory holds one `gitdir: <path>` * line. git writes one for every linked worktree (`git worktree add`) and every * submodule, so the real git directory sits outside the tree being worked in, * and reading the file as if it were a directory is how this used to fail. */ async function followGitfile(vfs, gitfile) { const line = DEC.decode(await readFile(vfs, gitfile)).trim(); if (!line.startsWith(GITDIR_PREFIX)) throw new InvalidGitFileError(gitfile.virtual); const target = line.slice(GITDIR_PREFIX.length).trim(); if (target === '') throw new InvalidGitFileError(gitfile.virtual); const resolved = PathSpec.fromStrPath(target, undefined, gitfile.parent); if ((await statOf(vfs, resolved)) === null) { // An absolute pointer names a path on the backend's own filesystem, which // is only reachable when the mount happens to span it: a worktree mounted // alone cannot see the repository it was cut from. git says the same thing // when the target is gone. throw new NotARepositoryError(resolved.virtual, false); } return resolved; } /** * The shared git directory behind a per-worktree one. * * A linked worktree's git directory carries a `commondir` file naming the * repository it belongs to, usually as `../..`. Objects, packed-refs and * branches live there; only HEAD and the index are the worktree's own. An * ordinary checkout has no such file and is its own common directory. */ async function commonDir(vfs, gitdir) { const data = await readOptional(vfs, gitdir.join(COMMON_DIR)); if (data === null) return gitdir; const target = DEC.decode(data).trim(); return target === '' ? gitdir : PathSpec.fromStrPath(target, undefined, gitdir); } /** * Whether git takes this for a git directory: its common directory, or null. * * A git directory holds its own HEAD and finds objects and refs in its common * directory, which is itself unless it is a linked worktree's. */ async function validated(vfs, gitdir) { const common = await commonDir(vfs, gitdir); for (const [path, kind] of [ [gitdir.join('HEAD'), FileType.FILE], [common.join('objects'), FileType.DIRECTORY], [common.join('refs'), FileType.DIRECTORY], ]) { const entry = await statOf(vfs, path); if (entry?.type !== kind) return null; } return common; } /** Refuse a start git cannot change into, in `-C`'s words. */ async function enter(vfs, start) { const here = await statOf(vfs, start); if (here?.type === FileType.DIRECTORY) return; throw new NoWorkingDirectoryError(start.dotted ? start.rawPath : start.virtual, here === null ? 'ENOENT' : 'ENOTDIR'); } /** * Find the repository governing a path, or throw git's own fatal. * * Walks up from `start` looking for a `.git` entry, stopping at the mount root. * Real git stops discovery at a filesystem boundary unless * GIT_DISCOVERY_ACROSS_FILESYSTEM is set, and a mount prefix is exactly that * boundary: crossing it would probe a different backend for a repository that * has nothing to do with the operand. * * Existence comes from `statPath` rather than one backend's stat because on a * prefix store a directory is not an object: `.git` answers on readdir while a * point lookup misses it entirely. That is the same fact `find` asks about its * own start point. * * What is found is not always the git directory. A `.git` file points at one * elsewhere, and the directory it points at may share its objects with another, * so the three paths are resolved here and carried separately rather than * derived again by each verb. * * @param vfs the session's files * @param start absolute virtual path to start from, normally the session cwd or * the argument of `-C` */ export async function discover(vfs, start, gitdir = null, worktree = null) { const root = PathSpec.fromStrPath(vfs.mountOf(start) ?? '/', undefined, '/'); if (gitdir !== null) { await enter(vfs, start); const candidate = gitdir; const info = await statOf(vfs, candidate); if (info === null) throw new NotARepositoryError(gitdir.rawPath); // git names the target a pointer leads to unquoted, as it does for one met // on the way up. const pointer = info.type !== FileType.DIRECTORY; const resolved = pointer ? await followGitfile(vfs, candidate) : candidate; const common = await validated(vfs, resolved); if (common === null) { throw pointer ? new NotARepositoryError(resolved.virtual, false) : new NotARepositoryError(gitdir.rawPath); } return location(vfs, resolved, common, start, worktree, root); } let current = start; let first = true; for (;;) { const candidate = current.join(GIT_DIR); const info = await statOf(vfs, candidate); if (info !== null) { const gitdir = info.type === FileType.DIRECTORY ? candidate : await followGitfile(vfs, candidate); return location(vfs, gitdir, await commonDir(vfs, gitdir), current, worktree, root); } if (first) { // git enters `-C` before it looks for anything, so a path it cannot // enter fails on its own terms even when a directory above it holds a // repository. A file counts as one it cannot enter: tolerating it would // walk up and run in the parent repository, which for a write verb means // mutating a repository the caller did not name. Asked only after the // first probe missed, because a hit already proves the directory is // there. await enter(vfs, start); first = false; } if (current.virtual === root.virtual || current.virtual === '/') throw new NotARepositoryError(); current = current.parent; } } /** * Resolve the work tree once for every verb, after locating metadata. * * CLI/environment paths are relative to -C; core.worktree is relative to the * git directory, as in native git 2.54.0, which enters a relative one before * any verb runs. A bare repository keeps the default, and only a verb that * needs a work tree refuses it (`requireWorkTree`). git parses core.bare * whichever tree wins, so a value it cannot read fails every verb, a named work * tree and a linked worktree included. Divergence: the `config.worktree` that * `extensions.worktreeConfig` adds is not read, so a linked worktree never takes * either key; and a bare repository that also names a core.worktree stays bare * without git's "do not make sense" warning. Mirrors _location in discover.py. */ async function location(vfs, gitdir, common, defaultWorktree, worktree, root) { const located = { gitdir, commondir: common, worktree: defaultWorktree, mountRoot: root, }; const bare = gitBool(await configValues(vfs, located, 'core.bare'), 'core.bare', false); if (worktree !== null) return { ...located, worktree }; if (gitdir.virtual !== common.virtual || bare) return located; const configured = (await configValues(vfs, located, 'core.worktree')).at(-1); if (configured === undefined) return located; const selected = PathSpec.fromStrPath(configured, undefined, gitdir); if (!configured.startsWith('/')) { const info = await statOf(vfs, selected); if (info === null) throw new WorkTreeChdirError(configured); if (info.type !== FileType.DIRECTORY) throw new WorkTreeChdirError(configured, 'ENOTDIR'); } return { ...located, worktree: selected }; } /** * Whether core.bare leaves the repository without a work tree. * * git reads core.bare, like core.worktree, only from a repository's own config, * and a linked worktree's config is its repository's, so a linked worktree is * never bare. A named work tree overrides it. */ export async function isBare(vfs, location) { if (location.gitdir.virtual !== location.commondir.virtual) return false; return gitBool(await configValues(vfs, location, 'core.bare'), 'core.bare', false); } /** * Refuse, in git's words, when there is no work tree to enter. * * Asked by every verb that reads or writes working files, so a bare repository * or a mistyped `--work-tree` is refused rather than read as a tree with every * file deleted. * * @param named `--work-tree` or `GIT_WORK_TREE` chose the tree */ export async function requireWorkTree(vfs, location, named) { if (!named && (await isBare(vfs, location))) throw new NotAWorkTreeError(); const info = await statOf(vfs, location.worktree); if (info?.type !== FileType.DIRECTORY) throw new NotAWorkTreeError(); } //# sourceMappingURL=discover.js.map