Add merge-history project import option

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta 2026-03-21 17:09:46 -05:00
parent 8b4850aaea
commit 5dfdbe91bb
3 changed files with 289 additions and 12 deletions

View file

@ -50,7 +50,7 @@ export type PlannedIssueInsert = {
targetProjectId: string | null;
targetProjectWorkspaceId: string | null;
targetGoalId: string | null;
projectResolution: "preserved" | "cleared" | "mapped";
projectResolution: "preserved" | "cleared" | "mapped" | "imported";
mappedProjectName: string | null;
adjustments: ImportAdjustment[];
};
@ -173,17 +173,26 @@ export type PlannedAttachmentSkip = {
action: "skip_existing" | "skip_missing_parent";
};
export type PlannedProjectImport = {
source: ProjectRow;
targetLeadAgentId: string | null;
targetGoalId: string | null;
workspaces: ProjectWorkspaceRow[];
};
export type WorktreeMergePlan = {
companyId: string;
companyName: string;
issuePrefix: string;
previewIssueCounterStart: number;
scopes: WorktreeMergeScope[];
projectImports: PlannedProjectImport[];
issuePlans: Array<PlannedIssueInsert | PlannedIssueSkip>;
commentPlans: Array<PlannedCommentInsert | PlannedCommentSkip>;
documentPlans: Array<PlannedIssueDocumentInsert | PlannedIssueDocumentMerge | PlannedIssueDocumentSkip>;
attachmentPlans: Array<PlannedAttachmentInsert | PlannedAttachmentSkip>;
counts: {
projectsToImport: number;
issuesToInsert: number;
issuesExisting: number;
issueDrift: number;
@ -338,6 +347,8 @@ export function buildWorktreeMergePlan(input: {
targetIssues: IssueRow[];
sourceComments: CommentRow[];
targetComments: CommentRow[];
sourceProjects?: ProjectRow[];
sourceProjectWorkspaces?: ProjectWorkspaceRow[];
sourceDocuments?: IssueDocumentRow[];
targetDocuments?: IssueDocumentRow[];
sourceDocumentRevisions?: DocumentRevisionRow[];
@ -348,6 +359,7 @@ export function buildWorktreeMergePlan(input: {
targetProjects: ProjectRow[];
targetProjectWorkspaces: ProjectWorkspaceRow[];
targetGoals: GoalRow[];
importProjectIds?: Iterable<string>;
projectIdOverrides?: Record<string, string | null | undefined>;
}): WorktreeMergePlan {
const targetIssuesById = new Map(input.targetIssues.map((issue) => [issue.id, issue]));
@ -357,6 +369,10 @@ export function buildWorktreeMergePlan(input: {
const targetProjectsById = new Map(input.targetProjects.map((project) => [project.id, project]));
const targetProjectWorkspaceIds = new Set(input.targetProjectWorkspaces.map((workspace) => workspace.id));
const targetGoalIds = new Set(input.targetGoals.map((goal) => goal.id));
const sourceProjectsById = new Map((input.sourceProjects ?? []).map((project) => [project.id, project]));
const sourceProjectWorkspaces = input.sourceProjectWorkspaces ?? [];
const sourceProjectWorkspacesByProjectId = groupBy(sourceProjectWorkspaces, (workspace) => workspace.projectId);
const importProjectIds = new Set(input.importProjectIds ?? []);
const scopes = new Set(input.scopes);
const adjustmentCounts: Record<ImportAdjustment, number> = {
@ -371,6 +387,34 @@ export function buildWorktreeMergePlan(input: {
clear_attachment_agent: 0,
};
const projectImports: PlannedProjectImport[] = [];
for (const projectId of importProjectIds) {
if (targetProjectIds.has(projectId)) continue;
const sourceProject = sourceProjectsById.get(projectId);
if (!sourceProject) continue;
projectImports.push({
source: sourceProject,
targetLeadAgentId:
sourceProject.leadAgentId && targetAgentIds.has(sourceProject.leadAgentId)
? sourceProject.leadAgentId
: null,
targetGoalId:
sourceProject.goalId && targetGoalIds.has(sourceProject.goalId)
? sourceProject.goalId
: null,
workspaces: [...(sourceProjectWorkspacesByProjectId.get(projectId) ?? [])].sort((left, right) => {
const primaryDelta = Number(right.isPrimary) - Number(left.isPrimary);
if (primaryDelta !== 0) return primaryDelta;
const createdDelta = left.createdAt.getTime() - right.createdAt.getTime();
if (createdDelta !== 0) return createdDelta;
return left.id.localeCompare(right.id);
}),
});
}
const importedProjectWorkspaceIds = new Set(
projectImports.flatMap((project) => project.workspaces.map((workspace) => workspace.id)),
);
const issuePlans: Array<PlannedIssueInsert | PlannedIssueSkip> = [];
let nextPreviewIssueNumber = input.previewIssueCounterStart;
for (const issue of sortIssuesForImport(input.sourceIssues)) {
@ -409,6 +453,14 @@ export function buildWorktreeMergePlan(input: {
projectResolution = "mapped";
mappedProjectName = targetProjectsById.get(overrideProjectId)?.name ?? null;
}
if (!targetProjectId && issue.projectId && importProjectIds.has(issue.projectId)) {
const sourceProject = sourceProjectsById.get(issue.projectId);
if (sourceProject) {
targetProjectId = sourceProject.id;
projectResolution = "imported";
mappedProjectName = sourceProject.name;
}
}
if (issue.projectId && !targetProjectId) {
adjustments.push("clear_project");
incrementAdjustment(adjustmentCounts, "clear_project");
@ -418,7 +470,8 @@ export function buildWorktreeMergePlan(input: {
targetProjectId
&& targetProjectId === issue.projectId
&& issue.projectWorkspaceId
&& targetProjectWorkspaceIds.has(issue.projectWorkspaceId)
&& (targetProjectWorkspaceIds.has(issue.projectWorkspaceId)
|| importedProjectWorkspaceIds.has(issue.projectWorkspaceId))
? issue.projectWorkspaceId
: null;
if (issue.projectWorkspaceId && !targetProjectWorkspaceId) {
@ -672,6 +725,7 @@ export function buildWorktreeMergePlan(input: {
}
const counts = {
projectsToImport: projectImports.length,
issuesToInsert: issuePlans.filter((plan) => plan.action === "insert").length,
issuesExisting: issuePlans.filter((plan) => plan.action === "skip_existing").length,
issueDrift: issuePlans.filter((plan) => plan.action === "skip_existing" && plan.driftKeys.length > 0).length,
@ -699,6 +753,7 @@ export function buildWorktreeMergePlan(input: {
issuePrefix: input.issuePrefix,
previewIssueCounterStart: input.previewIssueCounterStart,
scopes: input.scopes,
projectImports,
issuePlans,
commentPlans,
documentPlans,