mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-10 05:26:44 +03:00
feat: add mixed strategy for repository mirroring, enhancing organization handling for personal and organizational repos
This commit is contained in:
@@ -79,18 +79,23 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right column - shows destination org for single-org, personal repos org for preserve, empty div for others */}
|
{/* Right column - shows destination org for single-org/mixed, personal repos org for preserve, empty div for others */}
|
||||||
{strategy === "single-org" ? (
|
{strategy === "single-org" || strategy === "mixed" ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="destinationOrg" className="text-sm font-normal flex items-center gap-2">
|
<Label htmlFor="destinationOrg" className="text-sm font-normal flex items-center gap-2">
|
||||||
Destination Organization
|
{strategy === "mixed" ? "Personal Repos Organization" : "Destination Organization"}
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>All repositories will be mirrored to this organization</p>
|
<p>
|
||||||
|
{strategy === "mixed"
|
||||||
|
? "Personal repositories will be mirrored to this organization, while organization repos preserve their structure"
|
||||||
|
: "All repositories will be mirrored to this organization"
|
||||||
|
}
|
||||||
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -103,7 +108,10 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
|||||||
className=""
|
className=""
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
Organization for consolidated repositories
|
{strategy === "mixed"
|
||||||
|
? "All personal repos will go to this organization"
|
||||||
|
: "Organization for consolidated repositories"
|
||||||
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : strategy === "preserve" ? (
|
) : strategy === "preserve" ? (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "@/components/ui/hover-card";
|
} from "@/components/ui/hover-card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type MirrorStrategy = "preserve" | "single-org" | "flat-user";
|
export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed";
|
||||||
|
|
||||||
interface OrganizationStrategyProps {
|
interface OrganizationStrategyProps {
|
||||||
strategy: MirrorStrategy;
|
strategy: MirrorStrategy;
|
||||||
@@ -56,6 +56,18 @@ const strategyConfig = {
|
|||||||
bg: "bg-green-50 dark:bg-green-950/30",
|
bg: "bg-green-50 dark:bg-green-950/30",
|
||||||
icon: "text-green-600 dark:text-green-400"
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,6 +222,52 @@ const MappingPreview: React.FC<{
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strategy === "mixed") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-2">GitHub</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<User className="h-3 w-3" />
|
||||||
|
<span className={cn(isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<Building2 className="h-3 w-3" />
|
||||||
|
<span>my-org/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||||
|
<Star className="h-3 w-3" />
|
||||||
|
<span>awesome/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-2">Gitea</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span>{destinationOrg || "github-mirrors"}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span>my-org/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||||
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
|
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const configSchema = z.object({
|
|||||||
visibility: z.enum(["public", "private", "limited"]).default("public"),
|
visibility: z.enum(["public", "private", "limited"]).default("public"),
|
||||||
starredReposOrg: z.string().default("github"),
|
starredReposOrg: z.string().default("github"),
|
||||||
preserveOrgStructure: z.boolean().default(false),
|
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
|
personalReposOrg: z.string().optional(), // Override destination for personal repos
|
||||||
}),
|
}),
|
||||||
include: z.array(z.string()).default(["*"]),
|
include: z.array(z.string()).default(["*"]),
|
||||||
|
|||||||
@@ -378,4 +378,47 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
|
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
|
||||||
expect(result).toBe("myorg");
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -136,6 +136,19 @@ export const getGiteaRepoOwner = ({
|
|||||||
// All non-starred repos go under the user account
|
// All non-starred repos go under the user account
|
||||||
return config.giteaConfig.username;
|
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:
|
||||||
// Default fallback
|
// Default fallback
|
||||||
return config.giteaConfig.username;
|
return config.giteaConfig.username;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ interface DbGiteaConfig {
|
|||||||
visibility: "public" | "private" | "limited";
|
visibility: "public" | "private" | "limited";
|
||||||
starredReposOrg: string;
|
starredReposOrg: string;
|
||||||
preserveOrgStructure: boolean;
|
preserveOrgStructure: boolean;
|
||||||
mirrorStrategy?: "preserve" | "single-org" | "flat-user";
|
mirrorStrategy?: "preserve" | "single-org" | "flat-user" | "mixed";
|
||||||
personalReposOrg?: string;
|
personalReposOrg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type Config as ConfigType } from "@/lib/db/schema";
|
import { type Config as ConfigType } from "@/lib/db/schema";
|
||||||
|
|
||||||
export type GiteaOrgVisibility = "public" | "private" | "limited";
|
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 {
|
export interface GiteaConfig {
|
||||||
url: string;
|
url: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user