diff --git a/src/components/config/OrganizationConfiguration.tsx b/src/components/config/OrganizationConfiguration.tsx index b04545b..2156889 100644 --- a/src/components/config/OrganizationConfiguration.tsx +++ b/src/components/config/OrganizationConfiguration.tsx @@ -79,18 +79,23 @@ export const OrganizationConfiguration: React.FC

- {/* Right column - shows destination org for single-org, personal repos org for preserve, empty div for others */} - {strategy === "single-org" ? ( + {/* Right column - shows destination org for single-org/mixed, personal repos org for preserve, empty div for others */} + {strategy === "single-org" || strategy === "mixed" ? (
) : strategy === "preserve" ? ( diff --git a/src/components/config/OrganizationStrategy.tsx b/src/components/config/OrganizationStrategy.tsx index 52d470d..0bdce9d 100644 --- a/src/components/config/OrganizationStrategy.tsx +++ b/src/components/config/OrganizationStrategy.tsx @@ -9,7 +9,7 @@ import { } from "@/components/ui/hover-card"; import { cn } from "@/lib/utils"; -export type MirrorStrategy = "preserve" | "single-org" | "flat-user"; +export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed"; interface OrganizationStrategyProps { strategy: MirrorStrategy; @@ -56,6 +56,18 @@ const strategyConfig = { bg: "bg-green-50 dark:bg-green-950/30", icon: "text-green-600 dark:text-green-400" } + }, + "mixed": { + title: "Mixed Mode", + icon: GitBranch, + description: "user repos in single org, org repos preserve structure", + color: "text-orange-600 dark:text-orange-400", + bgColor: "bg-orange-50 dark:bg-orange-950/20", + borderColor: "border-orange-200 dark:border-orange-900", + repoColors: { + bg: "bg-orange-50 dark:bg-orange-950/30", + icon: "text-orange-600 dark:text-orange-400" + } } }; @@ -209,6 +221,52 @@ const MappingPreview: React.FC<{ ); } + + if (strategy === "mixed") { + return ( +
+
+
GitHub
+
+
+ + {displayGithubUsername}/my-repo +
+
+ + my-org/team-repo +
+
+ + awesome/starred-repo +
+
+
+ +
+ +
+ +
+
Gitea
+
+
+ + {destinationOrg || "github-mirrors"}/my-repo +
+
+ + my-org/team-repo +
+
+ + {starredReposOrg || "starred"}/starred-repo +
+
+
+
+ ); + } return null; }; diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 7c59bfd..b9decb5 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -44,7 +44,7 @@ export const configSchema = z.object({ visibility: z.enum(["public", "private", "limited"]).default("public"), starredReposOrg: z.string().default("github"), preserveOrgStructure: z.boolean().default(false), - mirrorStrategy: z.enum(["preserve", "single-org", "flat-user"]).optional(), + mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).optional(), personalReposOrg: z.string().optional(), // Override destination for personal repos }), include: z.array(z.string()).default(["*"]), diff --git a/src/lib/gitea.test.ts b/src/lib/gitea.test.ts index 6c4183f..f2dfae1 100644 --- a/src/lib/gitea.test.ts +++ b/src/lib/gitea.test.ts @@ -378,4 +378,47 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => { const result = getGiteaRepoOwner({ config: baseConfig, repository: repo }); expect(result).toBe("myorg"); }); + + test("mixed strategy: personal repos go to organization", () => { + const configWithMixed = { + ...baseConfig, + giteaConfig: { + ...baseConfig.giteaConfig!, + mirrorStrategy: "mixed" as const, + organization: "github-mirrors" + } + }; + const repo = { ...baseRepo, organization: undefined }; + const result = getGiteaRepoOwner({ config: configWithMixed, repository: repo }); + expect(result).toBe("github-mirrors"); + }); + + test("mixed strategy: org repos preserve their structure", () => { + const configWithMixed = { + ...baseConfig, + giteaConfig: { + ...baseConfig.giteaConfig!, + mirrorStrategy: "mixed" as const, + organization: "github-mirrors" + } + }; + const repo = { ...baseRepo, organization: "myorg" }; + const result = getGiteaRepoOwner({ config: configWithMixed, repository: repo }); + expect(result).toBe("myorg"); + }); + + test("mixed strategy: fallback to username if no org configs", () => { + const configWithMixed = { + ...baseConfig, + giteaConfig: { + ...baseConfig.giteaConfig!, + mirrorStrategy: "mixed" as const, + organization: undefined, + personalReposOrg: undefined + } + }; + const repo = { ...baseRepo, organization: undefined }; + const result = getGiteaRepoOwner({ config: configWithMixed, repository: repo }); + expect(result).toBe("giteauser"); + }); }); diff --git a/src/lib/gitea.ts b/src/lib/gitea.ts index c80053e..9233fcf 100644 --- a/src/lib/gitea.ts +++ b/src/lib/gitea.ts @@ -136,6 +136,19 @@ export const getGiteaRepoOwner = ({ // All non-starred repos go under the user account return config.giteaConfig.username; + case "mixed": + // Mixed mode: personal repos to single org, organization repos preserve structure + if (repository.organization) { + // Organization repos preserve their structure + return repository.organization; + } + // Personal repos go to configured organization (same as single-org) + if (config.giteaConfig.organization) { + return config.giteaConfig.organization; + } + // Fallback to username if no organization specified + return config.giteaConfig.username; + default: // Default fallback return config.giteaConfig.username; diff --git a/src/lib/utils/config-mapper.ts b/src/lib/utils/config-mapper.ts index 8b157b9..45f3511 100644 --- a/src/lib/utils/config-mapper.ts +++ b/src/lib/utils/config-mapper.ts @@ -35,7 +35,7 @@ interface DbGiteaConfig { visibility: "public" | "private" | "limited"; starredReposOrg: string; preserveOrgStructure: boolean; - mirrorStrategy?: "preserve" | "single-org" | "flat-user"; + mirrorStrategy?: "preserve" | "single-org" | "flat-user" | "mixed"; personalReposOrg?: string; } diff --git a/src/types/config.ts b/src/types/config.ts index 4fbdeb5..8dfd682 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -1,7 +1,7 @@ import { type Config as ConfigType } from "@/lib/db/schema"; export type GiteaOrgVisibility = "public" | "private" | "limited"; -export type MirrorStrategy = "preserve" | "single-org" | "flat-user"; +export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed"; export interface GiteaConfig { url: string;