@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
223 lines • 10.2 kB
JavaScript
// ========= 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