Files
PaperClipAI/server/src/services/git-workspace-contents.ts
T
DottaandPaperclip c1d567ff6a fix(workspaces): isolate archived nested Git reconstruction (#19409)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Durable workspace import must preserve archived task files and Git
history.
> - A saved nested checkout can be a linked worktree whose branch
belongs to its old absolute path.
> - Archived import reconstructs that checkout in a new scratch
directory.
> - Ordinary linked recovery rejects the old branch at that new path. It
can also write into a cache used by other import workers.
> - This pull request reconstructs archived nested checkouts as private
standalone repositories.
> - Each import preserves saved files and extra history without changing
shared worktree registrations or branches.

## Linked Issues or Issue Description

**What happened?**
Archived workspace import fails when a saved nested linked checkout has
a branch owned by the old path. The new scratch path fails the
linked-worktree ownership check. The existing cache lock is
process-local, so separate import processes also share mutable Git state
without a common lock.

**Expected behavior**
Archived import must create an independent portable copy of the saved
checkout. It must retain the saved commit, branch name, staged edits,
working files, ignored recovery files, settings, and extra saved
commits. It must keep the source cache unchanged and enforce source
authorization.

**Steps to reproduce**
1. Create a managed nested linked checkout with an unpushed commit and
staged and unstaged edits.
2. Save an encrypted restore point, including ignored files and an extra
history tip.
3. Restore its saved contents under a different absolute workspace path.
4. Before this repair, the new path cannot adopt the branch owned by the
old path.

**Paperclip version or commit**
The real Git regression fails before the repair at `fc1f5f3052`. The
final branch includes the merged interrupted-upload and nested-source
authorization fix from #19396.

**Deployment mode**
Archived remote workspace migration with real Git repositories,
disposable PostgreSQL, and encrypted object storage. Local integration
tests use the filesystem storage provider.

Builds on merged #19402 and #19396. Related #19374 concerns interrupted
clone staging; it does not implement private archived reconstruction. No
duplicate open repair was found.

## What Changed

- Enable private repository reconstruction only for archived migration.
Ordinary linked recovery keeps its path ownership guard.
- Add an object-only source to the managed checkout owner. A verified
cache supplies only the saved commit and authenticated bundle
prerequisites through scoped fetches. The private copy includes no
unrelated task refs or objects, hard links, object alternates, or
current cache files.
- Fetch missing authenticated bundle prerequisites before importing a
saved bundle in both private reconstruction and ordinary linked
recovery. A pruned cache can use the approved remote; unavailable
history still blocks restore.
- Keep every authenticated restore point's extra commit under a private
ref in root and nested private copies so portable archive capture and
garbage collection retain it. Ordinary recovery keeps its existing
history and expiration rules.
- Share the verified, bounded ignore-file copy between root and nested
reconstruction. Sources remain unchanged.
- Retain source authorization before and after reconstruction and the
final source-row locks from #19396.
- Test concurrent private copies, source rejection, foreign task refs
and objects, ordinary linked prerequisite recovery, a real migration,
and cold restore after removing all original repositories. Test hard
links, oversized excludes, unsafe info paths, and replacement during a
descriptor read.

## Verification

- Final affected run: 292 tests pass across ten suites. These cover real
Git repositories, encrypted restore points, disposable PostgreSQL
migration, portable archives, the restore-point ledger, source
rejection, and concurrent copies.
- Full repository typecheck and build pass after the scoped-object
repair. Final server typecheck and full build also pass after
restricting preserved refs to archive mode.
- RED then GREEN: a linked checkout at a different path; a pruned bundle
prerequisite still available from the approved remote; foreign task refs
and objects entering the archive; root extra history disappearing after
cold restore; and archive-only refs blocking ordinary standalone
expiration.
- Two concurrent private copies retain the saved head, branch, staged
and unstaged edits, ignored recovery bytes, settings, and modes. Source
refs, index, worktree registrations, and working files remain unchanged.
- Delete the original checkout, cache, remote, and private copies after
portable capture. Cold restore retains the saved files and extra history
after garbage collection. No alternates or unrelated task objects
remain.
- Ordinary linked recovery retains its path ownership check and recovers
pruned prerequisites. New and existing ordinary standalone destinations
have no archive-only refs and remain eligible for expiration after
normal object cleanup.
- Unsafe exclude tests reject hard links, oversized files, nonregular
paths, info-directory substitution, and replacement during the
descriptor read. They preserve the target and replacement source bytes.
- Preserve the failed attempts: an initial migration fixture changed its
home after constructing the source path. Later expiration fixture
attempts used a filtered current-head tip, omitted the local-source
callback, or used cleanup that generated Git server-info files or left
packed unreachable objects. Correct those setup assumptions before the
actual regressions fail. Final affected suites pass.
- Two earlier local full-suite attempts were interrupted before editing
valid review findings. Neither is reported as a full-suite PASS.
Final-head full CI passes; review is 5/5 with no open findings.

## Risks

Archived imports use additional private disk space for independent Git
objects. Missing, changed, or unsafe sources still block import. The
repair does not weaken the default linked-worktree ownership check,
change stored formats, or update live provider state. The shared
checkout addition is optional and has no effect for existing callers
that do not supply an object source.

## Model Used

OpenAI GPT-6 in Codex, with code editing, tool use, and test execution.
The exact runtime model identifier and context-window size are not
exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-06 00:56:06 -06:00

321 lines
21 KiB
TypeScript

import fs from "node:fs/promises";
import { constants } from "node:fs";
import path from "node:path";
import { createHash } from "node:crypto";
import { isDeepStrictEqual } from "node:util";
import { PROJECT_REPOSITORIES_DIR } from "@paperclipai/adapter-utils/git-workspace-sync";
import { localEncryptedProvider } from "../secrets/local-encrypted-provider.js";
import type { StoredSecretVersionMaterial } from "../secrets/types.js";
import type { GitRemoteAuthProvider } from "./git-credentials.js";
import { localRepositoryPath } from "./local-repository-path.js";
import { managedProjectCheckoutName } from "./managed-project-checkout-name.js";
import { materializeManagedProjectWorkspace } from "./managed-repository-checkout.js";
import { ensureSavedGitCommit, gitCacheCommand, GitCacheRestorationFailure } from "./git-workspace-restoration.js";
import { applyGitRestorePointToWorktree, ensureGitRestorePointCommit, loadGitRestorePoint, type GitRestorePointRef } from "./git-restore-point-storage.js";
import { copyVerifiedGitExcludes } from "./git-repository-excludes.js";
import { importRestorePoint } from "./git-restore-point.js";
import { isNestedWorktreeBranchName, isVerifiedNestedRepositoryBase, nestedRepositoryBaseCandidates, readLinkedNestedCheckout, recordedNestedRepositoryBase, restoreNestedRepositoryWorktree, withNestedRepositoryBaseLock } from "./nested-repository-worktrees.js";
import { getStorageService } from "../storage/index.js";
import type { StorageService } from "../storage/types.js";
export const CLAUDE_SETTINGS_PATH = ".claude/settings.local.json";
const SETTINGS_LIMIT = 64 * 1024;
const CHECKOUT_LIMIT = 50;
type Identity = { companyId: string; workspaceId: string };
type Settings = { material: StoredSecretVersionMaterial } | null;
export type ManagedCheckoutSource = { id: string; repoUrl: string; repoRef: string | null; cwd: string | null };
export type ManagedCheckoutContents = {
name: string; source: ManagedCheckoutSource; commit: string; branchName: string | null;
worktree?: { base: string; admin: string };
restorePoint?: GitRestorePointRef;
};
export type GitWorkspaceContents = {
schema: "paperclip.git-workspace-contents.v1";
settings: Settings;
checkouts: Array<ManagedCheckoutContents & { settings: Settings }>;
};
async function ensureCheckoutPrerequisites(cwd: string, checkout: ManagedCheckoutContents,
manifest: { prerequisites: readonly string[] }, resolveGitAuth: GitRemoteAuthProvider) {
const commits = new Set(manifest.prerequisites);
if (checkout.restorePoint?.headOnRemote) commits.add(checkout.commit);
for (const commit of commits) await ensureSavedGitCommit(cwd, checkout.source.repoUrl, commit, resolveGitAuth);
}
async function entry(target: string) {
return fs.lstat(target).catch((error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") return null;
throw error;
});
}
async function checkedDirectory(cwd: string, relative: string, create = false) {
let current = await fs.realpath(cwd);
for (const segment of relative.split("/")) {
current = path.join(current, segment);
if (create) await fs.mkdir(current, { mode: 0o700 }).catch((error: NodeJS.ErrnoException) => {
if (error.code !== "EEXIST") throw error;
});
const stat = await entry(current);
if (!stat) return null;
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new GitCacheRestorationFailure();
}
return current;
}
async function readSettings(cwd: string) {
if (!await checkedDirectory(cwd, ".claude")) return null;
const target = path.join(cwd, CLAUDE_SETTINGS_PATH);
const initial = await entry(target);
if (!initial) return null;
if (!initial.isFile() || initial.isSymbolicLink() || initial.nlink !== 1 || initial.size > SETTINGS_LIMIT) throw new Error("unsupported_local_settings");
if (await gitCacheCommand(cwd, ["ls-files", "--error-unmatch", "--", CLAUDE_SETTINGS_PATH]).then(() => true, () => false)) throw new Error("tracked_local_settings");
const handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
try {
const stat = await handle.stat();
if (!stat.isFile() || stat.nlink !== 1 || stat.size > SETTINGS_LIMIT) throw new Error("unsupported_local_settings");
const buffer = Buffer.alloc(SETTINGS_LIMIT + 1);
let length = 0;
while (length < buffer.length) {
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
if (bytesRead === 0) break;
length += bytesRead;
}
if (length > SETTINGS_LIMIT) throw new Error("unsupported_local_settings");
return { mode: stat.mode & 0o777, bytes: buffer.subarray(0, length).toString("base64") };
} finally { await handle.close(); }
}
async function saveSettings(cwd: string, identity: Identity, relativePath: string): Promise<Settings> {
const value = await readSettings(cwd);
if (!value) return null;
const { material } = await localEncryptedProvider.createVersion({ value: JSON.stringify({ ...identity, relativePath, ...value }) });
return { material };
}
async function savedSettings(settings: Settings, identity: Identity, relativePath: string) {
if (settings === null) return null;
const value = JSON.parse(await localEncryptedProvider.resolveVersion({ material: settings.material, externalRef: null }));
if (value.companyId !== identity.companyId || value.workspaceId !== identity.workspaceId || value.relativePath !== relativePath
|| !Number.isInteger(value.mode) || value.mode < 0 || value.mode > 0o777
|| typeof value.bytes !== "string" || value.bytes.length > Math.ceil(SETTINGS_LIMIT / 3) * 4
|| Buffer.from(value.bytes, "base64").toString("base64") !== value.bytes) throw new GitCacheRestorationFailure();
return { mode: value.mode as number, bytes: value.bytes as string };
}
export function validateGitWorkspaceContents(contents: GitWorkspaceContents | undefined) {
if (contents === undefined) return;
const settingsValid = (settings: Settings) => settings === null || !!settings && typeof settings.material === "object"
&& settings.material !== null && JSON.stringify(settings.material).length <= SETTINGS_LIMIT * 2;
if (!contents || contents.schema !== "paperclip.git-workspace-contents.v1" || !settingsValid(contents.settings)
|| !Array.isArray(contents.checkouts) || contents.checkouts.length > CHECKOUT_LIMIT) throw new GitCacheRestorationFailure();
const names = new Set<string>();
for (const checkout of contents.checkouts) {
if (!checkout || !checkout.source || typeof checkout.source.id !== "string" || typeof checkout.source.repoUrl !== "string"
|| !(checkout.source.repoRef === null || typeof checkout.source.repoRef === "string")
|| !(checkout.source.cwd === null || typeof checkout.source.cwd === "string")
|| typeof checkout.name !== "string" || !/^[A-Za-z0-9_-]+-[a-f0-9]{12}$/.test(checkout.name)
|| checkout.name !== managedProjectCheckoutName(checkout.source) || names.has(checkout.name)
|| !/^[a-f0-9]{40}$/.test(checkout.commit)
|| !(checkout.branchName === null || typeof checkout.branchName === "string" && checkout.branchName.length > 0
&& checkout.branchName.length <= 1024 && !checkout.branchName.startsWith("-") && !checkout.branchName.includes("\0"))
|| !settingsValid(checkout.settings)
|| (checkout.worktree !== undefined && (!checkout.worktree || typeof checkout.worktree.base !== "string"
|| !path.isAbsolute(checkout.worktree.base) || checkout.worktree.base.includes("\0") || checkout.worktree.base.length > 4096
|| typeof checkout.worktree.admin !== "string" || !/^[A-Za-z0-9._-]{1,255}$/.test(checkout.worktree.admin)
|| [".", ".."].includes(checkout.worktree.admin) || Object.keys(checkout.worktree).length !== 2
|| (checkout.branchName !== null && !isNestedWorktreeBranchName(checkout.branchName))))) throw new GitCacheRestorationFailure();
names.add(checkout.name);
}
}
export function selectRestoredProjectRepositories<T extends { id: string; repoUrl: string | null; repoRef: string | null; cwd?: string | null }>(sources: T[], contents?: GitWorkspaceContents): T[] {
validateGitWorkspaceContents(contents);
for (const checkout of contents?.checkouts ?? []) {
if (!sources.some(source => source.id === checkout.source.id && source.repoUrl === checkout.source.repoUrl)) throw new GitCacheRestorationFailure();
}
return sources.map(source => {
const saved = contents?.checkouts.find(checkout => checkout.source.id === source.id)?.source;
return saved ? { ...source, repoRef: saved.repoRef, cwd: saved.cwd } : source;
});
}
export async function readManagedCheckoutContents(checkout: string, name: string, source: ManagedCheckoutSource, signal?: AbortSignal): Promise<ManagedCheckoutContents> {
const commit = (await gitCacheCommand(checkout, ["rev-parse", "HEAD"], signal)).trim();
const branchName = (await gitCacheCommand(checkout, ["symbolic-ref", "--quiet", "--short", "HEAD"], signal).catch(() => "")).trim() || null;
const linked = await readLinkedNestedCheckout(checkout);
return linked ? { name, source, commit, branchName, worktree: { base: linked.base, admin: linked.admin } } : { name, source, commit, branchName };
}
export async function captureGitWorkspaceContents(cwd: string, identity: Identity, checkouts: ManagedCheckoutContents[]): Promise<GitWorkspaceContents> {
if (checkouts.length > CHECKOUT_LIMIT) throw new Error("managed_checkout_budget");
const contents: GitWorkspaceContents = { schema: "paperclip.git-workspace-contents.v1",
settings: await saveSettings(cwd, identity, CLAUDE_SETTINGS_PATH), checkouts: [] };
for (const checkout of checkouts.sort((a, b) => a.name.localeCompare(b.name))) {
const relative = `${PROJECT_REPOSITORIES_DIR}/${checkout.name}`;
if (!await checkedDirectory(cwd, relative)) throw new GitCacheRestorationFailure();
contents.checkouts.push({ ...checkout, settings: await saveSettings(path.join(cwd, relative), identity, `${relative}/${CLAUDE_SETTINGS_PATH}`) });
}
validateGitWorkspaceContents(contents);
return contents;
}
export async function assertGitWorkspaceContentsUnchanged(cwd: string, identity: Identity, contents: GitWorkspaceContents | undefined, checkouts: ManagedCheckoutContents[]) {
validateGitWorkspaceContents(contents);
const recorded = contents?.checkouts.map(({ settings: _settings, restorePoint: _restorePoint, ...checkout }) => checkout) ?? [];
const actual = checkouts.map(({ restorePoint: _restorePoint, ...checkout }) => checkout).sort((a, b) => a.name.localeCompare(b.name));
if (!isDeepStrictEqual(recorded, actual)) throw new Error("managed_checkouts_changed");
const assertSettings = async (root: string, saved: Settings, relative: string) => {
if (!isDeepStrictEqual(await readSettings(root), await savedSettings(saved, identity, relative))) throw new Error("local_settings_changed");
};
await assertSettings(cwd, contents?.settings ?? null, CLAUDE_SETTINGS_PATH);
for (const checkout of contents?.checkouts ?? []) {
const relative = `${PROJECT_REPOSITORIES_DIR}/${checkout.name}`;
if (!await checkedDirectory(cwd, relative)) throw new Error("managed_checkouts_changed");
await assertSettings(path.join(cwd, relative), checkout.settings, `${relative}/${CLAUDE_SETTINGS_PATH}`);
}
}
async function restoreSettings(cwd: string, settings: Settings, identity: Identity, relative: string) {
const saved = await savedSettings(settings, identity, relative);
const directory = await checkedDirectory(cwd, ".claude", saved !== null);
if (!directory) return;
const target = path.join(directory, "settings.local.json");
const temporary = path.join(directory, `.settings-restore-${createHash("sha256").update(JSON.stringify(settings)).digest("hex")}`);
const pending = await entry(temporary);
if (pending) {
if (!saved || !pending.isFile() || pending.isSymbolicLink() || pending.size > SETTINGS_LIMIT) throw new GitCacheRestorationFailure();
const current = await entry(target);
const expected = Buffer.from(saved.bytes, "base64");
const bytes = await fs.readFile(temporary);
const unpublished = pending.nlink === 1 && !current && bytes.length <= expected.length
&& bytes.equals(expected.subarray(0, bytes.length)) && [0o600, saved.mode].includes(pending.mode & 0o777);
const published = pending.nlink === 2 && current?.ino === pending.ino && current.dev === pending.dev
&& bytes.equals(expected) && (pending.mode & 0o777) === saved.mode;
if (!unpublished && !published) throw new GitCacheRestorationFailure();
await fs.unlink(temporary);
}
const current = await readSettings(cwd);
if (current) {
if (!isDeepStrictEqual(current, saved)) throw new GitCacheRestorationFailure();
return;
}
if (!saved) return;
try {
await fs.writeFile(temporary, Buffer.from(saved.bytes, "base64"), { flag: "wx", mode: 0o600 });
await fs.chmod(temporary, saved.mode);
await fs.link(temporary, target);
} finally { await fs.rm(temporary, { force: true }); }
}
export async function restoreGitWorkspaceContents(input: Identity & { cwd: string; contents?: GitWorkspaceContents; projectId?: string | null;
prepareLocalSource?: (sourceCwd: string) => Promise<() => Promise<void>>;
storage?: StorageService;
isolateRepositories?: boolean;
resolveGitAuth: GitRemoteAuthProvider; authorizeSource: (source: ManagedCheckoutSource) => Promise<void> }) {
if (!input.contents) return;
validateGitWorkspaceContents(input.contents);
const workspace = { companyId: input.companyId, id: input.workspaceId };
try {
for (const checkout of input.contents.checkouts) {
await input.authorizeSource(checkout.source);
const localSource = localRepositoryPath(checkout.source.repoUrl);
if (localSource && !input.prepareLocalSource) throw new GitCacheRestorationFailure();
const recheck = localSource ? await input.prepareLocalSource!(localSource) : undefined;
const root = await checkedDirectory(input.cwd, PROJECT_REPOSITORIES_DIR, true);
const destination = path.join(root!, checkout.name);
const relative = `${PROJECT_REPOSITORIES_DIR}/${checkout.name}/${CLAUDE_SETTINGS_PATH}`;
const existing = await entry(destination);
const candidates = checkout.worktree && input.projectId
? nestedRepositoryBaseCandidates({ companyId: input.companyId, projectId: input.projectId, repoUrl: checkout.source.repoUrl }) : null;
if (checkout.worktree && !candidates) throw new GitCacheRestorationFailure();
if (existing && checkout.worktree && !input.isolateRepositories) {
const linked = existing.isDirectory() && !existing.isSymbolicLink() ? await readLinkedNestedCheckout(destination, candidates!) : null;
if (!linked || linked.checkout !== destination || !await isVerifiedNestedRepositoryBase(linked.base, candidates!, checkout.source.repoUrl)) {
throw new GitCacheRestorationFailure();
}
} else if (existing) {
if (!existing.isDirectory() || existing.isSymbolicLink() || !(await entry(path.join(destination, ".git")))?.isDirectory()
|| (await fs.realpath(path.join(destination, ".git"))) !== path.join(destination, ".git")) throw new GitCacheRestorationFailure();
}
if (existing) {
if ((await gitCacheCommand(destination, ["rev-parse", "--show-toplevel"])).trim() !== destination
|| (await gitCacheCommand(destination, ["config", "--local", "--null", "--get-all", "remote.origin.url"])) !== `${checkout.source.repoUrl}\0`) throw new GitCacheRestorationFailure();
const actual = await readManagedCheckoutContents(destination, checkout.name, checkout.source);
if (actual.commit !== checkout.commit || actual.branchName !== checkout.branchName) throw new GitCacheRestorationFailure();
if (checkout.restorePoint) await applyGitRestorePointToWorktree({ storage: input.storage ?? getStorageService(), workspace,
ref: checkout.restorePoint, repositoryCwd: destination, worktreePath: destination,
preserveImportedHistory: input.isolateRepositories });
await restoreSettings(destination, checkout.settings, input, relative);
continue;
}
if (checkout.worktree && !input.isolateRepositories) {
const recordedBase = await recordedNestedRepositoryBase(checkout.worktree.base, candidates!);
if (!recordedBase) throw new GitCacheRestorationFailure();
if (!await isVerifiedNestedRepositoryBase(recordedBase, candidates!, checkout.source.repoUrl)) {
if (await entry(recordedBase)) throw new GitCacheRestorationFailure();
await materializeManagedProjectWorkspace(recordedBase, { repoUrl: checkout.source.repoUrl, resolveGitAuth: input.resolveGitAuth });
if (!await isVerifiedNestedRepositoryBase(recordedBase, candidates!, checkout.source.repoUrl)) throw new GitCacheRestorationFailure();
}
const base = await fs.realpath(recordedBase);
await withNestedRepositoryBaseLock(base, async () => {
let preserved: string[] = [];
if (checkout.restorePoint) {
const { payload, manifest } = await loadGitRestorePoint({ storage: input.storage ?? getStorageService(), workspace, ref: checkout.restorePoint });
await ensureCheckoutPrerequisites(base, checkout, manifest, input.resolveGitAuth);
await importRestorePoint({ repositoryCwd: base, payload });
preserved = manifest.preserved;
}
await ensureSavedGitCommit(base, checkout.source.repoUrl, checkout.commit, input.resolveGitAuth);
if (checkout.branchName) await gitCacheCommand(base, ["check-ref-format", "--branch", checkout.branchName]);
await restoreNestedRepositoryWorktree({ base, cwd: destination, commit: checkout.commit, branchName: checkout.branchName,
repoRef: checkout.source.repoRef, preserved,
afterAdd: async cwd => {
if (checkout.restorePoint) await applyGitRestorePointToWorktree({ storage: input.storage ?? getStorageService(), workspace,
ref: checkout.restorePoint, repositoryCwd: cwd, worktreePath: cwd });
await restoreSettings(cwd, checkout.settings, input, relative);
await input.authorizeSource(checkout.source);
await recheck?.();
} });
});
continue;
}
let gitObjectSource: string | undefined;
if (checkout.worktree) {
const recorded = await recordedNestedRepositoryBase(checkout.worktree.base, candidates!);
if (!recorded) throw new GitCacheRestorationFailure();
if (await entry(recorded)) {
if (!await isVerifiedNestedRepositoryBase(recorded, candidates!, checkout.source.repoUrl)) throw new GitCacheRestorationFailure();
gitObjectSource = await fs.realpath(recorded);
}
}
const savedRestorePoint = checkout.restorePoint && input.isolateRepositories
? await loadGitRestorePoint({ storage: input.storage ?? getStorageService(), workspace, ref: checkout.restorePoint }) : null;
const result = await materializeManagedProjectWorkspace(destination, { repoUrl: checkout.source.repoUrl,
gitObjectSource: gitObjectSource ? { cwd: gitObjectSource, commits: [checkout.commit, ...(savedRestorePoint?.manifest.prerequisites ?? [])] } : undefined,
resolveGitAuth: input.resolveGitAuth, beforePublish: async temporary => {
if (gitObjectSource && checkout.restorePoint) await copyVerifiedGitExcludes(gitObjectSource, temporary);
if (savedRestorePoint) {
const { payload, manifest } = savedRestorePoint;
await ensureCheckoutPrerequisites(temporary, checkout, manifest, input.resolveGitAuth);
await importRestorePoint({ repositoryCwd: temporary, payload });
} else if (checkout.restorePoint) await ensureGitRestorePointCommit({ storage: input.storage ?? getStorageService(), workspace,
ref: checkout.restorePoint, repositoryCwd: temporary });
await ensureSavedGitCommit(temporary, checkout.source.repoUrl, checkout.commit, input.resolveGitAuth);
if (checkout.branchName) await gitCacheCommand(temporary, ["check-ref-format", "--branch", checkout.branchName]);
await gitCacheCommand(temporary, checkout.branchName
? ["checkout", "-B", checkout.branchName, checkout.commit] : ["checkout", "--detach", checkout.commit]);
if (checkout.restorePoint) await applyGitRestorePointToWorktree({ storage: input.storage ?? getStorageService(), workspace,
ref: checkout.restorePoint, repositoryCwd: temporary, worktreePath: temporary, preserveImportedHistory: input.isolateRepositories });
await gitCacheCommand(temporary, ["reflog", "expire", "--expire=now", "--all"]);
await restoreSettings(temporary, checkout.settings, input, relative);
await input.authorizeSource(checkout.source);
await recheck?.();
if (gitObjectSource && !await isVerifiedNestedRepositoryBase(gitObjectSource, candidates!, checkout.source.repoUrl)) throw new GitCacheRestorationFailure();
} });
if (result.warning) throw new GitCacheRestorationFailure();
}
await restoreSettings(input.cwd, input.contents.settings, input, CLAUDE_SETTINGS_PATH);
} catch { throw new GitCacheRestorationFailure(); }
}