@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
134 lines • 6.4 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 { headEntries, stagedEntries, workEntries } from "./changes.js";
import { readIndex, refuseUnresolved } from "./index_file.js";
import { commitEntries, treeEntries } from "./tree.js";
import { compare, limited, renderChanges, parseDiffFlags, renamesEnabled } from "./diff_output.js";
import { HEAD } from "./constants.js";
import { IOResult } from "../../../../io/types.js";
import { FlagView } from "../../../spec/flag_view.js";
import { GitError, InvalidOptionError, NoMergeBaseError, UsageError } from "./errors.js";
import { pathspecPatterns, patternRoots } from "./pathspec.js";
import { configBool } from "./repo.js";
import { deltas, shownSides } from "./rows.js";
import { shownEntries } from "./seen.js";
import { opened } from "./session.js";
import { mergeBases, rangeCommits, resolveCommit } from "./revparse.js";
import { optionOperand, splitMarked, startPoint, STDERR, verbUsage, requireVfs } from "./util.js";
import { encodeText } from "../../../../shell/bytes.js";
const ENC = new TextEncoder();
/**
* The two sides a diff compares, and any warning.
*
* No revision compares the index with the working tree, one compares that
* revision with it, and two compare them with each other: the old side is null
* for the index, the new one for the working tree. `A..B` is the two-revision
* form written as one operand, and `A...B` compares B with the merge base of
* the two, which is what a branch changed since it forked; with several bases
* git warns and takes the first (pinned against git 2.50).
*/
async function sides(repo, texts) {
const first = texts[0];
if (first === undefined)
return [null, null, ''];
const ends = texts.length === 1 ? await rangeCommits(repo, first) : null;
if (ends === null) {
const second = texts[1];
return [
shownEntries(repo, await commitEntries(repo, await resolveCommit(repo, first))),
second === undefined
? null
: shownEntries(repo, await commitEntries(repo, await resolveCommit(repo, second))),
'',
];
}
const [left, right, symmetric] = ends;
const after = shownEntries(repo, await treeEntries(repo, right.tree));
if (!symmetric)
return [shownEntries(repo, await treeEntries(repo, left.tree)), after, ''];
const bases = await mergeBases(repo, left, right);
const base = bases[0];
if (base === undefined)
throw new NoMergeBaseError(first);
const warning = bases.length > 1 ? `warning: ${first}: multiple merge bases, using ${base.oid}\n` : '';
return [shownEntries(repo, await treeEntries(repo, base.tree)), after, warning];
}
/**
* Diff the working tree, staged content or commits.
*
* No revision compares the index with the working tree, and one compares that
* revision with it, as git does; two diff against each other. The index is
* compared with the named revision under --cached or --staged, defaulting to
* HEAD or the empty tree on an unborn branch. Operands after `--` are
* pathspecs, read once the revisions have resolved, as git reads them, and
* every format shows only the paths they name.
*/
export async function diff(_accessor, paths, texts, ctx) {
const fl = new FlagView(ctx.flags);
const cached = fl.asBool('cached') || fl.asBool('staged');
const [revisions, marked] = splitMarked(texts, ctx.argv ?? []);
const vfs = requireVfs(ctx);
const word = optionOperand(ctx, texts, STDERR);
if (word !== null && (cached || revisions.some((text) => !text.startsWith('-')))) {
throw new UsageError('', verbUsage(ctx));
}
if (word !== null)
throw new InvalidOptionError(word, verbUsage(ctx));
const repo = await opened(ctx, fl);
const parsed = parseDiffFlags(fl, true, 'off', true, await renamesEnabled(repo), await configBool(repo, 'core.quotepath', true));
let before, after;
let warning = '';
const pathspecs = pathspecPatterns(repo.location, startPoint(fl).virtual, marked);
if (cached) {
if (revisions.length > 1)
throw new GitError('--cached accepts at most one revision');
const state = await readIndex(repo, repo.vfs);
refuseUnresolved(state);
before = shownEntries(repo, revisions.length
? await commitEntries(repo, await resolveCommit(repo, revisions[0] ?? HEAD))
: ((await headEntries(repo)) ?? new Map()));
after = shownEntries(repo, stagedEntries(state));
}
else {
const [old, fresh, note] = await sides(repo, revisions);
warning = note;
if (fresh === null) {
const state = await readIndex(repo, repo.vfs);
after = await workEntries(repo, vfs, state, patternRoots(pathspecs));
if (old === null)
for (const path of state.conflicts.keys())
after.delete(path);
before = old ?? shownEntries(repo, stagedEntries(state));
}
else {
before = old ?? new Map();
after = fresh;
}
}
const flags = { ...parsed, pathspecs };
const rows = deltas(repo.location, [[limited(before, pathspecs), limited(after, pathspecs)]]);
const render = async (shown) => {
const [oldSide, newSide] = shownSides(shown, 1)[0] ?? [new Map(), new Map()];
const body = await renderChanges(repo, await compare(repo, oldSide, newSide, flags.renames), flags);
return body === '' ? null : encodeText(body);
};
return [
null,
new IOResult({
...(warning ? { stderr: ENC.encode(warning) } : {}),
rows: { items: rows, render },
}),
];
}
//# sourceMappingURL=diff.js.map