@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
102 lines • 4.6 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 { byteView, decodeText } from "../../../../shell/bytes.js";
import { getOpcodes } from "../../../builtin/diff_format.js";
import { PosixRegex } from "../../../../rs/regex.js";
import { blobData, byteLines } from "./patch.js";
import { BINARY_SNIFF } from "./summary.js";
/** Fold only ASCII letters, as Python's bytes.lower does for a -S string. */
function foldAscii(text) {
return text.replace(/[A-Z]+/g, (run) => run.toLowerCase());
}
/**
* How many times a pattern matches a blob, git's `contains` under
* `--pickaxe-regex` and glibc's REG_NEWLINE: a match stays in its line, each
* search resumes where the last match ended (a step further after an empty
* one), and `^` never holds where it resumes. mirage-rs counts the whole blob
* in one request.
*/
export function contains(text, pattern, signal) {
return pattern.contains(text, signal);
}
/**
* How many times a string appears in one side, or a pattern matches it as git
* counts under `--pickaxe-regex`.
*/
async function occurrences(repo, entry, needle, ignoreCase, signal) {
const data = await blobData(repo, entry);
if (needle instanceof PosixRegex)
return contains(byteView(data, needle.utf8), needle, signal);
const text = ignoreCase ? foldAscii(decodeText(data)) : decodeText(data);
if (needle === '')
return 0;
let count = 0;
let at = text.indexOf(needle);
while (at !== -1) {
count += 1;
at = text.indexOf(needle, at + needle.length);
}
return count;
}
/**
* Whether a pair's diff adds or removes a line the pattern matches: git's `-G`.
* The lines are the ones `git log -p` prints with a `+` or `-`, from the same
* line diff; a binary side is skipped, as git does without `--text`.
*/
async function greps(repo, oldEntry, newEntry, pattern, forceText, signal) {
const old = await blobData(repo, oldEntry);
const now = await blobData(repo, newEntry);
if (!forceText && [old, now].some((data) => data.subarray(0, BINARY_SNIFF).includes(0)))
return false;
const before = byteLines(old);
const after = byteLines(now);
for (const [tag, i1, i2, j1, j2] of getOpcodes(before, after)) {
if (tag === 'equal')
continue;
for (const line of [...before.slice(i1, i2), ...after.slice(j1, j2)]) {
const text = byteView(line.endsWith('\n') ? line.slice(0, -1) : line, pattern.utf8);
if ((await pattern.search(text, 0, signal)) !== null)
return true;
}
}
return false;
}
/**
* Whether one file pair passes the pickaxe.
*
* `-S` is deliberately not a grep: a pair that merely moves a line holding the
* string leaves its count alone, so it does not match, and the commit that
* *introduced* the string does, which is what makes `-S <name> --reverse`
* answer "where did this come from". A missing side holds nothing, so whatever
* a new file adds counts as introduced, and each side reads the way `blobData`
* reads it, so a gitlink is its `Subproject commit` line, as git's pickaxe sees
* it. `-i` counts without regard to ASCII case; a `--pickaxe-regex` pattern
* carries its own case folding.
*
* @param repo repository holding the blobs
* @param pickaxe the search
* @param old the old side, null when the path is new
* @param now the new side, null when the path is gone
*/
export async function matches(repo, pickaxe, old, now) {
const { needle, ignoreCase, signal } = pickaxe;
if (pickaxe.changed !== null)
return greps(repo, old, now, pickaxe.changed, pickaxe.forceText, signal);
if (needle === null)
return true;
const wanted = ignoreCase && typeof needle === 'string' ? foldAscii(needle) : needle;
return ((await occurrences(repo, old, wanted, ignoreCase, signal)) !==
(await occurrences(repo, now, wanted, ignoreCase, signal)));
}
//# sourceMappingURL=pickaxe.js.map