UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

141 lines • 6.81 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 { PathSpec } from "../../../../types.js"; import { IOResult } from "../../../../io/types.js"; import { FlagView } from "../../../spec/flag_view.js"; import { identity } from "./commit.js"; import { HEAD } from "./constants.js"; import { BadRefNameUpdateError, DeleteHeadError, EmptyUpdateMessageError, HeadOutsideRefsError, InvalidSymbolicTargetError, NoSuchRefError, NotASymbolicRefError, NotSymbolicDeleteError, SymbolicRefLockError, SymbolicRefReadOnlyError, UsageError, } from "./errors.js"; import { removeFile, writeFile } from "./io.js"; import { shortenRef } from "./ref_fields.js"; import { append, entry, logged, LOGS_DIR, ZERO } from "./reflog.js"; import { blockingRef, deleteRef, loadRefs, rawRef, resolveSymbolic, safeRefName, SYMREF_PREFIX, wholeRefName, } from "./refs.js"; import { opened } from "./session.js"; import { checkSwitches, verbUsage } from "./util.js"; import { systemNow } from "../../../../utils/clock.js"; const ENC = new TextEncoder(); const REFS_PREFIX = 'refs/'; /** * The last of an option and its `--no-` twin on the line, the fallback when * neither is there. */ function switched(fl, name, fallback) { let value = fallback; for (const [key] of fl.occurrences(name, `no_${name}`)) value = key === name; return value; } /** * The git directory a ref lives in: HEAD and the one-level names belong to the * checkout, every `refs/` name to the repository its worktrees share. */ function ownerOf(location, name) { return name === HEAD || !name.startsWith(REFS_PREFIX) ? location.gitdir : location.commondir; } /** symbolic-ref's refusal by a read-only mount, at the lock on the ref it names. */ export const symbolicRefReadOnly = (texts, _ctx, location) => { const name = texts[0] ?? ''; return new SymbolicRefReadOnlyError(name, (location === null ? PathSpec.fromStrPath('/.git') : ownerOf(location, name)).join(name) .virtual); }; /** The object id a ref ends at, through any symbolic hops; null for none. */ async function objectOf(repo, table, name) { const end = await resolveSymbolic(repo.vfs, repo.location.gitdir, table, name, true); if (end === null) return null; const raw = await rawRef(repo.vfs, repo.location.gitdir, table, end.name); return raw === null || raw.startsWith(SYMREF_PREFIX) ? null : raw; } /** * Point a ref at another one, symbolically, and log the move. * * The log line runs from what the ref resolved to before to what its new target * resolves to, and is skipped while that target names nothing yet, as git skips * it for a dangling symbolic ref. An empty message leaves the line without one. */ async function setSymbolic(repo, table, name, target, who, message) { if (!safeRefName(name)) throw new BadRefNameUpdateError(name); const held = blockingRef(new Set(table.keys()), name); if (held !== null) throw new SymbolicRefLockError(name, held); const before = await objectOf(repo, table, name); const owner = ownerOf(repo.location, name); await writeFile(repo.vfs, owner.join(name), ENC.encode(`${SYMREF_PREFIX}${target}\n`)); const after = await objectOf(repo, table, target); const log = owner.join(LOGS_DIR, name); if (after === null || !(await logged(repo.vfs, repo.location, name, log))) return; await append(repo.vfs, owner, `${LOGS_DIR}/${name}`, entry(before ?? ZERO, after, who, Math.floor(systemNow()), message)); } /** * `git symbolic-ref`: read, change or delete a symbolic ref (pinned against git * 2.47.3). * * One operand prints the ref it points at, followed to the end of the chain * unless `--no-recurse`, shortened by `--short`; a ref that is not symbolic is a * fatal, or a bare exit 1 under `-q`. Two operands point the first at the * second, which HEAD may only do inside `refs/`. `-d` removes a symbolic ref * and its log, never HEAD, and `-q` does not quiet its refusal. */ export async function symbolicRef(_accessor, paths, texts, ctx) { const fl = new FlagView(ctx.flags); checkSwitches(ctx, texts); const repo = await opened(ctx, fl); const message = fl.asStr('m'); if (message === '') throw new EmptyUpdateMessageError(); const quiet = switched(fl, 'quiet', false); const table = await loadRefs(repo.vfs, repo.location.gitdir, repo.location.commondir); const [name = '', target] = texts; if (switched(fl, 'delete', false)) { if (texts.length !== 1) throw new UsageError('', verbUsage(ctx)); const found = await resolveSymbolic(repo.vfs, repo.location.gitdir, table, name, false); if (found === null) throw new NoSuchRefError(name); if (!found.symbolic) throw new NotSymbolicDeleteError(name); if (name === HEAD) throw new DeleteHeadError(); const owner = ownerOf(repo.location, name); await deleteRef(repo.vfs, owner, name); await removeFile(repo.vfs, owner.join(LOGS_DIR, name)); return [null, new IOResult()]; } if (texts.length === 2 && target !== undefined) { if (name === HEAD && !target.startsWith(REFS_PREFIX)) throw new HeadOutsideRefsError(); if (!wholeRefName(target)) throw new InvalidSymbolicTargetError(name, target); const who = identity(fl, ctx.session).line; await setSymbolic(repo, table, name, target, who, message ?? ''); return [null, new IOResult()]; } if (texts.length !== 1) throw new UsageError('', verbUsage(ctx)); const found = await resolveSymbolic(repo.vfs, repo.location.gitdir, table, name, switched(fl, 'recurse', true)); if (found === null) throw new NoSuchRefError(name); if (!found.symbolic) { if (quiet) return [null, new IOResult({ exitCode: 1 })]; throw new NotASymbolicRefError(name); } const shown = switched(fl, 'short', false) ? shortenRef(found.name, new Set(table.keys()), false) : found.name; return [ENC.encode(`${shown}\n`), new IOResult()]; } //# sourceMappingURL=symbolic_ref.js.map