Skip missing worktree attachment objects
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
73ada45037
commit
fb63d61ae5
2 changed files with 55 additions and 1 deletions
|
|
@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
copyGitHooksToWorktreeGitDir,
|
copyGitHooksToWorktreeGitDir,
|
||||||
copySeededSecretsKey,
|
copySeededSecretsKey,
|
||||||
|
readSourceAttachmentBody,
|
||||||
rebindWorkspaceCwd,
|
rebindWorkspaceCwd,
|
||||||
resolveSourceConfigPath,
|
resolveSourceConfigPath,
|
||||||
resolveGitWorktreeAddArgs,
|
resolveGitWorktreeAddArgs,
|
||||||
|
|
@ -195,6 +196,19 @@ describe("worktree helpers", () => {
|
||||||
expect(formatShellExports(env)).toContain("export PAPERCLIP_INSTANCE_ID='feature-worktree-support'");
|
expect(formatShellExports(env)).toContain("export PAPERCLIP_INSTANCE_ID='feature-worktree-support'");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats missing source attachment objects as a non-fatal skip", async () => {
|
||||||
|
const missingErr = Object.assign(new Error("missing"), { code: "ENOENT" });
|
||||||
|
await expect(
|
||||||
|
readSourceAttachmentBody(
|
||||||
|
{
|
||||||
|
getObject: vi.fn().mockRejectedValue(missingErr),
|
||||||
|
},
|
||||||
|
"company-1",
|
||||||
|
"company-1/issues/issue-1/missing.png",
|
||||||
|
),
|
||||||
|
).resolves.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("generates vivid worktree colors as hex", () => {
|
it("generates vivid worktree colors as hex", () => {
|
||||||
expect(generateWorktreeColor()).toMatch(/^#[0-9a-f]{6}$/);
|
expect(generateWorktreeColor()).toMatch(/^#[0-9a-f]{6}$/);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -349,6 +349,31 @@ async function streamToBuffer(stream: NodeJS.ReadableStream): Promise<Buffer> {
|
||||||
return Buffer.concat(chunks);
|
return Buffer.concat(chunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMissingStorageObjectError(error: unknown): boolean {
|
||||||
|
if (!error || typeof error !== "object") return false;
|
||||||
|
const candidate = error as { code?: unknown; status?: unknown; name?: unknown; message?: unknown };
|
||||||
|
return candidate.code === "ENOENT"
|
||||||
|
|| candidate.status === 404
|
||||||
|
|| candidate.name === "NoSuchKey"
|
||||||
|
|| candidate.name === "NotFound"
|
||||||
|
|| candidate.message === "Object not found.";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readSourceAttachmentBody(
|
||||||
|
sourceStorage: Pick<ConfiguredStorage, "getObject">,
|
||||||
|
companyId: string,
|
||||||
|
objectKey: string,
|
||||||
|
): Promise<Buffer | null> {
|
||||||
|
try {
|
||||||
|
return await sourceStorage.getObject(companyId, objectKey);
|
||||||
|
} catch (error) {
|
||||||
|
if (isMissingStorageObjectError(error)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveWorktreeMakeTargetPath(name: string): string {
|
export function resolveWorktreeMakeTargetPath(name: string): string {
|
||||||
return path.resolve(os.homedir(), resolveWorktreeMakeName(name));
|
return path.resolve(os.homedir(), resolveWorktreeMakeName(name));
|
||||||
}
|
}
|
||||||
|
|
@ -2158,6 +2183,7 @@ async function applyMergePlan(input: {
|
||||||
).map((row) => row.id),
|
).map((row) => row.id),
|
||||||
);
|
);
|
||||||
let insertedAttachments = 0;
|
let insertedAttachments = 0;
|
||||||
|
let skippedMissingAttachmentObjects = 0;
|
||||||
for (const attachment of attachmentCandidates) {
|
for (const attachment of attachmentCandidates) {
|
||||||
if (existingAttachmentIds.has(attachment.source.id)) continue;
|
if (existingAttachmentIds.has(attachment.source.id)) continue;
|
||||||
const parentExists = await tx
|
const parentExists = await tx
|
||||||
|
|
@ -2167,7 +2193,15 @@ async function applyMergePlan(input: {
|
||||||
.then((rows) => rows[0] ?? null);
|
.then((rows) => rows[0] ?? null);
|
||||||
if (!parentExists) continue;
|
if (!parentExists) continue;
|
||||||
|
|
||||||
const body = await input.sourceStorage.getObject(companyId, attachment.source.objectKey);
|
const body = await readSourceAttachmentBody(
|
||||||
|
input.sourceStorage,
|
||||||
|
companyId,
|
||||||
|
attachment.source.objectKey,
|
||||||
|
);
|
||||||
|
if (!body) {
|
||||||
|
skippedMissingAttachmentObjects += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
await input.targetStorage.putObject(
|
await input.targetStorage.putObject(
|
||||||
companyId,
|
companyId,
|
||||||
attachment.source.objectKey,
|
attachment.source.objectKey,
|
||||||
|
|
@ -2209,6 +2243,7 @@ async function applyMergePlan(input: {
|
||||||
mergedDocuments,
|
mergedDocuments,
|
||||||
insertedDocumentRevisions,
|
insertedDocumentRevisions,
|
||||||
insertedAttachments,
|
insertedAttachments,
|
||||||
|
skippedMissingAttachmentObjects,
|
||||||
insertedIssueIdentifiers,
|
insertedIssueIdentifiers,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -2299,6 +2334,11 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined,
|
||||||
company,
|
company,
|
||||||
plan: collected.plan,
|
plan: collected.plan,
|
||||||
});
|
});
|
||||||
|
if (applied.skippedMissingAttachmentObjects > 0) {
|
||||||
|
p.log.warn(
|
||||||
|
`Skipped ${applied.skippedMissingAttachmentObjects} attachments whose source files were missing from storage.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
p.outro(
|
p.outro(
|
||||||
pc.green(
|
pc.green(
|
||||||
`Imported ${applied.insertedIssues} issues, ${applied.insertedComments} comments, ${applied.insertedDocuments} documents (${applied.insertedDocumentRevisions} revisions, ${applied.mergedDocuments} merged), and ${applied.insertedAttachments} attachments into ${company.issuePrefix}.`,
|
`Imported ${applied.insertedIssues} issues, ${applied.insertedComments} comments, ${applied.insertedDocuments} documents (${applied.insertedDocumentRevisions} revisions, ${applied.mergedDocuments} merged), and ${applied.insertedAttachments} attachments into ${company.issuePrefix}.`,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue