UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

102 lines • 4.6 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 { 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