Allow starred repos to be mirrored preserving structure

This commit is contained in:
Tobeas Arren
2026-02-14 13:08:41 +01:00
parent 2496d6f6e0
commit f4d391b240
18 changed files with 237 additions and 72 deletions

View File

@@ -47,6 +47,7 @@ DOCKER_TAG=latest
# SKIP_FORKS=false
# MIRROR_STARRED=false
# STARRED_REPOS_ORG=starred # Organization name for starred repos
# STARRED_REPOS_MODE=dedicated-org # dedicated-org | preserve-owner
# Organization Settings
# MIRROR_ORGANIZATIONS=false
@@ -183,4 +184,4 @@ DOCKER_TAG=latest
# ===========================================
# TLS/SSL Configuration
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing

View File

@@ -209,7 +209,7 @@ bun run dev
3. **Customization**
- Click edit buttons on organization cards to set custom destinations
- 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

View File

@@ -62,6 +62,7 @@ Settings for connecting to and configuring GitHub repository sources.
| `SKIP_FORKS` | Skip forked 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_MODE` | How starred repos are mirrored | `dedicated-org` | `dedicated-org`, `preserve-owner` |
### Organization Settings

View File

@@ -47,6 +47,7 @@ export function ConfigTabs() {
organization: 'github-mirrors',
visibility: 'public',
starredReposOrg: 'starred',
starredReposMode: 'dedicated-org',
preserveOrgStructure: false,
},
scheduleConfig: {

View File

@@ -224,6 +224,7 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
strategy={mirrorStrategy}
destinationOrg={config.organization}
starredReposOrg={config.starredReposOrg}
starredReposMode={config.starredReposMode}
onStrategyChange={setMirrorStrategy}
githubUsername={githubUsername}
giteaUsername={config.username}
@@ -235,6 +236,7 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
strategy={mirrorStrategy}
destinationOrg={config.organization}
starredReposOrg={config.starredReposOrg}
starredReposMode={config.starredReposMode}
personalReposOrg={config.personalReposOrg}
visibility={config.visibility}
onDestinationOrgChange={(org) => {
@@ -247,6 +249,11 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
setConfig(newConfig);
if (onAutoSave) onAutoSave(newConfig);
}}
onStarredReposModeChange={(mode) => {
const newConfig = { ...config, starredReposMode: mode };
setConfig(newConfig);
if (onAutoSave) onAutoSave(newConfig);
}}
onPersonalReposOrgChange={(org) => {
const newConfig = { ...config, personalReposOrg: org };
setConfig(newConfig);

View File

@@ -9,16 +9,18 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import type { MirrorStrategy, GiteaOrgVisibility } from "@/types/config";
import type { MirrorStrategy, GiteaOrgVisibility, StarredReposMode } from "@/types/config";
interface OrganizationConfigurationProps {
strategy: MirrorStrategy;
destinationOrg?: string;
starredReposOrg?: string;
starredReposMode?: StarredReposMode;
personalReposOrg?: string;
visibility: GiteaOrgVisibility;
onDestinationOrgChange: (org: string) => void;
onStarredReposOrgChange: (org: string) => void;
onStarredReposModeChange: (mode: StarredReposMode) => void;
onPersonalReposOrgChange: (org: string) => void;
onVisibilityChange: (visibility: GiteaOrgVisibility) => void;
}
@@ -33,13 +35,19 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
strategy,
destinationOrg,
starredReposOrg,
starredReposMode,
personalReposOrg,
visibility,
onDestinationOrgChange,
onStarredReposOrgChange,
onStarredReposModeChange,
onPersonalReposOrgChange,
onVisibilityChange,
}) => {
const activeStarredMode = starredReposMode || "dedicated-org";
const showStarredReposOrgInput = activeStarredMode === "dedicated-org";
const showDestinationOrgInput = strategy === "single-org" || strategy === "mixed";
return (
<div className="space-y-4">
<div>
@@ -49,38 +57,83 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
</h4>
</div>
{/* First row - Organization inputs with consistent layout */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Left column - always shows starred repos org */}
<div className="space-y-1">
<Label htmlFor="starredReposOrg" className="text-sm font-normal flex items-center gap-2">
<Star className="h-3.5 w-3.5" />
Starred Repos Organization
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Info className="h-3.5 w-3.5 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Starred repositories will be organized separately in this organization</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Input
id="starredReposOrg"
value={starredReposOrg || ""}
onChange={(e) => onStarredReposOrgChange(e.target.value)}
placeholder="starred"
className=""
/>
<p className="text-xs text-muted-foreground mt-1">
Keep starred repos organized separately
</p>
<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>
{/* Right column - shows destination org for single-org/mixed, personal repos org for preserve, empty div for others */}
{strategy === "single-org" || strategy === "mixed" ? (
{/* First row - Organization inputs */}
{(showStarredReposOrgInput || showDestinationOrgInput) && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{showStarredReposOrgInput ? (
<div className="space-y-1">
<Label htmlFor="starredReposOrg" className="text-sm font-normal flex items-center gap-2">
<Star className="h-3.5 w-3.5" />
Starred Repos Organization
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Info className="h-3.5 w-3.5 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>Starred repositories will be organized separately in this organization</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Input
id="starredReposOrg"
value={starredReposOrg || ""}
onChange={(e) => onStarredReposOrgChange(e.target.value)}
placeholder="starred"
className=""
/>
<p className="text-xs text-muted-foreground mt-1">
Keep starred repos organized separately
</p>
</div>
) : (
<div className="hidden md:block" />
)}
{showDestinationOrgInput ? (
<div className="space-y-1">
<Label htmlFor="destinationOrg" className="text-sm font-normal flex items-center gap-2">
{strategy === "mixed" ? "Personal Repos Organization" : "Destination Organization"}
@@ -114,10 +167,11 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
}
</p>
</div>
) : (
<div className="hidden md:block" />
)}
</div>
) : (
<div className="hidden md:block" />
)}
</div>
)}
{/* Second row - Organization Visibility (always shown) */}
<div className="space-y-2">
@@ -172,4 +226,3 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
</div>
);
};

View File

@@ -8,6 +8,7 @@ import {
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { cn } from "@/lib/utils";
import type { StarredReposMode } from "@/types/config";
export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed";
@@ -15,6 +16,7 @@ interface OrganizationStrategyProps {
strategy: MirrorStrategy;
destinationOrg?: string;
starredReposOrg?: string;
starredReposMode?: StarredReposMode;
onStrategyChange: (strategy: MirrorStrategy) => void;
githubUsername?: string;
giteaUsername?: string;
@@ -76,13 +78,18 @@ const MappingPreview: React.FC<{
config: typeof strategyConfig.preserve;
destinationOrg?: string;
starredReposOrg?: string;
starredReposMode?: StarredReposMode;
githubUsername?: string;
giteaUsername?: string;
}> = ({ strategy, config, destinationOrg, starredReposOrg, githubUsername, giteaUsername }) => {
}> = ({ strategy, config, destinationOrg, starredReposOrg, starredReposMode, githubUsername, giteaUsername }) => {
const displayGithubUsername = githubUsername || "<username>";
const displayGiteaUsername = giteaUsername || "<username>";
const isGithubPlaceholder = !githubUsername;
const isGiteaPlaceholder = !giteaUsername;
const starredDestination =
(starredReposMode || "dedicated-org") === "preserve-owner"
? "awesome/starred-repo"
: `${starredReposOrg || "starred"}/starred-repo`;
if (strategy === "preserve") {
return (
@@ -122,7 +129,7 @@ const MappingPreview: React.FC<{
</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>
<span>{starredDestination}</span>
</div>
</div>
</div>
@@ -168,7 +175,7 @@ const MappingPreview: React.FC<{
</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>
<span>{starredDestination}</span>
</div>
</div>
</div>
@@ -214,7 +221,7 @@ const MappingPreview: React.FC<{
</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>
<span>{starredDestination}</span>
</div>
</div>
</div>
@@ -260,7 +267,7 @@ const MappingPreview: React.FC<{
</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>
<span>{starredDestination}</span>
</div>
</div>
</div>
@@ -275,6 +282,7 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
strategy,
destinationOrg,
starredReposOrg,
starredReposMode,
onStrategyChange,
githubUsername,
giteaUsername,
@@ -339,7 +347,7 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
<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.
Follow your starred-repo mode and cannot be overridden per repository.
</p>
</div>
</div>
@@ -415,6 +423,7 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
config={config}
destinationOrg={destinationOrg}
starredReposOrg={starredReposOrg}
starredReposMode={starredReposMode}
githubUsername={githubUsername}
giteaUsername={giteaUsername}
/>
@@ -434,4 +443,4 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
</RadioGroup>
</div>
);
};
};

View File

@@ -28,9 +28,16 @@ export function InlineDestinationEditor({
// Determine the default destination based on repository properties and config
const getDefaultDestination = () => {
// Starred repos always go to the configured starredReposOrg
if (repository.isStarred && giteaConfig?.starredReposOrg) {
return giteaConfig.starredReposOrg;
// Starred repos can use either dedicated org or preserved source owner
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 "starred";
}
// Check mirror strategy
@@ -60,7 +67,7 @@ export function InlineDestinationEditor({
const defaultDestination = getDefaultDestination();
const currentDestination = repository.destinationOrg || defaultDestination;
const hasOverride = repository.destinationOrg && repository.destinationOrg !== defaultDestination;
const isStarredRepo = repository.isStarred && giteaConfig?.starredReposOrg;
const isStarredRepo = repository.isStarred;
useEffect(() => {
if (isEditing && inputRef.current) {
@@ -184,4 +191,4 @@ export function InlineDestinationEditor({
</div>
</div>
);
}
}

View File

@@ -25,6 +25,7 @@ export const githubConfigSchema = z.object({
includePublic: z.boolean().default(true),
includeOrganizations: z.array(z.string()).default([]),
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"),
defaultOrg: z.string().optional(),
starredCodeOnly: z.boolean().default(false),

View File

@@ -23,6 +23,7 @@ interface EnvConfig {
onlyMirrorOrgs?: boolean;
starredCodeOnly?: boolean;
starredReposOrg?: string;
starredReposMode?: 'dedicated-org' | 'preserve-owner';
mirrorStrategy?: 'preserve' | 'single-org' | 'flat-user' | 'mixed';
};
gitea: {
@@ -112,6 +113,7 @@ function parseEnvConfig(): EnvConfig {
onlyMirrorOrgs: process.env.ONLY_MIRROR_ORGS === 'true',
starredCodeOnly: process.env.SKIP_STARRED_ISSUES === 'true',
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',
},
gitea: {
@@ -256,6 +258,7 @@ export async function initializeConfigFromEnv(): Promise<void> {
includePublic: envConfig.github.publicRepositories ?? existingConfig?.[0]?.githubConfig?.includePublic ?? true,
includeOrganizations: envConfig.github.mirrorOrganizations ? [] : (existingConfig?.[0]?.githubConfig?.includeOrganizations ?? []),
starredReposOrg: envConfig.github.starredReposOrg || existingConfig?.[0]?.githubConfig?.starredReposOrg || 'starred',
starredReposMode: envConfig.github.starredReposMode || existingConfig?.[0]?.githubConfig?.starredReposMode || 'dedicated-org',
mirrorStrategy,
defaultOrg: envConfig.gitea.organization || existingConfig?.[0]?.githubConfig?.defaultOrg || 'github-mirrors',
starredCodeOnly: envConfig.github.starredCodeOnly ?? existingConfig?.[0]?.githubConfig?.starredCodeOnly ?? false,

View File

@@ -342,6 +342,8 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
mirrorPublicOrgs: false,
publicOrgs: [],
starredCodeOnly: false,
starredReposOrg: "starred",
starredReposMode: "dedicated-org",
mirrorStrategy: "preserve"
},
giteaConfig: {
@@ -350,7 +352,6 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
token: "gitea-token",
organization: "github-mirrors",
visibility: "public",
starredReposOrg: "starred",
preserveVisibility: false
}
};
@@ -390,8 +391,8 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
const repo = { ...baseRepo, isStarred: true };
const configWithoutStarredOrg = {
...baseConfig,
giteaConfig: {
...baseConfig.giteaConfig,
githubConfig: {
...baseConfig.githubConfig,
starredReposOrg: undefined
}
};
@@ -399,6 +400,34 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
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
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");
});
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");
});
});

View File

@@ -77,8 +77,12 @@ export const getGiteaRepoOwnerAsync = async ({
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) {
const starredReposMode = config.githubConfig.starredReposMode || "dedicated-org";
if (starredReposMode === "preserve-owner") {
return repository.organization || repository.owner;
}
return config.githubConfig.starredReposOrg || "starred";
}
@@ -122,8 +126,12 @@ export const getGiteaRepoOwner = ({
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) {
const starredReposMode = config.githubConfig.starredReposMode || "dedicated-org";
if (starredReposMode === "preserve-owner") {
return repository.organization || repository.owner;
}
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)
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)
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)
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)
const githubOwner = repository.fullName.split('/')[0];

View File

@@ -92,8 +92,13 @@ async function preCreateOrganizations({
// Get unique organization names
const orgNames = new Set<string>();
// Add starred repos org
if (config.githubConfig?.starredReposOrg) {
const starredReposMode = config.githubConfig?.starredReposMode || "dedicated-org";
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);
} else {
orgNames.add("starred");
@@ -129,7 +134,11 @@ async function processStarredRepository({
octokit: Octokit;
strategyConfig: ReturnType<typeof getMirrorStrategyConfig>;
}): 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
const existingRepo = await getGiteaRepoInfo({
@@ -257,7 +266,11 @@ export async function syncStarredRepositories({
if (error instanceof Error && error.message.includes("not a mirror")) {
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({
config,
owner: starredOrg,
@@ -287,4 +300,4 @@ export async function syncStarredRepositories({
},
}
);
}
}

View File

@@ -71,6 +71,7 @@ export async function createDefaultConfig({ userId, envOverrides = {} }: Default
includePublic: true,
includeOrganizations: [],
starredReposOrg: "starred",
starredReposMode: "dedicated-org",
mirrorStrategy: "preserve",
defaultOrg: "github-mirrors",
},

View File

@@ -48,6 +48,7 @@ export function mapUiToDbConfig(
// Starred repos organization
starredReposOrg: giteaConfig.starredReposOrg,
starredReposMode: giteaConfig.starredReposMode || "dedicated-org",
// Mirror strategy
mirrorStrategy: giteaConfig.mirrorStrategy || "preserve",
@@ -131,6 +132,7 @@ export function mapDbToUiConfig(dbConfig: any): {
organization: dbConfig.githubConfig?.defaultOrg || "github-mirrors", // Get from GitHub config
visibility: dbConfig.giteaConfig?.visibility === "default" ? "public" : dbConfig.giteaConfig?.visibility || "public",
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
mirrorStrategy: dbConfig.githubConfig?.mirrorStrategy || "preserve", // Get from GitHub config
personalReposOrg: undefined, // Not stored in current schema

View File

@@ -108,15 +108,14 @@ export const POST: APIRoute = async ({ request }) => {
console.log(`Repository ${repo.name} will be mirrored to owner: ${owner}`);
// For single-org and starred repos strategies, or when mirroring to an org,
// always use the org mirroring function to ensure proper organization handling
// For single-org strategy, or when mirroring to an org,
// use the org mirroring function to ensure proper organization handling
const mirrorStrategy = config.githubConfig?.mirrorStrategy ||
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
(config.giteaConfig?.preserveOrgStructure ? "preserve" : "flat-user");
const shouldUseOrgMirror =
owner !== config.giteaConfig?.defaultOwner || // Different owner means org
mirrorStrategy === "single-org" || // Single-org strategy always uses org
repoData.isStarred; // Starred repos always go to org
mirrorStrategy === "single-org"; // Single-org strategy always uses org
if (shouldUseOrgMirror) {
await mirrorGitHubOrgRepoToGiteaOrg({
@@ -222,4 +221,4 @@ export const POST: APIRoute = async ({ request }) => {
return createSecureErrorResponse(error, "mirror-repo API", 500);
}
};
};

View File

@@ -142,15 +142,14 @@ export const POST: APIRoute = async ({ request }) => {
console.log(`Importing repo: ${repo.name} to owner: ${owner}`);
// For single-org and starred repos strategies, or when mirroring to an org,
// always use the org mirroring function to ensure proper organization handling
// For single-org strategy, or when mirroring to an org,
// use the org mirroring function to ensure proper organization handling
const mirrorStrategy = config.githubConfig?.mirrorStrategy ||
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
const shouldUseOrgMirror =
owner !== config.giteaConfig?.defaultOwner || // Different owner means org
mirrorStrategy === "single-org" || // Single-org strategy always uses org
repoData.isStarred; // Starred repos always go to org
mirrorStrategy === "single-org"; // Single-org strategy always uses org
if (shouldUseOrgMirror) {
await mirrorGitHubOrgRepoToGiteaOrg({

View File

@@ -2,6 +2,7 @@ import { type Config as ConfigType } from "@/lib/db/schema";
export type GiteaOrgVisibility = "public" | "private" | "limited";
export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed";
export type StarredReposMode = "dedicated-org" | "preserve-owner";
export interface GiteaConfig {
url: string;
@@ -10,6 +11,7 @@ export interface GiteaConfig {
organization: string;
visibility: GiteaOrgVisibility;
starredReposOrg: string;
starredReposMode?: StarredReposMode;
preserveOrgStructure: boolean;
mirrorStrategy?: MirrorStrategy; // New field for the strategy
personalReposOrg?: string; // Override destination for personal repos
@@ -46,6 +48,7 @@ export interface GitHubConfig {
privateRepositories: boolean;
mirrorStarred: boolean;
starredDuplicateStrategy?: DuplicateNameStrategy;
starredReposMode?: StarredReposMode;
}
export interface MirrorOptions {