From b660d2dd9ae10ce7f8d26d5f98933cbecc23f3bc Mon Sep 17 00:00:00 2001
From: Arunavo Ray
Date: Tue, 24 Jun 2025 13:51:43 +0530
Subject: [PATCH] feat: add mixed strategy for repository mirroring, enhancing
organization handling for personal and organizational repos
---
.../config/OrganizationConfiguration.tsx | 18 ++++--
.../config/OrganizationStrategy.tsx | 60 ++++++++++++++++++-
src/lib/db/schema.ts | 2 +-
src/lib/gitea.test.ts | 43 +++++++++++++
src/lib/gitea.ts | 13 ++++
src/lib/utils/config-mapper.ts | 2 +-
src/types/config.ts | 2 +-
7 files changed, 131 insertions(+), 9 deletions(-)
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" ? (
- Destination Organization
+ {strategy === "mixed" ? "Personal Repos Organization" : "Destination Organization"}
- All repositories will be mirrored to this organization
+
+ {strategy === "mixed"
+ ? "Personal repositories will be mirrored to this organization, while organization repos preserve their structure"
+ : "All repositories will be mirrored to this organization"
+ }
+
@@ -103,7 +108,10 @@ export const OrganizationConfiguration: React.FC
className=""
/>
- Organization for consolidated repositories
+ {strategy === "mixed"
+ ? "All personal repos will go to this organization"
+ : "Organization for consolidated repositories"
+ }
) : 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;