UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

134 lines • 5.67 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 { GitIndexManager } from 'isomorphic-git/managers'; import { FileSystem } from 'isomorphic-git/models'; import { ResolveIndexError } from "./errors.js"; import { exists } from "./io.js"; const MERGE_HEAD = 'MERGE_HEAD'; function stageOf(entry) { return entry.stage ?? entry.flags?.stage ?? 0; } function toEntry(entry) { return { path: entry.path, oid: entry.oid, mode: entry.mode, size: entry.size, stage: stageOf(entry), }; } /** * Run a closure against the live index, writing it back if it was changed. * * A fresh cache per call on purpose: the index is re-read every time rather than * memoized, because a mirage mutation between two verbs happens outside * isomorphic-git's knowledge and a shared cache would replay the old one. * * The index lives in `gitdir`, never `commondir`: a linked worktree stages its * own content, and pointing this at the shared directory would show one * checkout's staged changes in another. */ async function withIndex(repo, closure) { // Wrapped rather than passed raw: the public API wraps every fs it is given // before handing it to a manager, and a manager calls the wrapper's own // methods (`lstat`, `readdir` returning null on a miss) rather than the plain // ones the plugin declares. return GitIndexManager.acquire({ fs: new FileSystem(repo.fs), gitdir: repo.location.gitdir.virtual, cache: {} }, closure); } /** * Read `.git/index` through the dispatcher. * * The index is the third thing `status` compares, and the only one that is a * single file: HEAD's tree is assembled from objects and the working tree is * walked, but staged content is one binary blob. * * An absent index is not an error. `git init` writes none until the first * `git add`, and every path is then untracked, which is what an empty table * already says. */ export async function readIndex(repo, vfs) { const merging = await exists(vfs, repo.location.gitdir.join(MERGE_HEAD)); const entries = new Map(); const conflicts = new Map(); const rows = await withIndex(repo, (index) => index.entriesFlat.map(toEntry)); for (const row of rows) { if (row.stage === 0) { entries.set(row.path, row); continue; } // Conflicted paths are carried apart rather than dropped, because a dropped // one reads as unmodified: the file would compare equal to nothing and // vanish from the report while git is refusing to commit because of it. const held = conflicts.get(row.path) ?? { ancestor: null, this: null, other: null }; conflicts.set(row.path, { ancestor: row.stage === 1 ? row : held.ancestor, this: row.stage === 2 ? row : held.this, other: row.stage === 3 ? row : held.other, }); } return { entries, conflicts, merging }; } /** * Replace the staged entries, leaving conflicts alone. * * The stat cache is written zeroed, which git reads as "do not trust it": it * then compares content rather than believing a size or an mtime mirage cannot * promise is meaningful. A zeroed `size` therefore means "not stated" to every * reader here, never "empty". * * @param repo the opened repository * @param staged the paths to set, mapped to what should be recorded for them * @param removed the paths to drop from the index entirely */ export async function updateIndex(repo, staged, removed = []) { await withIndex(repo, (index) => { for (const path of removed) index.delete({ filepath: path }); for (const [path, entry] of staged) { index.insert({ filepath: path, oid: entry.oid, stats: { ctimeSeconds: 0, ctimeNanoseconds: 0, mtimeSeconds: 0, mtimeNanoseconds: 0, dev: 0, ino: 0, mode: entry.mode, uid: 0, gid: 0, size: entry.size, }, }); } }); } /** * Refuse a branch move while the index still records conflict stages. * * Every collision check a checkout makes reads stage 0, so a path held only as * stages 1-3 is invisible to all of them, and git refuses before it reads * either tree. The refusal is not the moving side's alone: `git switch * <current>` and `git checkout <current>` move nothing and still die on it, so * a line that answers "Already on" out of a shortcut has to ask the index * first. Pinned against git 2.50.1, where `git switch -c <new>` is the one line * that survives an unresolved index, because it writes a ref and nothing else. */ export function refuseUnresolved(state) { if (state.conflicts.size === 0) return; throw new ResolveIndexError([...state.conflicts.keys()]); } //# sourceMappingURL=index_file.js.map