Search sibling storage roots for attachments
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -196,13 +196,37 @@ describe("worktree helpers", () => {
|
||||
expect(formatShellExports(env)).toContain("export PAPERCLIP_INSTANCE_ID='feature-worktree-support'");
|
||||
});
|
||||
|
||||
it("treats missing source attachment objects as a non-fatal skip", async () => {
|
||||
it("falls back across storage roots before skipping a missing attachment object", async () => {
|
||||
const missingErr = Object.assign(new Error("missing"), { code: "ENOENT" });
|
||||
const expected = Buffer.from("image-bytes");
|
||||
await expect(
|
||||
readSourceAttachmentBody(
|
||||
[
|
||||
{
|
||||
getObject: vi.fn().mockRejectedValue(missingErr),
|
||||
},
|
||||
{
|
||||
getObject: vi.fn().mockResolvedValue(expected),
|
||||
},
|
||||
],
|
||||
"company-1",
|
||||
"company-1/issues/issue-1/missing.png",
|
||||
),
|
||||
).resolves.toEqual(expected);
|
||||
});
|
||||
|
||||
it("returns null when an attachment object is missing from every lookup storage", async () => {
|
||||
const missingErr = Object.assign(new Error("missing"), { code: "ENOENT" });
|
||||
await expect(
|
||||
readSourceAttachmentBody(
|
||||
[
|
||||
{
|
||||
getObject: vi.fn().mockRejectedValue(missingErr),
|
||||
},
|
||||
{
|
||||
getObject: vi.fn().mockRejectedValue(Object.assign(new Error("missing"), { status: 404 })),
|
||||
},
|
||||
],
|
||||
"company-1",
|
||||
"company-1/issues/issue-1/missing.png",
|
||||
),
|
||||
|
||||
@@ -360,19 +360,22 @@ export function isMissingStorageObjectError(error: unknown): boolean {
|
||||
}
|
||||
|
||||
export async function readSourceAttachmentBody(
|
||||
sourceStorage: Pick<ConfiguredStorage, "getObject">,
|
||||
sourceStorages: Array<Pick<ConfiguredStorage, "getObject">>,
|
||||
companyId: string,
|
||||
objectKey: string,
|
||||
): Promise<Buffer | null> {
|
||||
for (const sourceStorage of sourceStorages) {
|
||||
try {
|
||||
return await sourceStorage.getObject(companyId, objectKey);
|
||||
} catch (error) {
|
||||
if (isMissingStorageObjectError(error)) {
|
||||
return null;
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolveWorktreeMakeTargetPath(name: string): string {
|
||||
return path.resolve(os.homedir(), resolveWorktreeMakeName(name));
|
||||
@@ -1350,6 +1353,29 @@ function resolveCurrentEndpoint(): ResolvedWorktreeEndpoint {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveAttachmentLookupStorages(input: {
|
||||
sourceEndpoint: ResolvedWorktreeEndpoint;
|
||||
targetEndpoint: ResolvedWorktreeEndpoint;
|
||||
}): ConfiguredStorage[] {
|
||||
const orderedConfigPaths = [
|
||||
input.sourceEndpoint.configPath,
|
||||
resolveCurrentEndpoint().configPath,
|
||||
input.targetEndpoint.configPath,
|
||||
...toMergeSourceChoices(process.cwd())
|
||||
.filter((choice) => choice.hasPaperclipConfig)
|
||||
.map((choice) => path.resolve(choice.worktree, ".paperclip", "config.json")),
|
||||
];
|
||||
const seen = new Set<string>();
|
||||
const storages: ConfiguredStorage[] = [];
|
||||
for (const configPath of orderedConfigPaths) {
|
||||
const resolved = path.resolve(configPath);
|
||||
if (seen.has(resolved) || !existsSync(resolved)) continue;
|
||||
seen.add(resolved);
|
||||
storages.push(openConfiguredStorage(resolved));
|
||||
}
|
||||
return storages;
|
||||
}
|
||||
|
||||
async function openConfiguredDb(configPath: string): Promise<OpenDbHandle> {
|
||||
const config = readConfig(configPath);
|
||||
if (!config) {
|
||||
@@ -1930,7 +1956,7 @@ async function promptForSourceEndpoint(excludeWorktreePath?: string): Promise<Re
|
||||
}
|
||||
|
||||
async function applyMergePlan(input: {
|
||||
sourceStorage: ConfiguredStorage;
|
||||
sourceStorages: ConfiguredStorage[];
|
||||
targetStorage: ConfiguredStorage;
|
||||
targetDb: ClosableDb;
|
||||
company: ResolvedMergeCompany;
|
||||
@@ -2194,7 +2220,7 @@ async function applyMergePlan(input: {
|
||||
if (!parentExists) continue;
|
||||
|
||||
const body = await readSourceAttachmentBody(
|
||||
input.sourceStorage,
|
||||
input.sourceStorages,
|
||||
companyId,
|
||||
attachment.source.objectKey,
|
||||
);
|
||||
@@ -2274,7 +2300,10 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined,
|
||||
const scopes = parseWorktreeMergeScopes(opts.scope);
|
||||
const sourceHandle = await openConfiguredDb(sourceEndpoint.configPath);
|
||||
const targetHandle = await openConfiguredDb(targetEndpoint.configPath);
|
||||
const sourceStorage = openConfiguredStorage(sourceEndpoint.configPath);
|
||||
const sourceStorages = resolveAttachmentLookupStorages({
|
||||
sourceEndpoint,
|
||||
targetEndpoint,
|
||||
});
|
||||
const targetStorage = openConfiguredStorage(targetEndpoint.configPath);
|
||||
|
||||
try {
|
||||
@@ -2328,7 +2357,7 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined,
|
||||
}
|
||||
|
||||
const applied = await applyMergePlan({
|
||||
sourceStorage,
|
||||
sourceStorages,
|
||||
targetStorage,
|
||||
targetDb: targetHandle.db,
|
||||
company,
|
||||
|
||||
Reference in New Issue
Block a user