@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
117 lines • 5.2 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 git from 'isomorphic-git';
import { decodeText } from "../../../../shell/bytes.js";
import { toHex } from "../../../../utils/hex.js";
import { GitError } from "./errors.js";
import { resolveObject, unwrapped } from "./revparse.js";
import { rawObject, repoArgs } from "./repo.js";
import { rooted } from "./seen.js";
const SPACE = 0x20;
const NUL = 0x00;
const OID_BYTES = 20;
const SHORT_TREE_MODE = '40000';
export const TREE_MODE = '040000';
const DEC = new TextDecoder();
/**
* The entries of one tree object, read from its raw bytes.
*
* Parsed here rather than through isomorphic-git's readTree, which decodes each
* name as UTF-8 and turns a byte that is not into U+FFFD. A name keeps every
* byte, a byte UTF-8 cannot read carried as its surrogate escape, so the name
* quotes and prints the way git's does. A subtree's `40000` is spelled
* `040000`, as isomorphic-git spells it.
*/
export async function treeItems(repo, treeOid) {
// The raw content is exactly what a byte-faithful name needs.
const { raw } = await rawObject(repo, treeOid);
const items = [];
for (let at = 0; at < raw.length;) {
const space = raw.indexOf(SPACE, at);
const nul = space < 0 ? -1 : raw.indexOf(NUL, space);
if (nul < 0 || nul + 1 + OID_BYTES > raw.length)
throw new GitError(`unable to read tree (${treeOid})`);
const mode = DEC.decode(raw.subarray(at, space));
const oid = toHex(raw.subarray(nul + 1, nul + 1 + OID_BYTES));
items.push({
path: decodeText(raw.subarray(space + 1, nul)),
oid,
mode: mode === SHORT_TREE_MODE ? TREE_MODE : mode,
});
at = nul + 1 + OID_BYTES;
}
return items;
}
/**
* Every blob a tree holds, keyed by repository-relative path.
*
* Flattened rather than walked per directory because every caller here compares
* two whole trees; git's own diff does the same on the paths it has already
* expanded. A submodule (`160000`) is carried through as an entry rather than
* descended into: it names a commit in another repository.
*/
export async function treeEntries(repo, treeOid, prefix = '') {
const rows = await listedTree(repo, treeOid, [], true, false, false, prefix);
return new Map(rows.map(([path, mode, oid]) => [path, { mode, oid }]));
}
/**
* Every blob a commit's tree holds, or an empty map for no commit at all.
*
* An empty map is what an unborn HEAD means: nothing is committed yet, so every
* staged path reads as an addition.
*/
export async function commitEntries(repo, commitOid) {
if (commitOid === null)
return new Map();
const { commit } = await git.readCommit({ ...repoArgs(repo), oid: commitOid });
return treeEntries(repo, commit.tree);
}
/** Resolve a tree-ish through tags and commits. */
export async function resolveTree(repo, name, resolved) {
const obj = await unwrapped(repo, resolved ?? (await resolveObject(repo, name)), name);
if (obj.type === 'commit') {
const tree = (await git.readCommit({ ...repoArgs(repo), oid: obj.oid })).commit.tree;
rooted(repo, tree, name);
return tree;
}
if (obj.type === 'tree')
return obj.oid;
throw new GitError('not a tree object');
}
/** Literal tree prefixes; a trailing slash descends even without -r. */
export async function listedTree(repo, tree, patterns, recursive, trees, directories, prefix = '') {
const out = [];
for (const entry of await treeItems(repo, tree)) {
const path = prefix ? `${prefix}/${entry.path}` : entry.path;
const selected = patterns.length === 0 ||
patterns.some((pattern) => pattern === '' || path === pattern || path.startsWith(pattern.replace(/\/$/, '') + '/'));
const row = [path, entry.mode, entry.oid];
if (entry.mode !== TREE_MODE) {
if (selected && !directories)
out.push(row);
continue;
}
const descend = (recursive && selected) || patterns.some((pattern) => pattern.startsWith(path + '/'));
const showTree = trees || (directories && (selected || recursive));
if ((selected && !descend) || (descend && showTree))
out.push(row);
if (descend) {
const children = await listedTree(repo, entry.oid, patterns, recursive, trees, directories, path);
for (const child of children)
out.push(child);
}
}
return out;
}
//# sourceMappingURL=tree.js.map