UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

222 lines • 10.7 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 { startPoint, checkSwitches, configSection, verbUsage, requireVfs } from "./util.js"; import { PathSpec, FileType } from "../../../../types.js"; import { IOResult } from "../../../../io/types.js"; import { FlagView } from "../../../spec/flag_view.js"; import { DETACHED_ADVICE, switchTo } from "./checkout.js"; import { CloneReadOnlyError, GitError, UsageError } from "./errors.js"; import { configuredHeaders, fetchObjects, ignoreFunny } from "./fetch.js"; import { layOut } from "./init.js"; import { readNames, removeTree, statOf, writeFile } from "./io.js"; import { append, entry, IDENTITY, ZERO } from "./reflog.js"; import { DEFAULT_BRANCH } from "./constants.js"; import { BRANCH_PREFIX, detachHead, setHead, TAG_PREFIX, validRefName, writeRef } from "./refs.js"; import { openRepo } from "./repo.js"; import { commitEntries } from "./tree.js"; import { isLocal, openTransport } from "./transport.js"; import { systemNow } from "../../../../utils/clock.js"; const ENC = new TextEncoder(); /** The directory git names a clone after when none is given. */ export function defaultDirectory(url) { let path = url.replace(/\/+$/, ''); if (path.endsWith('/.git')) path = path.slice(0, -'/.git'.length).replace(/\/+$/, ''); if (path.endsWith('.git')) path = path.slice(0, -'.git'.length); return path.split(/[/:]/).at(-1) ?? path; } /** A clone's config, in the order git writes it. */ function config(url, remote, branch) { let text = '[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n' + '\tlogallrefupdates = true\n' + configSection('remote', remote, [ ['url', url], ['fetch', `+refs/heads/*:refs/remotes/${remote}/*`], ]); if (branch !== null) text += configSection('branch', branch, [ ['remote', remote], ['merge', `${BRANCH_PREFIX}${branch}`], ]); return text; } /** The branch a clone checks out and the commit it starts at. */ export function remoteHead(adv, chosen) { if (chosen !== null) { for (const ref of [`${BRANCH_PREFIX}${chosen}`, `${TAG_PREFIX}${chosen}`]) { const oid = adv.refs.get(ref); if (oid !== undefined) return [ref.startsWith(BRANCH_PREFIX) ? chosen : null, adv.peeled.get(ref) ?? oid]; } return [chosen, null]; } if (adv.head !== null) { const oid = adv.refs.get(adv.head); if (oid !== undefined) return [adv.head.slice(BRANCH_PREFIX.length), oid]; } return [null, adv.refs.get('HEAD') ?? null]; } /** * Clone a repository into a new directory. * * A path or `file://` URL is a repository inside the workspace, an `https://` * one is fetched over smart HTTP. Every branch lands as a remote-tracking ref * and every tag as a tag, and the remote's HEAD branch is checked out. A failed * clone removes what it wrote, and keeps a directory that was already there, * empty, as git does. `credentials` is the Authorization `gh repo clone` sends, * scoped to the URL's origin as userinfo is and never written to the clone's * config. */ export async function clone(_accessor, paths, texts, ctx, credentials = {}) { const fl = new FlagView(ctx.flags); const [url, named] = texts; checkSwitches(ctx, texts); if (url === undefined) { throw new UsageError('', `fatal: You must specify a repository to clone.\n\n${verbUsage(ctx)}`); } if (texts.length > 2) throw new UsageError('', `fatal: Too many arguments.\n\n${verbUsage(ctx)}`); const name = named ?? defaultDirectory(url); const quiet = fl.asBool('quiet'); const vfs = requireVfs(ctx); const start = startPoint(fl); const target = PathSpec.fromStrPath(name, undefined, start); const info = await statOf(vfs, target); if (info !== null && (info.type !== FileType.DIRECTORY || (await readNames(vfs, target)).length)) throw new GitError(`destination path '${name}' already exists and is not an empty directory.`); const fresh = info === null; const transport = await openTransport(ctx, url, start, await configuredHeaders(ctx, null), credentials); const local = isLocal(url); // git records a local path as the directory it ran in joined to the path as // typed, not normalized. const stored = !local || url.startsWith('/') ? url : `${start.virtual.replace(/\/+$/, '')}/${url}`; let notes = quiet ? '' : `Cloning into '${name}'...\n`; try { notes += await populate(ctx, transport, target, stored, local && !quiet); } catch (err) { if (!(err instanceof GitError)) throw err; const vfs = ctx.vfs; if (fresh && vfs !== undefined) await removeTree(vfs, target); else if (vfs !== undefined) for (const entry of await readNames(vfs, target)) { const child = entry.replace(/\/+$/, '').split('/').at(-1) ?? ''; if (child) await removeTree(vfs, target.join(child)); } const refusal = err.prefix === null ? err.message : `${err.prefix}: ${err.message}`; return [ null, new IOResult({ exitCode: err.exitCode, stderr: ENC.encode(`${notes}${refusal}\n`) }), ]; } return [null, new IOResult({ stderr: ENC.encode(notes) })]; } /** Lay the clone out, fetch into it and check it out; returns what it prints after `Cloning into`. */ async function populate(ctx, transport, target, url, local) { const fl = new FlagView(ctx.flags); const vfs = requireVfs(ctx); const gitdir = target.join('.git'); const remote = fl.asStr('origin', 'origin'); if (!validRefName(`refs/remotes/${remote}/test`)) throw new GitError(`'${remote}' is not a valid remote name`); await layOut(vfs, gitdir, DEFAULT_BRANCH, ''); const advertised = await transport.advertise(); const location = { gitdir, commondir: gitdir, worktree: target, mountRoot: PathSpec.fromStrPath(vfs.mountOf(target) ?? '/', undefined, '/'), }; const [wants, funny] = ignoreFunny([...advertised.refs] .filter(([ref]) => ref.startsWith(BRANCH_PREFIX) || ref.startsWith(TAG_PREFIX)) .map(([ref, oid]) => ({ remote: ref, oid, local: ref.startsWith(BRANCH_PREFIX) ? `refs/remotes/${remote}/${ref.slice(BRANCH_PREFIX.length)}` : ref, force: false, merge: false, listed: true, }))); let notes = funny; // Only what survived is a candidate for the branch to check out: a HEAD // naming a refused branch detaches, as git's does. const kept = new Set(wants.map((want) => want.remote)); const adv = { refs: new Map([...advertised.refs].filter(([ref]) => kept.has(ref) || ref === 'HEAD')), peeled: new Map([...advertised.peeled].filter(([ref]) => kept.has(ref))), head: advertised.head !== null && kept.has(advertised.head) ? advertised.head : null, }; const chosen = fl.asStr('branch') ?? null; const picked = remoteHead(adv, chosen); let branch = picked[0]; const commit = picked[1]; if (chosen !== null && commit === null) throw new GitError(`Remote branch ${chosen} not found in upstream ${remote}`); if (!advertised.refs.size) notes += 'warning: You appear to have cloned an empty repository.\n'; // No HEAD to start from, in an empty remote or one whose HEAD names a branch // it lacks: the clone is born on the branch that HEAD names, as git's unborn // advertisement leaves it, or on the default one. if (branch === null && commit === null) { const head = advertised.head; branch = (head?.startsWith(BRANCH_PREFIX) && validRefName(head) ? head : `${BRANCH_PREFIX}${DEFAULT_BRANCH}`).slice(BRANCH_PREFIX.length); } const [repo] = await fetchObjects(await openRepo(vfs, location), transport, adv, wants, false); for (const want of wants) if (want.local !== null) await writeRef(vfs, gitdir, want.local, want.oid); const reason = `clone: from ${url}`; const now = Math.floor(systemNow()); const headOid = adv.head === null ? undefined : adv.refs.get(adv.head); if (adv.head !== null && headOid !== undefined) { const tracking = `refs/remotes/${remote}/HEAD`; await writeFile(vfs, gitdir.join(tracking), ENC.encode(`ref: refs/remotes/${remote}/${adv.head.slice(BRANCH_PREFIX.length)}\n`)); await append(vfs, gitdir, `logs/${tracking}`, entry(ZERO, headOid, IDENTITY, now, reason)); } await writeFile(vfs, gitdir.join('config'), ENC.encode(config(url, remote, branch))); if (local) notes += 'done.\n'; if (commit === null) { if (advertised.refs.size && !fl.asBool('no_checkout')) notes += 'warning: remote HEAD refers to nonexistent ref, unable to checkout\n'; await setHead(vfs, gitdir, `${BRANCH_PREFIX}${branch ?? DEFAULT_BRANCH}`); return notes; } const line = entry(ZERO, commit, IDENTITY, now, reason); if (branch !== null) { await writeRef(vfs, gitdir, `${BRANCH_PREFIX}${branch}`, commit); await setHead(vfs, gitdir, `${BRANCH_PREFIX}${branch}`); await append(vfs, gitdir, `logs/${BRANCH_PREFIX}${branch}`, line); } else { await detachHead(vfs, gitdir, commit); notes += `Note: switching to '${commit}'.\n\n${DETACHED_ADVICE}\n`; } await append(vfs, gitdir, 'logs/HEAD', line); if (!fl.asBool('no_checkout')) await switchTo(repo, vfs, new Map(), await commitEntries(repo, commit)); return notes; } /** clone's refusal by a read-only mount, at the work tree it could not make. */ export const cloneReadOnly = (texts) => new CloneReadOnlyError(texts[1] ?? defaultDirectory(texts[0] ?? '')); //# sourceMappingURL=clone.js.map