mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-03-14 06:23:01 +03:00
Allow starred repos to be mirrored preserving structure
This commit is contained in:
@@ -47,6 +47,7 @@ DOCKER_TAG=latest
|
|||||||
# SKIP_FORKS=false
|
# SKIP_FORKS=false
|
||||||
# MIRROR_STARRED=false
|
# MIRROR_STARRED=false
|
||||||
# STARRED_REPOS_ORG=starred # Organization name for starred repos
|
# STARRED_REPOS_ORG=starred # Organization name for starred repos
|
||||||
|
# STARRED_REPOS_MODE=dedicated-org # dedicated-org | preserve-owner
|
||||||
|
|
||||||
# Organization Settings
|
# Organization Settings
|
||||||
# MIRROR_ORGANIZATIONS=false
|
# MIRROR_ORGANIZATIONS=false
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ bun run dev
|
|||||||
3. **Customization**
|
3. **Customization**
|
||||||
- Click edit buttons on organization cards to set custom destinations
|
- Click edit buttons on organization cards to set custom destinations
|
||||||
- Override individual repository destinations in the table view
|
- Override individual repository destinations in the table view
|
||||||
- Starred repositories automatically go to a dedicated organization
|
- Starred repositories can go to a dedicated org or preserve source owner/org paths
|
||||||
|
|
||||||
## Advanced Features
|
## Advanced Features
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ Settings for connecting to and configuring GitHub repository sources.
|
|||||||
| `SKIP_FORKS` | Skip forked repositories | `false` | `true`, `false` |
|
| `SKIP_FORKS` | Skip forked repositories | `false` | `true`, `false` |
|
||||||
| `MIRROR_STARRED` | Mirror starred repositories | `false` | `true`, `false` |
|
| `MIRROR_STARRED` | Mirror starred repositories | `false` | `true`, `false` |
|
||||||
| `STARRED_REPOS_ORG` | Organization name for starred repos | `starred` | Any string |
|
| `STARRED_REPOS_ORG` | Organization name for starred repos | `starred` | Any string |
|
||||||
|
| `STARRED_REPOS_MODE` | How starred repos are mirrored | `dedicated-org` | `dedicated-org`, `preserve-owner` |
|
||||||
|
|
||||||
### Organization Settings
|
### Organization Settings
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export function ConfigTabs() {
|
|||||||
organization: 'github-mirrors',
|
organization: 'github-mirrors',
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
starredReposOrg: 'starred',
|
starredReposOrg: 'starred',
|
||||||
|
starredReposMode: 'dedicated-org',
|
||||||
preserveOrgStructure: false,
|
preserveOrgStructure: false,
|
||||||
},
|
},
|
||||||
scheduleConfig: {
|
scheduleConfig: {
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
|||||||
strategy={mirrorStrategy}
|
strategy={mirrorStrategy}
|
||||||
destinationOrg={config.organization}
|
destinationOrg={config.organization}
|
||||||
starredReposOrg={config.starredReposOrg}
|
starredReposOrg={config.starredReposOrg}
|
||||||
|
starredReposMode={config.starredReposMode}
|
||||||
onStrategyChange={setMirrorStrategy}
|
onStrategyChange={setMirrorStrategy}
|
||||||
githubUsername={githubUsername}
|
githubUsername={githubUsername}
|
||||||
giteaUsername={config.username}
|
giteaUsername={config.username}
|
||||||
@@ -235,6 +236,7 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
|||||||
strategy={mirrorStrategy}
|
strategy={mirrorStrategy}
|
||||||
destinationOrg={config.organization}
|
destinationOrg={config.organization}
|
||||||
starredReposOrg={config.starredReposOrg}
|
starredReposOrg={config.starredReposOrg}
|
||||||
|
starredReposMode={config.starredReposMode}
|
||||||
personalReposOrg={config.personalReposOrg}
|
personalReposOrg={config.personalReposOrg}
|
||||||
visibility={config.visibility}
|
visibility={config.visibility}
|
||||||
onDestinationOrgChange={(org) => {
|
onDestinationOrgChange={(org) => {
|
||||||
@@ -247,6 +249,11 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
|||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
if (onAutoSave) onAutoSave(newConfig);
|
if (onAutoSave) onAutoSave(newConfig);
|
||||||
}}
|
}}
|
||||||
|
onStarredReposModeChange={(mode) => {
|
||||||
|
const newConfig = { ...config, starredReposMode: mode };
|
||||||
|
setConfig(newConfig);
|
||||||
|
if (onAutoSave) onAutoSave(newConfig);
|
||||||
|
}}
|
||||||
onPersonalReposOrgChange={(org) => {
|
onPersonalReposOrgChange={(org) => {
|
||||||
const newConfig = { ...config, personalReposOrg: org };
|
const newConfig = { ...config, personalReposOrg: org };
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
|
|||||||
@@ -9,16 +9,18 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { MirrorStrategy, GiteaOrgVisibility } from "@/types/config";
|
import type { MirrorStrategy, GiteaOrgVisibility, StarredReposMode } from "@/types/config";
|
||||||
|
|
||||||
interface OrganizationConfigurationProps {
|
interface OrganizationConfigurationProps {
|
||||||
strategy: MirrorStrategy;
|
strategy: MirrorStrategy;
|
||||||
destinationOrg?: string;
|
destinationOrg?: string;
|
||||||
starredReposOrg?: string;
|
starredReposOrg?: string;
|
||||||
|
starredReposMode?: StarredReposMode;
|
||||||
personalReposOrg?: string;
|
personalReposOrg?: string;
|
||||||
visibility: GiteaOrgVisibility;
|
visibility: GiteaOrgVisibility;
|
||||||
onDestinationOrgChange: (org: string) => void;
|
onDestinationOrgChange: (org: string) => void;
|
||||||
onStarredReposOrgChange: (org: string) => void;
|
onStarredReposOrgChange: (org: string) => void;
|
||||||
|
onStarredReposModeChange: (mode: StarredReposMode) => void;
|
||||||
onPersonalReposOrgChange: (org: string) => void;
|
onPersonalReposOrgChange: (org: string) => void;
|
||||||
onVisibilityChange: (visibility: GiteaOrgVisibility) => void;
|
onVisibilityChange: (visibility: GiteaOrgVisibility) => void;
|
||||||
}
|
}
|
||||||
@@ -33,13 +35,19 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
|||||||
strategy,
|
strategy,
|
||||||
destinationOrg,
|
destinationOrg,
|
||||||
starredReposOrg,
|
starredReposOrg,
|
||||||
|
starredReposMode,
|
||||||
personalReposOrg,
|
personalReposOrg,
|
||||||
visibility,
|
visibility,
|
||||||
onDestinationOrgChange,
|
onDestinationOrgChange,
|
||||||
onStarredReposOrgChange,
|
onStarredReposOrgChange,
|
||||||
|
onStarredReposModeChange,
|
||||||
onPersonalReposOrgChange,
|
onPersonalReposOrgChange,
|
||||||
onVisibilityChange,
|
onVisibilityChange,
|
||||||
}) => {
|
}) => {
|
||||||
|
const activeStarredMode = starredReposMode || "dedicated-org";
|
||||||
|
const showStarredReposOrgInput = activeStarredMode === "dedicated-org";
|
||||||
|
const showDestinationOrgInput = strategy === "single-org" || strategy === "mixed";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -49,9 +57,52 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
|||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* First row - Organization inputs with consistent layout */}
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-normal flex items-center gap-2">
|
||||||
|
Starred Repository Destination
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Choose whether starred repos use one org or keep their source owner paths</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</Label>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onStarredReposModeChange("dedicated-org")}
|
||||||
|
className={cn(
|
||||||
|
"text-left px-3 py-2 rounded-md border text-sm transition-colors",
|
||||||
|
activeStarredMode === "dedicated-org"
|
||||||
|
? "bg-accent border-accent-foreground/20"
|
||||||
|
: "bg-background hover:bg-accent/50 border-input"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Dedicated organization
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onStarredReposModeChange("preserve-owner")}
|
||||||
|
className={cn(
|
||||||
|
"text-left px-3 py-2 rounded-md border text-sm transition-colors",
|
||||||
|
activeStarredMode === "preserve-owner"
|
||||||
|
? "bg-accent border-accent-foreground/20"
|
||||||
|
: "bg-background hover:bg-accent/50 border-input"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Preserve source owner/org
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* First row - Organization inputs */}
|
||||||
|
{(showStarredReposOrgInput || showDestinationOrgInput) && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Left column - always shows starred repos org */}
|
{showStarredReposOrgInput ? (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="starredReposOrg" className="text-sm font-normal flex items-center gap-2">
|
<Label htmlFor="starredReposOrg" className="text-sm font-normal flex items-center gap-2">
|
||||||
<Star className="h-3.5 w-3.5" />
|
<Star className="h-3.5 w-3.5" />
|
||||||
@@ -78,9 +129,11 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
|||||||
Keep starred repos organized separately
|
Keep starred repos organized separately
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="hidden md:block" />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Right column - shows destination org for single-org/mixed, personal repos org for preserve, empty div for others */}
|
{showDestinationOrgInput ? (
|
||||||
{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">
|
||||||
{strategy === "mixed" ? "Personal Repos Organization" : "Destination Organization"}
|
{strategy === "mixed" ? "Personal Repos Organization" : "Destination Organization"}
|
||||||
@@ -118,6 +171,7 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
|||||||
<div className="hidden md:block" />
|
<div className="hidden md:block" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Second row - Organization Visibility (always shown) */}
|
{/* Second row - Organization Visibility (always shown) */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -172,4 +226,3 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from "@/components/ui/hover-card";
|
} from "@/components/ui/hover-card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { StarredReposMode } from "@/types/config";
|
||||||
|
|
||||||
export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed";
|
export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed";
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ interface OrganizationStrategyProps {
|
|||||||
strategy: MirrorStrategy;
|
strategy: MirrorStrategy;
|
||||||
destinationOrg?: string;
|
destinationOrg?: string;
|
||||||
starredReposOrg?: string;
|
starredReposOrg?: string;
|
||||||
|
starredReposMode?: StarredReposMode;
|
||||||
onStrategyChange: (strategy: MirrorStrategy) => void;
|
onStrategyChange: (strategy: MirrorStrategy) => void;
|
||||||
githubUsername?: string;
|
githubUsername?: string;
|
||||||
giteaUsername?: string;
|
giteaUsername?: string;
|
||||||
@@ -76,13 +78,18 @@ const MappingPreview: React.FC<{
|
|||||||
config: typeof strategyConfig.preserve;
|
config: typeof strategyConfig.preserve;
|
||||||
destinationOrg?: string;
|
destinationOrg?: string;
|
||||||
starredReposOrg?: string;
|
starredReposOrg?: string;
|
||||||
|
starredReposMode?: StarredReposMode;
|
||||||
githubUsername?: string;
|
githubUsername?: string;
|
||||||
giteaUsername?: string;
|
giteaUsername?: string;
|
||||||
}> = ({ strategy, config, destinationOrg, starredReposOrg, githubUsername, giteaUsername }) => {
|
}> = ({ strategy, config, destinationOrg, starredReposOrg, starredReposMode, githubUsername, giteaUsername }) => {
|
||||||
const displayGithubUsername = githubUsername || "<username>";
|
const displayGithubUsername = githubUsername || "<username>";
|
||||||
const displayGiteaUsername = giteaUsername || "<username>";
|
const displayGiteaUsername = giteaUsername || "<username>";
|
||||||
const isGithubPlaceholder = !githubUsername;
|
const isGithubPlaceholder = !githubUsername;
|
||||||
const isGiteaPlaceholder = !giteaUsername;
|
const isGiteaPlaceholder = !giteaUsername;
|
||||||
|
const starredDestination =
|
||||||
|
(starredReposMode || "dedicated-org") === "preserve-owner"
|
||||||
|
? "awesome/starred-repo"
|
||||||
|
: `${starredReposOrg || "starred"}/starred-repo`;
|
||||||
|
|
||||||
if (strategy === "preserve") {
|
if (strategy === "preserve") {
|
||||||
return (
|
return (
|
||||||
@@ -122,7 +129,7 @@ const MappingPreview: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
<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)} />
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
<span>{starredDestination}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,7 +175,7 @@ const MappingPreview: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
<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)} />
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
<span>{starredDestination}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,7 +221,7 @@ const MappingPreview: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
<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)} />
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
<span>{starredDestination}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -260,7 +267,7 @@ const MappingPreview: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
<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)} />
|
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||||
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
<span>{starredDestination}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,6 +282,7 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
|||||||
strategy,
|
strategy,
|
||||||
destinationOrg,
|
destinationOrg,
|
||||||
starredReposOrg,
|
starredReposOrg,
|
||||||
|
starredReposMode,
|
||||||
onStrategyChange,
|
onStrategyChange,
|
||||||
githubUsername,
|
githubUsername,
|
||||||
giteaUsername,
|
giteaUsername,
|
||||||
@@ -339,7 +347,7 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
|||||||
<span className="text-xs font-medium">Starred Repositories</span>
|
<span className="text-xs font-medium">Starred Repositories</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground pl-5">
|
<p className="text-xs text-muted-foreground pl-5">
|
||||||
Always go to the configured starred repos organization and cannot be overridden.
|
Follow your starred-repo mode and cannot be overridden per repository.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,6 +423,7 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
|||||||
config={config}
|
config={config}
|
||||||
destinationOrg={destinationOrg}
|
destinationOrg={destinationOrg}
|
||||||
starredReposOrg={starredReposOrg}
|
starredReposOrg={starredReposOrg}
|
||||||
|
starredReposMode={starredReposMode}
|
||||||
githubUsername={githubUsername}
|
githubUsername={githubUsername}
|
||||||
giteaUsername={giteaUsername}
|
giteaUsername={giteaUsername}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,10 +28,17 @@ export function InlineDestinationEditor({
|
|||||||
|
|
||||||
// Determine the default destination based on repository properties and config
|
// Determine the default destination based on repository properties and config
|
||||||
const getDefaultDestination = () => {
|
const getDefaultDestination = () => {
|
||||||
// Starred repos always go to the configured starredReposOrg
|
// Starred repos can use either dedicated org or preserved source owner
|
||||||
if (repository.isStarred && giteaConfig?.starredReposOrg) {
|
if (repository.isStarred) {
|
||||||
|
const starredReposMode = giteaConfig?.starredReposMode || "dedicated-org";
|
||||||
|
if (starredReposMode === "preserve-owner") {
|
||||||
|
return repository.organization || repository.owner;
|
||||||
|
}
|
||||||
|
if (giteaConfig?.starredReposOrg) {
|
||||||
return giteaConfig.starredReposOrg;
|
return giteaConfig.starredReposOrg;
|
||||||
}
|
}
|
||||||
|
return "starred";
|
||||||
|
}
|
||||||
|
|
||||||
// Check mirror strategy
|
// Check mirror strategy
|
||||||
const strategy = giteaConfig?.mirrorStrategy || 'preserve';
|
const strategy = giteaConfig?.mirrorStrategy || 'preserve';
|
||||||
@@ -60,7 +67,7 @@ export function InlineDestinationEditor({
|
|||||||
const defaultDestination = getDefaultDestination();
|
const defaultDestination = getDefaultDestination();
|
||||||
const currentDestination = repository.destinationOrg || defaultDestination;
|
const currentDestination = repository.destinationOrg || defaultDestination;
|
||||||
const hasOverride = repository.destinationOrg && repository.destinationOrg !== defaultDestination;
|
const hasOverride = repository.destinationOrg && repository.destinationOrg !== defaultDestination;
|
||||||
const isStarredRepo = repository.isStarred && giteaConfig?.starredReposOrg;
|
const isStarredRepo = repository.isStarred;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditing && inputRef.current) {
|
if (isEditing && inputRef.current) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const githubConfigSchema = z.object({
|
|||||||
includePublic: z.boolean().default(true),
|
includePublic: z.boolean().default(true),
|
||||||
includeOrganizations: z.array(z.string()).default([]),
|
includeOrganizations: z.array(z.string()).default([]),
|
||||||
starredReposOrg: z.string().optional(),
|
starredReposOrg: z.string().optional(),
|
||||||
|
starredReposMode: z.enum(["dedicated-org", "preserve-owner"]).default("dedicated-org"),
|
||||||
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).default("preserve"),
|
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).default("preserve"),
|
||||||
defaultOrg: z.string().optional(),
|
defaultOrg: z.string().optional(),
|
||||||
starredCodeOnly: z.boolean().default(false),
|
starredCodeOnly: z.boolean().default(false),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface EnvConfig {
|
|||||||
onlyMirrorOrgs?: boolean;
|
onlyMirrorOrgs?: boolean;
|
||||||
starredCodeOnly?: boolean;
|
starredCodeOnly?: boolean;
|
||||||
starredReposOrg?: string;
|
starredReposOrg?: string;
|
||||||
|
starredReposMode?: 'dedicated-org' | 'preserve-owner';
|
||||||
mirrorStrategy?: 'preserve' | 'single-org' | 'flat-user' | 'mixed';
|
mirrorStrategy?: 'preserve' | 'single-org' | 'flat-user' | 'mixed';
|
||||||
};
|
};
|
||||||
gitea: {
|
gitea: {
|
||||||
@@ -112,6 +113,7 @@ function parseEnvConfig(): EnvConfig {
|
|||||||
onlyMirrorOrgs: process.env.ONLY_MIRROR_ORGS === 'true',
|
onlyMirrorOrgs: process.env.ONLY_MIRROR_ORGS === 'true',
|
||||||
starredCodeOnly: process.env.SKIP_STARRED_ISSUES === 'true',
|
starredCodeOnly: process.env.SKIP_STARRED_ISSUES === 'true',
|
||||||
starredReposOrg: process.env.STARRED_REPOS_ORG,
|
starredReposOrg: process.env.STARRED_REPOS_ORG,
|
||||||
|
starredReposMode: process.env.STARRED_REPOS_MODE as 'dedicated-org' | 'preserve-owner',
|
||||||
mirrorStrategy: process.env.MIRROR_STRATEGY as 'preserve' | 'single-org' | 'flat-user' | 'mixed',
|
mirrorStrategy: process.env.MIRROR_STRATEGY as 'preserve' | 'single-org' | 'flat-user' | 'mixed',
|
||||||
},
|
},
|
||||||
gitea: {
|
gitea: {
|
||||||
@@ -256,6 +258,7 @@ export async function initializeConfigFromEnv(): Promise<void> {
|
|||||||
includePublic: envConfig.github.publicRepositories ?? existingConfig?.[0]?.githubConfig?.includePublic ?? true,
|
includePublic: envConfig.github.publicRepositories ?? existingConfig?.[0]?.githubConfig?.includePublic ?? true,
|
||||||
includeOrganizations: envConfig.github.mirrorOrganizations ? [] : (existingConfig?.[0]?.githubConfig?.includeOrganizations ?? []),
|
includeOrganizations: envConfig.github.mirrorOrganizations ? [] : (existingConfig?.[0]?.githubConfig?.includeOrganizations ?? []),
|
||||||
starredReposOrg: envConfig.github.starredReposOrg || existingConfig?.[0]?.githubConfig?.starredReposOrg || 'starred',
|
starredReposOrg: envConfig.github.starredReposOrg || existingConfig?.[0]?.githubConfig?.starredReposOrg || 'starred',
|
||||||
|
starredReposMode: envConfig.github.starredReposMode || existingConfig?.[0]?.githubConfig?.starredReposMode || 'dedicated-org',
|
||||||
mirrorStrategy,
|
mirrorStrategy,
|
||||||
defaultOrg: envConfig.gitea.organization || existingConfig?.[0]?.githubConfig?.defaultOrg || 'github-mirrors',
|
defaultOrg: envConfig.gitea.organization || existingConfig?.[0]?.githubConfig?.defaultOrg || 'github-mirrors',
|
||||||
starredCodeOnly: envConfig.github.starredCodeOnly ?? existingConfig?.[0]?.githubConfig?.starredCodeOnly ?? false,
|
starredCodeOnly: envConfig.github.starredCodeOnly ?? existingConfig?.[0]?.githubConfig?.starredCodeOnly ?? false,
|
||||||
|
|||||||
@@ -342,6 +342,8 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
mirrorPublicOrgs: false,
|
mirrorPublicOrgs: false,
|
||||||
publicOrgs: [],
|
publicOrgs: [],
|
||||||
starredCodeOnly: false,
|
starredCodeOnly: false,
|
||||||
|
starredReposOrg: "starred",
|
||||||
|
starredReposMode: "dedicated-org",
|
||||||
mirrorStrategy: "preserve"
|
mirrorStrategy: "preserve"
|
||||||
},
|
},
|
||||||
giteaConfig: {
|
giteaConfig: {
|
||||||
@@ -350,7 +352,6 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
token: "gitea-token",
|
token: "gitea-token",
|
||||||
organization: "github-mirrors",
|
organization: "github-mirrors",
|
||||||
visibility: "public",
|
visibility: "public",
|
||||||
starredReposOrg: "starred",
|
|
||||||
preserveVisibility: false
|
preserveVisibility: false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -390,8 +391,8 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
const repo = { ...baseRepo, isStarred: true };
|
const repo = { ...baseRepo, isStarred: true };
|
||||||
const configWithoutStarredOrg = {
|
const configWithoutStarredOrg = {
|
||||||
...baseConfig,
|
...baseConfig,
|
||||||
giteaConfig: {
|
githubConfig: {
|
||||||
...baseConfig.giteaConfig,
|
...baseConfig.githubConfig,
|
||||||
starredReposOrg: undefined
|
starredReposOrg: undefined
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -399,6 +400,34 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
expect(result).toBe("starred");
|
expect(result).toBe("starred");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("starred repos preserve owner/org when starredReposMode is preserve-owner", () => {
|
||||||
|
const repo = { ...baseRepo, isStarred: true, owner: "FOO", organization: "FOO", fullName: "FOO/BAR" };
|
||||||
|
const configWithPreserveStarred = {
|
||||||
|
...baseConfig,
|
||||||
|
githubConfig: {
|
||||||
|
...baseConfig.githubConfig!,
|
||||||
|
starredReposMode: "preserve-owner" as const,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getGiteaRepoOwner({ config: configWithPreserveStarred, repository: repo });
|
||||||
|
expect(result).toBe("FOO");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("starred personal repos preserve owner when starredReposMode is preserve-owner", () => {
|
||||||
|
const repo = { ...baseRepo, isStarred: true, owner: "alice", organization: undefined, fullName: "alice/demo" };
|
||||||
|
const configWithPreserveStarred = {
|
||||||
|
...baseConfig,
|
||||||
|
githubConfig: {
|
||||||
|
...baseConfig.githubConfig!,
|
||||||
|
starredReposMode: "preserve-owner" as const,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getGiteaRepoOwner({ config: configWithPreserveStarred, repository: repo });
|
||||||
|
expect(result).toBe("alice");
|
||||||
|
});
|
||||||
|
|
||||||
// Removed test for personalReposOrg as this field no longer exists
|
// Removed test for personalReposOrg as this field no longer exists
|
||||||
|
|
||||||
test("preserve strategy: personal repos fallback to username when no override", () => {
|
test("preserve strategy: personal repos fallback to username when no override", () => {
|
||||||
@@ -492,4 +521,24 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
|
|
||||||
expect(result).toBe("custom-org");
|
expect(result).toBe("custom-org");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("getGiteaRepoOwnerAsync preserves starred owner when preserve-owner mode is enabled", async () => {
|
||||||
|
const configWithUser: Partial<Config> = {
|
||||||
|
...baseConfig,
|
||||||
|
userId: "user-id",
|
||||||
|
githubConfig: {
|
||||||
|
...baseConfig.githubConfig!,
|
||||||
|
starredReposMode: "preserve-owner",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const repo = { ...baseRepo, isStarred: true, owner: "FOO", organization: "FOO", fullName: "FOO/BAR" };
|
||||||
|
|
||||||
|
const result = await getGiteaRepoOwnerAsync({
|
||||||
|
config: configWithUser,
|
||||||
|
repository: repo,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toBe("FOO");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -77,8 +77,12 @@ export const getGiteaRepoOwnerAsync = async ({
|
|||||||
throw new Error("User ID is required for organization overrides.");
|
throw new Error("User ID is required for organization overrides.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if repository is starred - starred repos always go to starredReposOrg (highest priority)
|
// Check if repository is starred
|
||||||
if (repository.isStarred) {
|
if (repository.isStarred) {
|
||||||
|
const starredReposMode = config.githubConfig.starredReposMode || "dedicated-org";
|
||||||
|
if (starredReposMode === "preserve-owner") {
|
||||||
|
return repository.organization || repository.owner;
|
||||||
|
}
|
||||||
return config.githubConfig.starredReposOrg || "starred";
|
return config.githubConfig.starredReposOrg || "starred";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,8 +126,12 @@ export const getGiteaRepoOwner = ({
|
|||||||
throw new Error("Gitea username is required.");
|
throw new Error("Gitea username is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if repository is starred - starred repos always go to starredReposOrg
|
// Check if repository is starred
|
||||||
if (repository.isStarred) {
|
if (repository.isStarred) {
|
||||||
|
const starredReposMode = config.githubConfig.starredReposMode || "dedicated-org";
|
||||||
|
if (starredReposMode === "preserve-owner") {
|
||||||
|
return repository.organization || repository.owner;
|
||||||
|
}
|
||||||
return config.githubConfig.starredReposOrg || "starred";
|
return config.githubConfig.starredReposOrg || "starred";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +380,11 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
// Determine the actual repository name to use (handle duplicates for starred repos)
|
// Determine the actual repository name to use (handle duplicates for starred repos)
|
||||||
let targetRepoName = repository.name;
|
let targetRepoName = repository.name;
|
||||||
|
|
||||||
if (repository.isStarred && config.githubConfig) {
|
if (
|
||||||
|
repository.isStarred &&
|
||||||
|
config.githubConfig &&
|
||||||
|
(config.githubConfig.starredReposMode || "dedicated-org") === "dedicated-org"
|
||||||
|
) {
|
||||||
// Extract GitHub owner from full_name (format: owner/repo)
|
// Extract GitHub owner from full_name (format: owner/repo)
|
||||||
const githubOwner = repository.fullName.split('/')[0];
|
const githubOwner = repository.fullName.split('/')[0];
|
||||||
|
|
||||||
@@ -990,7 +1002,11 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
|||||||
// Determine the actual repository name to use (handle duplicates for starred repos)
|
// Determine the actual repository name to use (handle duplicates for starred repos)
|
||||||
let targetRepoName = repository.name;
|
let targetRepoName = repository.name;
|
||||||
|
|
||||||
if (repository.isStarred && config.githubConfig) {
|
if (
|
||||||
|
repository.isStarred &&
|
||||||
|
config.githubConfig &&
|
||||||
|
(config.githubConfig.starredReposMode || "dedicated-org") === "dedicated-org"
|
||||||
|
) {
|
||||||
// Extract GitHub owner from full_name (format: owner/repo)
|
// Extract GitHub owner from full_name (format: owner/repo)
|
||||||
const githubOwner = repository.fullName.split('/')[0];
|
const githubOwner = repository.fullName.split('/')[0];
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,13 @@ async function preCreateOrganizations({
|
|||||||
// Get unique organization names
|
// Get unique organization names
|
||||||
const orgNames = new Set<string>();
|
const orgNames = new Set<string>();
|
||||||
|
|
||||||
// Add starred repos org
|
const starredReposMode = config.githubConfig?.starredReposMode || "dedicated-org";
|
||||||
if (config.githubConfig?.starredReposOrg) {
|
|
||||||
|
if (starredReposMode === "preserve-owner") {
|
||||||
|
for (const repo of repositories) {
|
||||||
|
orgNames.add(repo.organization || repo.owner);
|
||||||
|
}
|
||||||
|
} else if (config.githubConfig?.starredReposOrg) {
|
||||||
orgNames.add(config.githubConfig.starredReposOrg);
|
orgNames.add(config.githubConfig.starredReposOrg);
|
||||||
} else {
|
} else {
|
||||||
orgNames.add("starred");
|
orgNames.add("starred");
|
||||||
@@ -129,7 +134,11 @@ async function processStarredRepository({
|
|||||||
octokit: Octokit;
|
octokit: Octokit;
|
||||||
strategyConfig: ReturnType<typeof getMirrorStrategyConfig>;
|
strategyConfig: ReturnType<typeof getMirrorStrategyConfig>;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const starredOrg = config.githubConfig?.starredReposOrg || "starred";
|
const starredReposMode = config.githubConfig?.starredReposMode || "dedicated-org";
|
||||||
|
const starredOrg =
|
||||||
|
starredReposMode === "preserve-owner"
|
||||||
|
? repository.organization || repository.owner
|
||||||
|
: config.githubConfig?.starredReposOrg || "starred";
|
||||||
|
|
||||||
// Check if repository exists in Gitea
|
// Check if repository exists in Gitea
|
||||||
const existingRepo = await getGiteaRepoInfo({
|
const existingRepo = await getGiteaRepoInfo({
|
||||||
@@ -257,7 +266,11 @@ export async function syncStarredRepositories({
|
|||||||
if (error instanceof Error && error.message.includes("not a mirror")) {
|
if (error instanceof Error && error.message.includes("not a mirror")) {
|
||||||
console.warn(`Repository ${repository.name} is not a mirror, handling...`);
|
console.warn(`Repository ${repository.name} is not a mirror, handling...`);
|
||||||
|
|
||||||
const starredOrg = config.githubConfig?.starredReposOrg || "starred";
|
const starredReposMode = config.githubConfig?.starredReposMode || "dedicated-org";
|
||||||
|
const starredOrg =
|
||||||
|
starredReposMode === "preserve-owner"
|
||||||
|
? repository.organization || repository.owner
|
||||||
|
: config.githubConfig?.starredReposOrg || "starred";
|
||||||
const repoInfo = await getGiteaRepoInfo({
|
const repoInfo = await getGiteaRepoInfo({
|
||||||
config,
|
config,
|
||||||
owner: starredOrg,
|
owner: starredOrg,
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export async function createDefaultConfig({ userId, envOverrides = {} }: Default
|
|||||||
includePublic: true,
|
includePublic: true,
|
||||||
includeOrganizations: [],
|
includeOrganizations: [],
|
||||||
starredReposOrg: "starred",
|
starredReposOrg: "starred",
|
||||||
|
starredReposMode: "dedicated-org",
|
||||||
mirrorStrategy: "preserve",
|
mirrorStrategy: "preserve",
|
||||||
defaultOrg: "github-mirrors",
|
defaultOrg: "github-mirrors",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export function mapUiToDbConfig(
|
|||||||
|
|
||||||
// Starred repos organization
|
// Starred repos organization
|
||||||
starredReposOrg: giteaConfig.starredReposOrg,
|
starredReposOrg: giteaConfig.starredReposOrg,
|
||||||
|
starredReposMode: giteaConfig.starredReposMode || "dedicated-org",
|
||||||
|
|
||||||
// Mirror strategy
|
// Mirror strategy
|
||||||
mirrorStrategy: giteaConfig.mirrorStrategy || "preserve",
|
mirrorStrategy: giteaConfig.mirrorStrategy || "preserve",
|
||||||
@@ -131,6 +132,7 @@ export function mapDbToUiConfig(dbConfig: any): {
|
|||||||
organization: dbConfig.githubConfig?.defaultOrg || "github-mirrors", // Get from GitHub config
|
organization: dbConfig.githubConfig?.defaultOrg || "github-mirrors", // Get from GitHub config
|
||||||
visibility: dbConfig.giteaConfig?.visibility === "default" ? "public" : dbConfig.giteaConfig?.visibility || "public",
|
visibility: dbConfig.giteaConfig?.visibility === "default" ? "public" : dbConfig.giteaConfig?.visibility || "public",
|
||||||
starredReposOrg: dbConfig.githubConfig?.starredReposOrg || "starred", // Get from GitHub config
|
starredReposOrg: dbConfig.githubConfig?.starredReposOrg || "starred", // Get from GitHub config
|
||||||
|
starredReposMode: dbConfig.githubConfig?.starredReposMode || "dedicated-org", // Get from GitHub config
|
||||||
preserveOrgStructure: dbConfig.giteaConfig?.preserveVisibility || false, // Map preserveVisibility
|
preserveOrgStructure: dbConfig.giteaConfig?.preserveVisibility || false, // Map preserveVisibility
|
||||||
mirrorStrategy: dbConfig.githubConfig?.mirrorStrategy || "preserve", // Get from GitHub config
|
mirrorStrategy: dbConfig.githubConfig?.mirrorStrategy || "preserve", // Get from GitHub config
|
||||||
personalReposOrg: undefined, // Not stored in current schema
|
personalReposOrg: undefined, // Not stored in current schema
|
||||||
|
|||||||
@@ -108,15 +108,14 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
console.log(`Repository ${repo.name} will be mirrored to owner: ${owner}`);
|
console.log(`Repository ${repo.name} will be mirrored to owner: ${owner}`);
|
||||||
|
|
||||||
// For single-org and starred repos strategies, or when mirroring to an org,
|
// For single-org strategy, or when mirroring to an org,
|
||||||
// always use the org mirroring function to ensure proper organization handling
|
// use the org mirroring function to ensure proper organization handling
|
||||||
const mirrorStrategy = config.githubConfig?.mirrorStrategy ||
|
const mirrorStrategy = config.githubConfig?.mirrorStrategy ||
|
||||||
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
(config.giteaConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
||||||
|
|
||||||
const shouldUseOrgMirror =
|
const shouldUseOrgMirror =
|
||||||
owner !== config.giteaConfig?.defaultOwner || // Different owner means org
|
owner !== config.giteaConfig?.defaultOwner || // Different owner means org
|
||||||
mirrorStrategy === "single-org" || // Single-org strategy always uses org
|
mirrorStrategy === "single-org"; // Single-org strategy always uses org
|
||||||
repoData.isStarred; // Starred repos always go to org
|
|
||||||
|
|
||||||
if (shouldUseOrgMirror) {
|
if (shouldUseOrgMirror) {
|
||||||
await mirrorGitHubOrgRepoToGiteaOrg({
|
await mirrorGitHubOrgRepoToGiteaOrg({
|
||||||
|
|||||||
@@ -142,15 +142,14 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
console.log(`Importing repo: ${repo.name} to owner: ${owner}`);
|
console.log(`Importing repo: ${repo.name} to owner: ${owner}`);
|
||||||
|
|
||||||
// For single-org and starred repos strategies, or when mirroring to an org,
|
// For single-org strategy, or when mirroring to an org,
|
||||||
// always use the org mirroring function to ensure proper organization handling
|
// use the org mirroring function to ensure proper organization handling
|
||||||
const mirrorStrategy = config.githubConfig?.mirrorStrategy ||
|
const mirrorStrategy = config.githubConfig?.mirrorStrategy ||
|
||||||
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
||||||
|
|
||||||
const shouldUseOrgMirror =
|
const shouldUseOrgMirror =
|
||||||
owner !== config.giteaConfig?.defaultOwner || // Different owner means org
|
owner !== config.giteaConfig?.defaultOwner || // Different owner means org
|
||||||
mirrorStrategy === "single-org" || // Single-org strategy always uses org
|
mirrorStrategy === "single-org"; // Single-org strategy always uses org
|
||||||
repoData.isStarred; // Starred repos always go to org
|
|
||||||
|
|
||||||
if (shouldUseOrgMirror) {
|
if (shouldUseOrgMirror) {
|
||||||
await mirrorGitHubOrgRepoToGiteaOrg({
|
await mirrorGitHubOrgRepoToGiteaOrg({
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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" | "mixed";
|
export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed";
|
||||||
|
export type StarredReposMode = "dedicated-org" | "preserve-owner";
|
||||||
|
|
||||||
export interface GiteaConfig {
|
export interface GiteaConfig {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -10,6 +11,7 @@ export interface GiteaConfig {
|
|||||||
organization: string;
|
organization: string;
|
||||||
visibility: GiteaOrgVisibility;
|
visibility: GiteaOrgVisibility;
|
||||||
starredReposOrg: string;
|
starredReposOrg: string;
|
||||||
|
starredReposMode?: StarredReposMode;
|
||||||
preserveOrgStructure: boolean;
|
preserveOrgStructure: boolean;
|
||||||
mirrorStrategy?: MirrorStrategy; // New field for the strategy
|
mirrorStrategy?: MirrorStrategy; // New field for the strategy
|
||||||
personalReposOrg?: string; // Override destination for personal repos
|
personalReposOrg?: string; // Override destination for personal repos
|
||||||
@@ -46,6 +48,7 @@ export interface GitHubConfig {
|
|||||||
privateRepositories: boolean;
|
privateRepositories: boolean;
|
||||||
mirrorStarred: boolean;
|
mirrorStarred: boolean;
|
||||||
starredDuplicateStrategy?: DuplicateNameStrategy;
|
starredDuplicateStrategy?: DuplicateNameStrategy;
|
||||||
|
starredReposMode?: StarredReposMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MirrorOptions {
|
export interface MirrorOptions {
|
||||||
|
|||||||
Reference in New Issue
Block a user