@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
496 lines • 18.1 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 { byteChar } from "../../../../shell/bytes.js";
import { compareCodePoints } from "../../../../utils/sort.js";
import { strverscmp } from "../../../../utils/strverscmp.js";
import { textWidth } from "../../../../utils/width.js";
import { FormatUsageError, GitError, UnparsableFormatError } from "./errors.js";
import { fieldValue, parseField } from "./ref_fields.js";
import { QuoteStyle, RefKind, } from "./types.js";
const HEX_DIGITS = '0123456789abcdefABCDEF';
const BLOCK_FIELDS = new Set(['align', 'if', 'then', 'else', 'end']);
const SPACE = /^[ \t\n\v\f\r]*$/;
// The quoting styles that cannot carry a raw object's bytes.
const TEXT_QUOTES = [QuoteStyle.SHELL, QuoteStyle.PYTHON, QuoteStyle.TCL];
const TCL_ESCAPES = {
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\v': '\\v',
};
const VERSION_PREFIXES = ['version:', 'v:'];
// The sort a listing gets when the line names none: by name, upward.
const DEFAULT_KEY = 'refname';
/**
* A format's text between fields: `%%` is a percent sign and `%` with two hex
* digits the byte they spell; any other `%` is itself.
*/
export function literalText(text) {
let out = '';
let at = 0;
while (at < text.length) {
const ch = text.charAt(at);
if (ch === '%' && text.charAt(at + 1) === '%') {
out += '%';
at += 2;
}
else if (ch === '%' &&
at + 2 < text.length &&
HEX_DIGITS.includes(text.charAt(at + 1)) &&
HEX_DIGITS.includes(text.charAt(at + 2))) {
out += byteChar(parseInt(text.slice(at + 1, at + 3), 16));
at += 3;
}
else {
out += ch;
at += 1;
}
}
return out;
}
/** Whether the `%` at `start` is the second half of a `%%` begun at or after `at`. */
function escaped(template, at, start) {
let run = 0;
while (start - run - 1 >= at && template.charAt(start - run - 1) === '%')
run++;
return run % 2 === 1;
}
/** A template's literal runs and fields, and where an unclosed `%(` starts, null when every one closes. */
function pieces(template) {
const out = [];
let at = 0;
for (;;) {
let start = template.indexOf('%(', at);
while (start !== -1 && escaped(template, at, start))
start = template.indexOf('%(', start + 1);
if (start === -1) {
if (at < template.length)
out.push(literalText(template.slice(at)));
return [out, null];
}
const end = template.indexOf(')', start);
if (end === -1)
return [out, start];
if (at < start)
out.push(literalText(template.slice(at, start)));
out.push(parseField(template.slice(start + 2, end)));
at = end + 1;
}
}
function checkRaw(parts, quote) {
if (!TEXT_QUOTES.includes(quote))
return;
for (const piece of parts) {
if (typeof piece !== 'string' && piece.name === 'raw' && !piece.variant) {
throw new GitError(`--format=${piece.text} cannot be used with --python, --shell, --tcl`);
}
}
}
/**
* Read a `--format` string once for every ref it renders.
*
* @throws FormatUsageError a `%(` that never closes
* @throws GitError a field git refuses, or `%(raw)` under a quoting style
* that cannot carry its bytes
*/
export function parseFormat(template, quote = QuoteStyle.NONE) {
const [parts, unclosed] = pieces(template);
if (unclosed !== null) {
throw new FormatUsageError(`malformed format string ${template.slice(unclosed)}`);
}
checkRaw(parts, quote);
return { pieces: parts, quote };
}
/**
* `branch`'s and `tag`'s `--format`, which refuse a `%(` that never closes in
* two lines rather than with the usage.
*/
export function listingFormat(template) {
const [parts, unclosed] = pieces(template);
if (unclosed !== null)
throw new UnparsableFormatError(template.slice(unclosed));
return { pieces: parts, quote: QuoteStyle.NONE };
}
/** Every field a listing reads: its format's and its sort keys'. */
export function usedFields(fmt, keys) {
const fields = fmt ? fmt.pieces.filter((p) => typeof p !== 'string') : [];
return [...fields, ...keys.map((key) => key.field)];
}
/**
* The `--sort` keys, the last given first, as git orders them. Each is an
* optional `-` (descending), then an optional `version:` or `v:`, then a
* field.
*/
export function parseSortKeys(spellings) {
const keys = [];
for (const spelling of [...spellings].reverse()) {
const reverse = spelling.startsWith('-');
const rest = reverse ? spelling.slice(1) : spelling;
const prefix = VERSION_PREFIXES.find((p) => rest.startsWith(p)) ?? '';
keys.push({ field: parseField(rest.slice(prefix.length)), reverse, version: prefix !== '' });
}
return keys;
}
/** One field's text quoted for the language a quoting option names. */
export function quoteText(text, style) {
if (style === QuoteStyle.SHELL) {
return `'${text.replaceAll("'", "'\\''").replaceAll('!', "'\\!'")}'`;
}
if (style === QuoteStyle.PERL)
return `'${text.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`;
if (style === QuoteStyle.PYTHON) {
const escapedText = text.replaceAll('\\', '\\\\').replaceAll("'", "\\'");
return `'${escapedText.replaceAll('\n', '\\n')}'`;
}
if (style === QuoteStyle.TCL) {
let out = '';
for (const ch of text)
out += '[]{}$\\"'.includes(ch) ? `\\${ch}` : (TCL_ESCAPES[ch] ?? ch);
return `"${out}"`;
}
return text;
}
/**
* Which configured suffix decides where `name` sorts: of those starting at or
* before the first difference and reaching it, the one starting earliest,
* then the longest, then the first configured.
*/
function suffixRank(name, off, suffixes) {
let best = null;
for (const [rank, suffix] of suffixes.entries()) {
if (!suffix)
continue;
let at = name.indexOf(suffix);
while (at !== -1 && at <= off) {
if (at + suffix.length >= off) {
// Suffixes come in configured order, so a tie keeps the first.
const earlier = best === null || at < best[0] || (at === best[0] && -suffix.length < best[1]);
if (earlier)
best = [at, -suffix.length, rank];
}
at = name.indexOf(suffix, at + 1);
}
}
return best === null ? null : best[2];
}
function commonPrefix(a, b) {
let at = 0;
while (at < a.length && at < b.length && a.charCodeAt(at) === b.charCodeAt(at))
at++;
return at;
}
/**
* Version order with `versionsort.suffix`. A name holding one of the suffixes
* where the two first differ sorts before one holding none, and two holding
* different ones sort in the order the suffixes were configured; an empty
* suffix in the list places the names holding none. Anything else is
* `strverscmp`. Fitted to git 2.47.3 on 40,950 pairs of names.
*/
export function versioncmp(a, b, suffixes = []) {
if (suffixes.length && a !== b) {
const off = commonPrefix(a, b);
let rankA = suffixRank(a, off, suffixes);
let rankB = suffixRank(b, off, suffixes);
const empty = suffixes.indexOf('');
if (empty !== -1) {
rankA ??= empty;
rankB ??= empty;
}
if (rankA !== rankB) {
if (rankA === null)
return 1;
if (rankB === null)
return -1;
return rankA < rankB ? -1 : 1;
}
}
return strverscmp(a, b);
}
/** Each field's value for each ref, read once however often a sort or a row asks. */
export class ValueTable {
fields;
ctx;
values = new Map();
constructor(fields, ctx) {
this.fields = fields;
this.ctx = ctx;
}
/**
* One field's value for one ref. A ref's first read reads every field the
* listing uses, so one refused for the ref stops the listing whichever field
* was asked for, as git's does.
*/
value(item, f) {
let row = this.values.get(item);
if (row === undefined) {
const read = new Map();
for (const used of this.fields)
if (!read.has(used))
read.set(used, this.read(item, used));
row = read;
this.values.set(item, row);
}
let found = row.get(f);
if (found === undefined) {
found = this.read(item, f);
row.set(f, found);
}
return found;
}
read(item, f) {
return BLOCK_FIELDS.has(f.name) ? { text: '', number: null } : fieldValue(f, item, this.ctx);
}
}
/** How many terminal columns a name takes. */
export function displayWidth(text) {
return textWidth(text);
}
function asciiFolded(text) {
return text.replace(/[A-Z]/g, (c) => c.toLowerCase());
}
function compare(key, a, b, ctx, icase) {
if (key.version)
return versioncmp(a.text, b.text, ctx.suffixes);
if (a.number !== null && b.number !== null)
return Math.sign(a.number - b.number);
if (icase)
return compareCodePoints(asciiFolded(a.text), asciiFolded(b.text));
return compareCodePoints(a.text, b.text);
}
/**
* A top-down merge sort, halves split at `n / 2` rounded down and a tie taken
* from the left, so the comparisons come in the order git's make them.
*/
function mergeSort(items, cmp) {
if (items.length <= 1)
return items;
const half = Math.floor(items.length / 2);
const left = mergeSort(items.slice(0, half), cmp);
const right = mergeSort(items.slice(half), cmp);
const out = [];
let i = 0;
let j = 0;
while (i < left.length && j < right.length) {
const a = left[i];
const b = right[j];
if (a === undefined || b === undefined)
break;
if (cmp(a, b) <= 0) {
out.push(a);
i++;
}
else {
out.push(b);
j++;
}
}
return [...out, ...left.slice(i), ...right.slice(j)];
}
/**
* The refs in the order the keys give, the first key deciding first; a tie on
* every key goes by name, upward whatever the keys' direction. A detached HEAD
* leads when `detachedFirst` says so.
*
* The sort's first comparison of a date with a style (`committerdate:short`),
* under whichever key, goes by the refs' instants, and every later one by the
* dates as shown, as git's sort does (measured on 504 random ref sets and
* eight styles against git 2.47.3 and 2.50.1, and on 1,800 random lists of up
* to four keys against 2.50.1).
*
* @throws GitError a key's field refused for some ref
*/
export function sortRefs(items, keys, table, ctx, icase, detachedFirst = false) {
let fresh = true;
return mergeSort([...items], (a, b) => {
if (detachedFirst) {
const leadA = a.kind === RefKind.DETACHED;
const leadB = b.kind === RefKind.DETACHED;
if (leadA !== leadB)
return leadA ? -1 : 1;
}
for (const key of keys) {
let valueA = table.value(a, key.field);
let valueB = table.value(b, key.field);
if (key.field.date !== null) {
if (!fresh) {
valueA = { text: valueA.text, number: null };
valueB = { text: valueB.text, number: null };
}
fresh = false;
}
const found = compare(key, valueA, valueB, ctx, icase);
if (found)
return key.reverse ? -found : found;
}
if (icase) {
return (compareCodePoints(asciiFolded(a.name), asciiFolded(b.name)) ||
compareCodePoints(a.name, b.name));
}
return compareCodePoints(a.name, b.name);
});
}
function holds(f, condition) {
if (f.variant === 'equals')
return condition === (f.argument ?? '').slice('equals='.length);
if (f.variant === 'notequals')
return condition !== (f.argument ?? '').slice('notequals='.length);
return !SPACE.test(condition);
}
function aligned(text, f) {
const gap = f.count - textWidth(text);
if (gap <= 0)
return text;
if (f.variant === 'right')
return ' '.repeat(gap) + text;
if (f.variant === 'middle') {
const left = Math.floor(gap / 2);
return ' '.repeat(left) + text + ' '.repeat(gap - left);
}
return text + ' '.repeat(gap);
}
function thenAtom(blocks) {
const block = blocks[blocks.length - 1];
if (block?.field.name !== 'if') {
throw new GitError('format: %(then) atom used without a %(if) atom');
}
if (block.condition !== null)
throw new GitError('format: %(then) atom used more than once');
block.condition = block.out.join('');
block.out = [];
}
function elseAtom(blocks) {
const block = blocks[blocks.length - 1];
if (block?.field.name !== 'if') {
throw new GitError('format: %(else) atom used without a %(if) atom');
}
if (block.condition === null) {
throw new GitError('format: %(else) atom used without a %(then) atom');
}
if (block.then !== null)
throw new GitError('format: %(else) atom used more than once');
block.then = block.out.join('');
block.out = [];
}
function endAtom(blocks) {
const block = blocks.pop();
if (block === undefined) {
throw new GitError('format: %(end) atom used without corresponding atom');
}
const body = block.out.join('');
if (block.field.name === 'align')
return aligned(body, block.field);
if (block.condition === null) {
throw new GitError('format: %(if) atom used without a %(then) atom');
}
const [then, other] = block.then !== null ? [block.then, body] : [body, ''];
return holds(block.field, block.condition) ? then : other;
}
/**
* One ref through a format: literal text as it is, each field (or each
* outermost block) quoted on its own.
*
* @throws GitError blocks that do not nest, or a field refused for this ref
*/
export function renderRow(fmt, item, table) {
const out = [];
const blocks = [];
const emit = (text, quoted) => {
const block = blocks[blocks.length - 1];
if (block !== undefined)
block.out.push(text);
else
out.push(quoted ? quoteText(text, fmt.quote) : text);
};
for (const piece of fmt.pieces) {
if (typeof piece === 'string')
emit(piece, false);
else if (piece.name === 'align' || piece.name === 'if') {
blocks.push({ field: piece, out: [], condition: null, then: null });
}
else if (piece.name === 'then')
thenAtom(blocks);
else if (piece.name === 'else')
elseAtom(blocks);
else if (piece.name === 'end')
emit(endAtom(blocks), true);
else
emit(table.value(item, piece).text, true);
}
if (blocks.length)
throw new GitError('format: %(end) atom missing');
return out.join('');
}
function defaultOrder(keys) {
const [key] = keys;
return keys.length === 1 && key?.field.text === DEFAULT_KEY && !key.reverse && !key.version;
}
/**
* A listing through `--format`, one row per ref. A listing in name order
* (`--no-sort`, or the one sort a line gets by default, case-sensitive) writes
* each row as it renders, in the order the refs came, so a field refused on a
* later ref stops it with the rows before already out; a sorted one renders
* every row first and prints none when one is refused. `--count` takes refs,
* an omitted empty row among them.
*
* @returns the rows printed, and the refusal the listing stopped at
*/
export function formatRefs(fmt, items, ctx, keys, options = {}) {
const { count = 0, omitEmpty = false, icase = false, detachedFirst = false } = options;
const table = new ValueTable(usedFields(fmt, keys ?? []), ctx);
// Case folding reorders even the default sort, so only a case-sensitive one
// streams.
const streamed = (options.stream ?? true) && (keys === null || (!icase && defaultOrder(keys)));
let ordered;
try {
ordered =
keys === null || streamed
? [...items]
: sortRefs(items, keys, table, ctx, icase, detachedFirst);
}
catch (error) {
if (error instanceof GitError)
return ['', error];
throw error;
}
// A streamed listing stops at its count; a sorted one renders every row
// before the count takes the first.
let rows = [];
let stopped = null;
for (const [taken, item] of ordered.entries()) {
if (streamed && count > 0 && taken >= count)
break;
try {
rows.push(renderRow(fmt, item, table));
}
catch (error) {
if (!(error instanceof GitError))
throw error;
if (!streamed)
return ['', error];
stopped = error;
break;
}
}
if (count > 0)
rows = rows.slice(0, count);
return [
rows
.filter((row) => row || !omitEmpty)
.map((row) => `${row}\n`)
.join(''),
stopped,
];
}
//# sourceMappingURL=ref_format.js.map