fix(server): preserve inactive rebase bookmarks in workspace snapshots (#19310)

Preserve a validated standalone REBASE_HEAD and its commit without changing the source workspace. Keep active Git operations, unmerged indexes, invalid markers and concurrent changes blocked.

Verified 156 affected tests, workspace typecheck and build. All hosted CI checks passed, and Greptile scored the exact head 5/5 with no unresolved threads. The bounded full local suite stopped during its fifth chat shard; this incomplete result is recorded in the PR.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-11-05 10:54:52 -08:00
1 parent 5de779848b
commit a695398052
3 files changed
+118 -3

No files matched your search

+6
View File
@@ -931,6 +931,12 @@ restore does not recreate runtime IPC. Named pipes and device entries remain
unsupported, while regular files, directories and confined symlinks retain their
existing validation.
Portable archives also preserve a standalone `REBASE_HEAD` bookmark and its commit
when no Git operation state or unmerged index remains. Capture does not remove or
change the source bookmark. The archive checks its identity throughout capture
and retains its objects even when no branch references them. An unfinished
rebase, merge, revert, cherry-pick, sequencer or bisect still blocks export.
Disk-backed native sandbox workspace snapshots run in at most two worker threads per
controller process. Each capture retains the same file hashes, modes, symlinks,
exclusions and ignored-path manifest as an inline capture. A worker seals its
@@ -275,6 +275,89 @@ describe("portable remote workspace archives", () => {
await expect(fs.access(path.join(restored, ".git", "objects", "info", "alternates"))).rejects.toMatchObject({ code: "ENOENT" });
await git(restored, "fsck", "--full", "--no-dangling");
});
it.each(["ordinary", "linked", "borrowed", "shallow", "sha256"].flatMap(kind =>
["local", "serialized"].map(mode => ({ kind, mode }))))(
"preserves an inactive rebase bookmark and its unreferenced commit in $mode $kind capture", async ({ kind, mode }) => {
const base = await source();
await git(base, "init", "-q", "-b", "main", ...(kind === "sha256" ? ["--object-format=sha256"] : []));
await fs.writeFile(path.join(base, "work.txt"), "base\n"); await git(base, "add", "."); await git(base, "commit", "-qm", "base");
const bookmark = async (repo: string) => {
await git(repo, "checkout", "-qb", "old-rebase-tip");
await fs.writeFile(path.join(repo, "bookmark.txt"), "preserved rebase commit\n");
await git(repo, "add", "."); await git(repo, "commit", "-qm", "old rebase tip");
const commit = await git(repo, "rev-parse", "HEAD");
await git(repo, "checkout", "-q", "main"); await git(repo, "branch", "-D", "old-rebase-tip");
return commit;
};
let marker = await bookmark(base), cwd = base;
if (kind === "linked") {
cwd = path.join(root, "linked"); await git(base, "worktree", "add", "-qb", "selected", cwd);
} else if (kind === "borrowed" || kind === "shallow") {
cwd = path.join(root, "selected");
await git(root, "clone", "-q", ...(kind === "borrowed" ? ["--shared", base] : ["--depth=1", `file://${base}`]), cwd);
if (kind === "shallow") marker = await bookmark(cwd);
}
await git(cwd, "update-ref", "REBASE_HEAD", marker);
await fs.writeFile(path.join(cwd, "work.txt"), "staged\n"); await git(cwd, "add", "work.txt");
await fs.writeFile(path.join(cwd, "work.txt"), "unstaged\n");
const head = await git(cwd, "rev-parse", "HEAD"), status = await git(cwd, "status", "--porcelain=v2");
const gitDir = await git(cwd, "rev-parse", "--absolute-git-dir");
const bytes = await sourceBytes(cwd), administration = await sourceBytes(gitDir);
let snapshot: Awaited<ReturnType<typeof capture>>;
if (mode === "serialized") {
const helper = path.join(root, "helper.cjs"), archivePath = path.join(root, "snapshot.pcws");
await fs.writeFile(helper, remoteWorkspaceArchiveScript());
const result = await execute(process.execPath, [helper, "capture", cwd, archivePath], { env });
snapshot = { archivePath, ...JSON.parse(result.stdout) };
} else snapshot = await capture(cwd);
expect(await sourceBytes(cwd)).toBe(bytes); expect(await sourceBytes(gitDir)).toBe(administration);
await fs.rm(base, { recursive: true, force: true });
if (cwd !== base) await fs.rm(cwd, { recursive: true, force: true });
const restored = await restore(snapshot);
expect(await git(restored, "rev-parse", "HEAD")).toBe(head);
expect(await git(restored, "status", "--porcelain=v2")).toBe(status);
expect(await git(restored, "rev-parse", "REBASE_HEAD")).toBe(marker);
expect(await git(restored, "show", "REBASE_HEAD:bookmark.txt")).toBe("preserved rebase commit");
expect(await git(restored, "show", ":work.txt")).toBe("staged");
expect(await fs.readFile(path.join(restored, "work.txt"), "utf8")).toBe("unstaged\n");
await expect(fs.access(path.join(restored, ".git", "FETCH_HEAD"))).rejects.toMatchObject({ code: "ENOENT" });
await git(restored, "fsck", "--full", "--no-dangling");
},
);
it.each(["invalid", "missing_commit", "symlink", "directory", "unmerged"])(
"rejects an unsafe inactive rebase bookmark with %s state", async kind => {
const cwd = await source(); await git(cwd, "init", "-q", "-b", "main");
await fs.writeFile(path.join(cwd, "work.txt"), "base\n"); await git(cwd, "add", "."); await git(cwd, "commit", "-qm", "base");
const marker = path.join(cwd, ".git", "REBASE_HEAD"), head = await git(cwd, "rev-parse", "HEAD");
if (kind === "symlink") await fs.symlink("HEAD", marker);
else if (kind === "directory") await fs.mkdir(marker);
else await fs.writeFile(marker, `${kind === "invalid" ? "not-a-commit" : kind === "missing_commit" ? "a".repeat(40) : head}\n`);
if (kind === "unmerged") {
const object = await git(cwd, "rev-parse", "HEAD:work.txt");
await git(cwd, "update-index", "--force-remove", "work.txt");
const command = execFile("git", ["-C", cwd, "update-index", "--index-info"], { env });
command.stdin!.end(`100644 ${object} 1\twork.txt\n100644 ${object} 2\twork.txt\n`);
await new Promise<void>((resolve, reject) => { command.once("error", reject); command.once("exit", code => code === 0 ? resolve() : reject(new Error("index fixture failed"))); });
}
const bytes = await sourceBytes(cwd);
await expect(capture(cwd)).rejects.toThrow("remote_workspace_archive_git_operation_in_progress");
expect(await sourceBytes(cwd)).toBe(bytes);
await expect(fs.access(path.join(root, "snapshot.pcws"))).rejects.toMatchObject({ code: "ENOENT" });
},
);
it("rejects a bookmark changed after the normalized repository was captured", async () => {
const cwd = await source(); await git(cwd, "init", "-q", "-b", "main");
await fs.writeFile(path.join(cwd, "work.txt"), "base\n"); await git(cwd, "add", "."); await git(cwd, "commit", "-qm", "base");
const first = await git(cwd, "rev-parse", "HEAD");
await fs.writeFile(path.join(cwd, "work.txt"), "next\n"); await git(cwd, "commit", "-qam", "next");
const second = await git(cwd, "rev-parse", "HEAD"), marker = path.join(cwd, ".git", "REBASE_HEAD");
await fs.writeFile(marker, `${first}\n`);
const archivePath = path.join(root, "snapshot.pcws");
archiveIo.beforeOpen = async target => { if (target === archivePath) await fs.writeFile(marker, `${second}\n`); };
await expect(capture(cwd)).rejects.toThrow("remote_workspace_archive_changed");
expect(await fs.readFile(marker, "utf8")).toBe(`${second}\n`);
await expect(fs.access(archivePath)).rejects.toMatchObject({ code: "ENOENT" });
});
it.each(["rebase-merge", "rebase-apply", "linked-rebase", "sequencer", "MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"])(
"rejects a real unfinished %s operation without changing its source or publishing an archive", async operation => {
let cwd = await source(); await git(cwd, "init", "-q", "-b", "main");
@@ -132,13 +132,31 @@ async function workspaceArchive(input: {
// scripts, autostash, or operation-only object references. Inspect the
// selected worktree's administration directory without following links.
for (const name of ["rebase-merge", "rebase-apply", "sequencer", "MERGE_HEAD", "CHERRY_PICK_HEAD",
"REVERT_HEAD", "REBASE_HEAD", "MERGE_AUTOSTASH", "BISECT_START"]) {
"REVERT_HEAD", "MERGE_AUTOSTASH", "BISECT_START"]) {
const state = await fs.lstat(path.join(gitDir, name)).catch((error: NodeJS.ErrnoException) => {
if (error.code !== "ENOENT") throw error;
return null;
});
if (state) fail("git_operation_in_progress: finish or abort the Git operation before saving the workspace");
}
const bookmark = path.join(gitDir, "REBASE_HEAD");
const state = await fs.lstat(bookmark).catch((error: NodeJS.ErrnoException) => {
if (error.code !== "ENOENT") throw error;
return null;
});
if (!state) return null;
if (!state.isFile() || state.isSymbolicLink() || state.size > 128) fail("git_operation_in_progress: finish or abort the Git operation before saving the workspace");
const bytes: Buffer[] = []; let size = 0;
const entry = await readStableFile(bookmark, async chunk => {
size += chunk.length;
if (size > 128) fail("git_operation_in_progress: finish or abort the Git operation before saving the workspace");
bytes.push(chunk);
});
const object = Buffer.concat(bytes).toString("utf8").trim();
if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(object)
|| await git(repo, ["rev-parse", "--verify", "REBASE_HEAD^{commit}"]).then(value => value.trim()).catch(() => null) !== object
|| await git(repo, ["ls-files", "--unmerged", "-z"])) fail("git_operation_in_progress: finish or abort the Git operation before saving the workspace");
return { object, sha256: entry.sha256 };
};
const readStableFile = async (source: string, consume?: (bytes: Buffer) => Promise<void>) => {
check();
@@ -266,13 +284,13 @@ async function workspaceArchive(input: {
const repositories = [...new Set(directories)];
const gitSignatures = new Map<string, string>();
const signature = async (repo: string, settings?: Awaited<ReturnType<typeof portableGitSettings>>) => {
await assertGitOperationComplete(repo);
const rebaseBookmark = await assertGitOperationComplete(repo);
const index = (await git(repo, ["rev-parse", "--path-format=absolute", "--git-path", "index"])).trim();
const indexHash = await hashFile(index).catch((error: NodeJS.ErrnoException) => error.code === "ENOENT" ? null : Promise.reject(error));
const config = settings ?? await portableGitSettings(repo), shallow = await gitStateHash(repo, "shallow");
const partial = config.some(setting => /^remote\..*\.promisor$/u.test(setting.key) && setting.value === "true");
return JSON.stringify([await git(repo, ["status", "--porcelain=v2", "-z", "--untracked-files=no"]), await git(repo, ["for-each-ref", "--format=%(objectname) %(refname)"]), await git(repo, ["symbolic-ref", "-q", "HEAD"]).catch(() => git(repo, ["rev-parse", "HEAD"])), indexHash, config,
shallow, await gitStateHash(repo, "info/sparse-checkout"), shallow || partial ? (await gitObjectFiles(repo)).files : null]);
shallow, await gitStateHash(repo, "info/sparse-checkout"), shallow || partial ? (await gitObjectFiles(repo)).files : null, rebaseBookmark]);
};
for (let index = 0; index < repositories.length; index++) {
const relative = repositories[index]!, repo = path.join(root, relative), normalized = path.join(scratch, `repo-${index}.git`);
@@ -302,6 +320,7 @@ async function workspaceArchive(input: {
for (const name of ["HEAD", "index", "ORIG_HEAD", "MERGE_HEAD", "MERGE_MSG", "CHERRY_PICK_HEAD", "REVERT_HEAD"]) {
await fs.copyFile(path.join(sourceGit, name), path.join(destination, name)).catch((error: NodeJS.ErrnoException) => { if (error.code !== "ENOENT") throw error; });
}
await copyGitFile(path.join(sourceGit, "REBASE_HEAD"), path.join(destination, "REBASE_HEAD"));
for (const directory of new Set([sourceGit, sourceCommon])) {
for (const name of await fs.readdir(directory)) if (/^sharedindex\.[a-f0-9]+$/.test(name)) await fs.copyFile(path.join(directory, name), path.join(destination, name));
}
@@ -323,6 +342,13 @@ async function workspaceArchive(input: {
const [object, ref] = row.split(" "); return `update ${ref} ${object}\n`;
}).join("");
if (updates) await git(restoredRepo, ["update-ref", "--stdin"], updates);
const rebaseBookmark = await assertGitOperationComplete(repo);
if (rebaseBookmark) {
const present = await git(restoredRepo, ["cat-file", "-e", `${rebaseBookmark.object}^{commit}`]).then(() => true).catch(() => false);
if (!present && !offline) await git(restoredRepo, ["-c", "protocol.file.allow=always", "fetch", "--no-tags", "--no-recurse-submodules",
"--no-write-fetch-head", "--no-auto-maintenance", "--", repo, rebaseBookmark.object]);
await git(restoredRepo, ["cat-file", "-e", `${rebaseBookmark.object}^{commit}`]);
}
await fs.rm(path.join(destination, "objects", "info", "alternates"), { force: true });
await git(restoredRepo, ["fsck", "--full", "--no-dangling"]);
if (await signature(repo) !== gitSignatures.get(relative)) fail("changed");