UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

199 lines • 9.56 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 { refuseAmbiguous, remoteBranch, trackMode } from "./branch.js"; import { moveHead } from "./checkout.js"; import { HEAD } from "./constants.js"; import { BranchExistsError, BranchExpectedError, DetachWithCreateError, GitError, InvalidBranchNameError, InvalidReferenceError, MissingBranchArgumentError, OneReferenceError, RefLockError, RefReadOnlyError, } from "./errors.js"; import { readIndex, refuseUnresolved } from "./index_file.js"; import { BRANCH_PREFIX, blockingRef, loadRefs, readHead, REMOTE_PREFIX, setHead, TAG_PREFIX, validBranchName, } from "./refs.js"; import { indexLocked, opened } from "./session.js"; import { COMMIT, noteAmbiguity, resolveCommit, TAG } from "./revparse.js"; import { checkSwitches, requireVfs } from "./util.js"; const ENC = new TextEncoder(); const REMOTE_BRANCH = 'remote branch'; /** Read the raw switch flag kwargs into a frozen struct. */ function parseFlags(fl) { return { create: fl.asStr('create'), detach: fl.asBool('detach') }; } /** * What a non-branch operand named, for the refusal that says so. * * git tells a tag and a remote-tracking branch apart from a bare commit in the * same sentence, so the refusal can say which one the caller reached for. */ export function expectedKind(known, name) { if (known.has(`${TAG_PREFIX}${name}`)) return TAG; if (known.has(`${REMOTE_PREFIX}${name}`)) return REMOTE_BRANCH; return COMMIT; } /** * Switch to a branch, creating it under `-c`. * * The same move `checkout` makes, with a narrower grammar: only a branch is * accepted, so a commit, tag or remote-tracking name is refused unless * `--detach` says that a detached HEAD is what was meant. That is the whole * reason git split the verb off, and it holds here for the same reason: a * detached HEAD is the state an agent loses commits in. */ export async function switchBranch(_accessor, paths, texts, ctx) { const fl = new FlagView(ctx.flags); const vfs = requireVfs(ctx); checkSwitches(ctx, texts); const flags = parseFlags(fl); let creating = flags.create !== undefined; if (creating && flags.detach) throw new DetachWithCreateError(); if (texts.length > 1) throw new OneReferenceError(); const first = texts[0]; // A detach takes HEAD when nothing is named, which is git's own default; // only an attaching switch needs a branch to name. if (!creating && first === undefined && !flags.detach) { throw new MissingBranchArgumentError(); } const repo = await opened(ctx, fl, true); const mode = await trackMode(repo); const head = await readHead(vfs, repo.location.gitdir); const known = await loadRefs(vfs, repo.location.gitdir, repo.location.commondir); let target; let oid; let attached; let startPoint; if (flags.create !== undefined) { target = flags.create; startPoint = first; // Before the first commit there is nothing for HEAD to resolve to, and // git makes the branch anyway: the line only repoints symbolic HEAD, // writing neither the ref nor a reflog line, because a branch with no // commit is a name and nothing else. A start point is a different line // and stays unresolvable (`fatal: invalid reference: main`), so this // reads the shape of the line rather than probing HEAD. Pinned against // git 2.50.1. const unborn = first === undefined && head.ref !== null && !known.has(head.ref); oid = ''; if (first !== undefined) { try { oid = await resolveCommit(repo, first); } catch (err) { if (err instanceof GitError) throw new InvalidReferenceError(first); throw err; } await refuseAmbiguous(repo, first, false); } // After a start point that was named and before HEAD is read or anything // is written, which is git's own order, and before it asks whether the // branch exists, so a refs/heads/HEAD already there is still refused as a // name. A ref is a path below .git, so an unchecked name reaches writeRef // as one: -c ../../config would land on the repository's own // configuration rather than on a branch. if (!validBranchName(target)) throw new InvalidBranchNameError(target); if (known.has(`${BRANCH_PREFIX}${target}`)) throw new BranchExistsError(target); if (first === undefined && !unborn) { try { oid = await resolveCommit(repo, HEAD); } catch (err) { if (err instanceof GitError) throw new InvalidReferenceError(HEAD); throw err; } } // And before the working tree moves: git takes the ref lock first, so a // name whose path another ref holds refuses with nothing checked out. const held = blockingRef(new Set(known.keys()), `${BRANCH_PREFIX}${target}`); if (held !== null) throw new RefLockError(`${BRANCH_PREFIX}${target}`, held); attached = true; if (unborn) { await setHead(vfs, repo.location.gitdir, `${BRANCH_PREFIX}${target}`); return [ ENC.encode(''), new IOResult({ stderr: fl.asBool('quiet') ? new Uint8Array() : ENC.encode(`Switched to a new branch '${target}'\n`), }), ]; } } else { target = first ?? HEAD; // The ref has to exist, not just be the name HEAD carries. A fresh // repository's HEAD names a branch that has never been written, and // reading the name alone answered `Already on 'main'` at exit 0 for a // branch with no commit to be on. git resolves it first and dies, which // is what leaves `switch -c` as the only line an unborn HEAD accepts. // Pinned against git 2.50.1. if (!flags.detach && target === head.branch && known.has(`${BRANCH_PREFIX}${target}`)) { await noteAmbiguity(repo, target); // Moving nothing is not the same as having nothing to check: git dies // on an unresolved index here too, so the shortcut reads it before it // answers. Every ref check above comes first, which is git's own order. refuseUnresolved(await readIndex(repo, vfs)); return [ null, new IOResult({ stderr: fl.asBool('quiet') ? new Uint8Array() : ENC.encode(`Already on '${target}'\n`), }), ]; } // git's --guess, on by default: no such branch here but one remote has // it, so the switch creates it tracking that one. const guessed = flags.detach || known.has(`${BRANCH_PREFIX}${target}`) ? null : await remoteBranch(repo, target); creating = guessed !== null; startPoint = guessed ?? undefined; // A branch of that name wins over every other reading, as git's switch // reads it, though the name is still reported as ambiguous. const branch = `${BRANCH_PREFIX}${target}`; const local = !flags.detach && guessed === null && known.has(branch); if (local) await noteAmbiguity(repo, target); try { oid = await resolveCommit(repo, local ? branch : (guessed ?? target)); } catch (err) { if (err instanceof GitError) throw new InvalidReferenceError(target); throw err; } attached = creating || (!flags.detach && known.has(`${BRANCH_PREFIX}${target}`)); if (!flags.detach && !attached) { throw new BranchExpectedError(expectedKind(known, target), target); } } return await moveHead(vfs, repo, known, head, oid, target, attached ? `${BRANCH_PREFIX}${target}` : null, creating, startPoint, mode, false, fl.asBool('quiet')); } /** * switch's refusal by a read-only mount: the new branch's ref under a `-c` with * no start point, whose lock git takes first, and the index's otherwise. */ export const switchReadOnly = (texts, ctx, location) => { const name = new FlagView(ctx.flags, ctx.spec).asStr('create'); if (name === undefined || texts.length > 0) return indexLocked(texts, ctx, location); const ref = `${BRANCH_PREFIX}${name}`; return new RefReadOnlyError(ref, (location?.commondir ?? PathSpec.fromStrPath('/.git')).join(ref).virtual); }; //# sourceMappingURL=switch.js.map