UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

133 lines • 5.92 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 { visibleEntries, matched, repoRelative } from "./pathspec.js"; import { IOResult } from "../../../../io/types.js"; import { FlagView } from "../../../spec/flag_view.js"; import { headEntries, workChanges } from "./changes.js"; import { isBare, requireWorkTree } from "./discover.js"; import { AmbiguousArgumentError, BareResetError, GitError, RevisionResetError } from "./errors.js"; import { readIndex, updateIndex } from "./index_file.js"; import { opened } from "./session.js"; import { resolveCommit } from "./revparse.js"; import { checkSwitches, startPoint, requireVfs } from "./util.js"; import { scan, UNTRACKED_NO } from "./worktree.js"; import { compareCodePoints } from "../../../../utils/sort.js"; const ENC = new TextEncoder(); const UNSTAGED_HEADER = 'Unstaged changes after reset:'; /** * Which fatal an operand that selected no path deserves. * * Two different mistakes reach here and git words them differently. A typo * names neither a revision nor a path, and git calls that ambiguous. A revision * names something real that this build cannot reset to, and answering "unknown * revision" for a revision it can resolve perfectly well would send the caller * looking for the wrong problem. */ async function unmatched(repo, operand) { try { await resolveCommit(repo, operand); } catch (err) { if (err instanceof GitError) return new AmbiguousArgumentError(operand); throw err; } return new RevisionResetError(operand); } /** * An index entry putting a path back to what HEAD records. * * The stat fields are zeroed for the same reason staging zeroes them: a mount * serves none of them meaningfully, and git reads a zeroed entry as one whose * cache it should not trust rather than as a corrupt one. The size is zero * because the blob was never read; every reader here takes that as "not stated". */ export function restored(oid, mode) { return { oid, mode, size: 0 }; } /** * Put the index back to what HEAD records, staging nothing. * * The working tree is never touched: this is `git reset` in its default mixed * mode, which unstages. `--hard` is deliberately not offered, because it * destroys uncommitted work and there is no reflog here to recover it from. * * A pathspec limits the reset to those paths, which is how a single file is * unstaged. */ export async function reset(_accessor, paths, texts, ctx) { const fl = new FlagView(ctx.flags); const vfs = requireVfs(ctx); checkSwitches(ctx, texts); const repo = await opened(ctx, fl); const named = fl.asPath('work_tree') !== undefined; if (!named && (await isBare(vfs, repo.location))) throw new BareResetError(); await requireWorkTree(vfs, repo.location, named); const state = await readIndex(repo, vfs); const tree = (await headEntries(repo)) ?? new Map(); const start = startPoint(fl).virtual; const names = new Set([ ...visibleEntries(repo.location, tree).keys(), ...visibleEntries(repo.location, state.entries).keys(), ]); let selected; if (texts.length > 0) { selected = new Set(); for (const operand of texts) { const hits = matched(names, repoRelative(repo.location, start, operand)); if (hits.size === 0) throw await unmatched(repo, operand); for (const path of hits) selected.add(path); } } else { selected = names; } const staged = new Map(); const removed = []; for (const name of selected) { const recorded = tree.get(name); if (recorded === undefined) removed.push(name); else staged.set(name, restored(recorded.oid, Number.parseInt(recorded.mode, 8))); } if (texts.length === 0) for (const item of visibleEntries(repo.location, state.conflicts).keys()) removed.push(item); // The tree is walked before the index is written, so a work tree past its // walk_limit leaves the index as it was. const tracked = new Set(visibleEntries(repo.location, state.entries).keys()); for (const name of removed) tracked.delete(name); for (const name of staged.keys()) tracked.add(name); const found = await scan(vfs, repo.location, tracked, UNTRACKED_NO); await updateIndex(repo, staged, removed); const after = await readIndex(repo, vfs); const unstaged = await workChanges(repo, vfs, repo.location.worktree, visibleEntries(repo.location, after.entries), found); if (unstaged.size === 0 || fl.asBool('quiet')) return [null, new IOResult()]; const lines = [UNSTAGED_HEADER]; // Python is `sorted(unstaged.items())`, a tuple sort: path then letter. // A bare .sort() here compared `${path},${letter}` instead, which orders // `a+b` before `a` because ',' outranks '+'. const entries = [...unstaged.entries()].sort((a, b) => compareCodePoints(a[0], b[0]) || compareCodePoints(a[1], b[1])); for (const [path, letter] of entries) lines.push(`${letter}\t${path}`); return [ENC.encode(lines.map((line) => `${line}\n`).join('')), new IOResult()]; } //# sourceMappingURL=reset.js.map