mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-08 04:26:44 +03:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00d516a59d | ||
|
|
d68b822c76 | ||
|
|
b660d2dd9a |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.18.0] - 2025-06-24
|
||||
|
||||
### Added
|
||||
- Fourth organization strategy "Mixed Mode" that combines aspects of existing strategies
|
||||
- Personal repositories go to a single configurable organization
|
||||
- Organization repositories preserve their GitHub organization structure
|
||||
- "Override Options" info button in Organization Strategy component explaining customization features
|
||||
- Organization overrides via edit buttons on organization cards
|
||||
- Repository overrides via inline destination editor
|
||||
- Starred repositories behavior and priority hierarchy
|
||||
|
||||
### Improved
|
||||
- Simplified mixed strategy implementation to reuse existing database fields
|
||||
- Enhanced organization strategy UI with comprehensive override documentation
|
||||
- Better visual indicators for the new mixed strategy with orange color theme
|
||||
|
||||
## [2.17.0] - 2025-06-24
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gitea-mirror",
|
||||
"type": "module",
|
||||
"version": "2.17.0",
|
||||
"version": "2.18.0",
|
||||
"engines": {
|
||||
"bun": ">=1.2.9"
|
||||
},
|
||||
|
||||
@@ -79,18 +79,23 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 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" ? (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="destinationOrg" className="text-sm font-normal flex items-center gap-2">
|
||||
Destination Organization
|
||||
{strategy === "mixed" ? "Personal Repos Organization" : "Destination Organization"}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<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>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -103,7 +108,10 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
||||
className=""
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
) : strategy === "preserve" ? (
|
||||
|
||||
@@ -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<{
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -223,14 +281,78 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||
<Building className="h-4 w-4" />
|
||||
Organization Strategy
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
Choose how your repositories will be organized in Gitea
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose how your repositories will be organized in Gitea
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<button
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors"
|
||||
type="button"
|
||||
>
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
<span>Override Options</span>
|
||||
</button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="left" align="start" className="w-[380px]">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h4 className="font-medium text-sm mb-1.5">Fine-tune Your Mirror Destinations</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
After selecting a strategy, you can customize destinations for specific organizations and repositories.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5 pt-2 border-t">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="h-3.5 w-3.5 text-primary" />
|
||||
<span className="text-xs font-medium">Organization Overrides</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pl-5">
|
||||
Click the edit button on any organization card to redirect all its repositories to a different Gitea organization.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="h-3.5 w-3.5 text-primary" />
|
||||
<span className="text-xs font-medium">Repository Overrides</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pl-5">
|
||||
Use the inline editor in the repository table's "Destination" column to set custom destinations for individual repositories.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="h-3.5 w-3.5 text-yellow-500" />
|
||||
<span className="text-xs font-medium">Starred Repositories</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pl-5">
|
||||
Always go to the configured starred repos organization and cannot be overridden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<span className="font-medium">Priority:</span> Repository override → Organization override → Strategy default
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RadioGroup value={strategy} onValueChange={onStrategyChange}>
|
||||
|
||||
@@ -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(["*"]),
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user