mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-08 12:36:44 +03:00
Added some basic options
This commit is contained in:
121
CLAUDE.md
Normal file
121
CLAUDE.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Gitea Mirror is a web application that automatically mirrors repositories from GitHub to self-hosted Gitea instances. It uses Astro for SSR, React for UI, SQLite for data storage, and Bun as the JavaScript runtime.
|
||||||
|
|
||||||
|
## Essential Commands
|
||||||
|
|
||||||
|
### Development
|
||||||
|
```bash
|
||||||
|
bun run dev # Start development server (port 3000)
|
||||||
|
bun run build # Build for production
|
||||||
|
bun run preview # Preview production build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
bun test # Run all tests
|
||||||
|
bun test:watch # Run tests in watch mode
|
||||||
|
bun test:coverage # Run tests with coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Management
|
||||||
|
```bash
|
||||||
|
bun run init-db # Initialize database
|
||||||
|
bun run reset-users # Reset user accounts (development)
|
||||||
|
bun run cleanup-db # Remove database files
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
```bash
|
||||||
|
bun run start # Start production server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture & Key Concepts
|
||||||
|
|
||||||
|
### Technology Stack
|
||||||
|
- **Frontend**: Astro (SSR) + React + Tailwind CSS v4 + Shadcn UI
|
||||||
|
- **Backend**: Bun runtime + SQLite + Drizzle ORM
|
||||||
|
- **APIs**: GitHub (Octokit) and Gitea APIs
|
||||||
|
- **Auth**: JWT tokens with bcryptjs password hashing
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
- `/src/pages/api/` - API endpoints (Astro API routes)
|
||||||
|
- `/src/components/` - React components organized by feature
|
||||||
|
- `/src/lib/db/` - Database queries and schema (Drizzle ORM)
|
||||||
|
- `/src/hooks/` - Custom React hooks for data fetching
|
||||||
|
- `/data/` - SQLite database storage location
|
||||||
|
|
||||||
|
### Key Architectural Patterns
|
||||||
|
|
||||||
|
1. **API Routes**: All API endpoints follow the pattern `/api/[resource]/[action]` and use `createSecureErrorResponse` for consistent error handling:
|
||||||
|
```typescript
|
||||||
|
import { createSecureErrorResponse } from '@/lib/utils/error-handler';
|
||||||
|
|
||||||
|
export async function POST({ request }: APIContext) {
|
||||||
|
try {
|
||||||
|
// Implementation
|
||||||
|
} catch (error) {
|
||||||
|
return createSecureErrorResponse(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Database Queries**: Located in `/src/lib/db/queries/` organized by domain (users, repositories, etc.)
|
||||||
|
|
||||||
|
3. **Real-time Updates**: Server-Sent Events (SSE) endpoint at `/api/events` for live dashboard updates
|
||||||
|
|
||||||
|
4. **Authentication Flow**:
|
||||||
|
- First user signup creates admin account
|
||||||
|
- JWT tokens stored in cookies
|
||||||
|
- Protected routes check auth via `getUserFromCookie()`
|
||||||
|
|
||||||
|
5. **Mirror Process**:
|
||||||
|
- Discovers repos from GitHub (user/org)
|
||||||
|
- Creates/updates mirror in Gitea
|
||||||
|
- Tracks status in database
|
||||||
|
- Supports scheduled automatic mirroring
|
||||||
|
|
||||||
|
### Database Schema (SQLite)
|
||||||
|
- `users` - User accounts and authentication
|
||||||
|
- `configs` - GitHub/Gitea connection settings
|
||||||
|
- `repositories` - Repository mirror status and metadata
|
||||||
|
- `organizations` - Organization structure preservation
|
||||||
|
- `mirror_jobs` - Scheduled mirror operations
|
||||||
|
- `events` - Activity log and notifications
|
||||||
|
|
||||||
|
### Testing Approach
|
||||||
|
- Uses Bun's native test runner (`bun:test`)
|
||||||
|
- Test files use `.test.ts` or `.test.tsx` extension
|
||||||
|
- Setup file at `/src/tests/setup.bun.ts`
|
||||||
|
- Mock utilities available for API testing
|
||||||
|
|
||||||
|
### Development Tips
|
||||||
|
- Environment variables in `.env` (copy from `.env.example`)
|
||||||
|
- JWT_SECRET auto-generated if not provided
|
||||||
|
- Database auto-initializes on first run
|
||||||
|
- Use `bun run dev:clean` for fresh database start
|
||||||
|
- Tailwind CSS v4 configured with Vite plugin
|
||||||
|
|
||||||
|
### Common Tasks
|
||||||
|
|
||||||
|
**Adding a new API endpoint:**
|
||||||
|
1. Create file in `/src/pages/api/[resource]/[action].ts`
|
||||||
|
2. Use `createSecureErrorResponse` for error handling
|
||||||
|
3. Add corresponding database query in `/src/lib/db/queries/`
|
||||||
|
4. Update types in `/src/types/` if needed
|
||||||
|
|
||||||
|
**Adding a new component:**
|
||||||
|
1. Create in appropriate `/src/components/[feature]/` directory
|
||||||
|
2. Use Shadcn UI components from `/src/components/ui/`
|
||||||
|
3. Follow existing naming patterns (e.g., `RepositoryCard`, `ConfigTabs`)
|
||||||
|
|
||||||
|
**Modifying database schema:**
|
||||||
|
1. Update schema in `/src/lib/db/schema.ts`
|
||||||
|
2. Run `bun run init-db` to recreate database
|
||||||
|
3. Update related queries in `/src/lib/db/queries/`
|
||||||
|
|
||||||
|
|
||||||
4
bun.lock
4
bun.lock
@@ -14,11 +14,11 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.6",
|
"@radix-ui/react-label": "^2.1.6",
|
||||||
"@radix-ui/react-popover": "^1.1.13",
|
"@radix-ui/react-popover": "^1.1.13",
|
||||||
"@radix-ui/react-radio-group": "^1.3.6",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
"@radix-ui/react-select": "^2.2.4",
|
"@radix-ui/react-select": "^2.2.4",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@radix-ui/react-tabs": "^1.1.12",
|
"@radix-ui/react-tabs": "^1.1.12",
|
||||||
"@radix-ui/react-tooltip": "^1.2.6",
|
"@radix-ui/react-tooltip": "^1.2.6",
|
||||||
|
|||||||
@@ -41,11 +41,11 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||||
"@radix-ui/react-label": "^2.1.6",
|
"@radix-ui/react-label": "^2.1.6",
|
||||||
"@radix-ui/react-popover": "^1.1.13",
|
"@radix-ui/react-popover": "^1.1.13",
|
||||||
"@radix-ui/react-radio-group": "^1.3.6",
|
"@radix-ui/react-radio-group": "^1.3.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
"@radix-ui/react-select": "^2.2.4",
|
"@radix-ui/react-select": "^2.2.4",
|
||||||
"@radix-ui/react-separator": "^1.1.7",
|
"@radix-ui/react-separator": "^1.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@radix-ui/react-tabs": "^1.1.12",
|
"@radix-ui/react-tabs": "^1.1.12",
|
||||||
"@radix-ui/react-tooltip": "^1.2.6",
|
"@radix-ui/react-tooltip": "^1.2.6",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { ScheduleConfigForm } from './ScheduleConfigForm';
|
|||||||
import { DatabaseCleanupConfigForm } from './DatabaseCleanupConfigForm';
|
import { DatabaseCleanupConfigForm } from './DatabaseCleanupConfigForm';
|
||||||
import { MirrorOptionsForm } from './MirrorOptionsForm';
|
import { MirrorOptionsForm } from './MirrorOptionsForm';
|
||||||
import { AdvancedOptionsForm } from './AdvancedOptionsForm';
|
import { AdvancedOptionsForm } from './AdvancedOptionsForm';
|
||||||
// Removed Tabs import as we're switching to grid layout
|
|
||||||
import type {
|
import type {
|
||||||
ConfigApiResponse,
|
ConfigApiResponse,
|
||||||
GiteaConfig,
|
GiteaConfig,
|
||||||
@@ -679,6 +678,7 @@ export function ConfigTabs() {
|
|||||||
}
|
}
|
||||||
onAutoSave={autoSaveGiteaConfig}
|
onAutoSave={autoSaveGiteaConfig}
|
||||||
isAutoSaving={isAutoSavingGitea}
|
isAutoSaving={isAutoSavingGitea}
|
||||||
|
githubUsername={config.githubConfig.username}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -16,21 +16,63 @@ import {
|
|||||||
} from "../ui/select";
|
} from "../ui/select";
|
||||||
import { Checkbox } from "../ui/checkbox";
|
import { Checkbox } from "../ui/checkbox";
|
||||||
import { giteaApi } from "@/lib/api";
|
import { giteaApi } from "@/lib/api";
|
||||||
import type { GiteaConfig, GiteaOrgVisibility } from "@/types/config";
|
import type { GiteaConfig, GiteaOrgVisibility, MirrorStrategy } from "@/types/config";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Info } from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
|
import { OrganizationStrategy } from "./OrganizationStrategy";
|
||||||
|
import { Separator } from "../ui/separator";
|
||||||
|
|
||||||
interface GiteaConfigFormProps {
|
interface GiteaConfigFormProps {
|
||||||
config: GiteaConfig;
|
config: GiteaConfig;
|
||||||
setConfig: React.Dispatch<React.SetStateAction<GiteaConfig>>;
|
setConfig: React.Dispatch<React.SetStateAction<GiteaConfig>>;
|
||||||
onAutoSave?: (giteaConfig: GiteaConfig) => Promise<void>;
|
onAutoSave?: (giteaConfig: GiteaConfig) => Promise<void>;
|
||||||
isAutoSaving?: boolean;
|
isAutoSaving?: boolean;
|
||||||
|
githubUsername?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving }: GiteaConfigFormProps) {
|
export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, githubUsername }: GiteaConfigFormProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Derive the mirror strategy from existing config for backward compatibility
|
||||||
|
const getMirrorStrategy = (): MirrorStrategy => {
|
||||||
|
if (config.mirrorStrategy) return config.mirrorStrategy;
|
||||||
|
if (config.preserveOrgStructure) return "preserve";
|
||||||
|
if (config.organization && config.organization !== config.username) return "single-org";
|
||||||
|
return "flat-user";
|
||||||
|
};
|
||||||
|
|
||||||
|
const [mirrorStrategy, setMirrorStrategy] = useState<MirrorStrategy>(getMirrorStrategy());
|
||||||
|
|
||||||
|
// Update config when strategy changes
|
||||||
|
useEffect(() => {
|
||||||
|
const newConfig = { ...config };
|
||||||
|
|
||||||
|
switch (mirrorStrategy) {
|
||||||
|
case "preserve":
|
||||||
|
newConfig.preserveOrgStructure = true;
|
||||||
|
newConfig.mirrorStrategy = "preserve";
|
||||||
|
break;
|
||||||
|
case "single-org":
|
||||||
|
newConfig.preserveOrgStructure = false;
|
||||||
|
newConfig.mirrorStrategy = "single-org";
|
||||||
|
if (!newConfig.organization) {
|
||||||
|
newConfig.organization = "github-mirrors";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "flat-user":
|
||||||
|
newConfig.preserveOrgStructure = false;
|
||||||
|
newConfig.mirrorStrategy = "flat-user";
|
||||||
|
newConfig.organization = "";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(newConfig);
|
||||||
|
if (onAutoSave) {
|
||||||
|
onAutoSave(newConfig);
|
||||||
|
}
|
||||||
|
}, [mirrorStrategy]);
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||||
) => {
|
) => {
|
||||||
@@ -168,124 +210,65 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving }:
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Separator className="my-2" />
|
||||||
|
|
||||||
|
<OrganizationStrategy
|
||||||
|
strategy={mirrorStrategy}
|
||||||
|
destinationOrg={config.organization}
|
||||||
|
starredReposOrg={config.starredReposOrg}
|
||||||
|
onStrategyChange={setMirrorStrategy}
|
||||||
|
onDestinationOrgChange={(org) => {
|
||||||
|
const newConfig = { ...config, organization: org };
|
||||||
|
setConfig(newConfig);
|
||||||
|
if (onAutoSave) onAutoSave(newConfig);
|
||||||
|
}}
|
||||||
|
onStarredReposOrgChange={(org) => {
|
||||||
|
const newConfig = { ...config, starredReposOrg: org };
|
||||||
|
setConfig(newConfig);
|
||||||
|
if (onAutoSave) onAutoSave(newConfig);
|
||||||
|
}}
|
||||||
|
githubUsername={githubUsername}
|
||||||
|
giteaUsername={config.username}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Separator className="my-2" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="organization"
|
htmlFor="visibility"
|
||||||
className="block text-sm font-medium mb-1.5"
|
className="block text-sm font-medium mb-1.5"
|
||||||
>
|
>
|
||||||
Destination organisation (optional)
|
Organization Visibility
|
||||||
</label>
|
</label>
|
||||||
<input
|
<Select
|
||||||
id="organization"
|
name="visibility"
|
||||||
name="organization"
|
value={config.visibility}
|
||||||
type="text"
|
onValueChange={(value) =>
|
||||||
value={config.organization}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
||||||
placeholder="Organization name"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Repos are created here if no per-repo org is set.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="preserve-org-structure"
|
|
||||||
name="preserveOrgStructure"
|
|
||||||
checked={config.preserveOrgStructure}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleChange({
|
handleChange({
|
||||||
target: {
|
target: { name: "visibility", value },
|
||||||
name: "preserveOrgStructure",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: Boolean(checked),
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
} as React.ChangeEvent<HTMLInputElement>)
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="preserve-org-structure"
|
|
||||||
className="ml-2 text-sm select-none flex items-center"
|
|
||||||
>
|
>
|
||||||
Mirror GitHub org / team hierarchy
|
<SelectTrigger className="w-full border border-input dark:bg-background dark:hover:bg-background">
|
||||||
<Tooltip>
|
<SelectValue placeholder="Select visibility" />
|
||||||
<TooltipTrigger asChild>
|
</SelectTrigger>
|
||||||
<span
|
<SelectContent className="bg-background text-foreground border border-input shadow-sm">
|
||||||
className="ml-1 cursor-pointer align-middle text-muted-foreground"
|
{(["public", "private", "limited"] as GiteaOrgVisibility[]).map(
|
||||||
role="button"
|
(option) => (
|
||||||
tabIndex={0}
|
<SelectItem
|
||||||
>
|
key={option}
|
||||||
<Info size={16} />
|
value={option}
|
||||||
</span>
|
className="cursor-pointer text-sm px-3 py-2 hover:bg-accent focus:bg-accent focus:text-accent-foreground"
|
||||||
</TooltipTrigger>
|
>
|
||||||
<TooltipContent side="right" className="max-w-xs text-xs">
|
{option.charAt(0).toUpperCase() + option.slice(1)}
|
||||||
Creates nested orgs or prefixes in Gitea so the layout matches GitHub.
|
</SelectItem>
|
||||||
When enabled, organization repositories will be mirrored to
|
)
|
||||||
the same organization structure in Gitea. When disabled, all
|
)}
|
||||||
repositories will be mirrored under your Gitea username.
|
</SelectContent>
|
||||||
</TooltipContent>
|
</Select>
|
||||||
</Tooltip>
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
</label>
|
Visibility for newly created organizations
|
||||||
</div>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="visibility"
|
|
||||||
className="block text-sm font-medium mb-1.5"
|
|
||||||
>
|
|
||||||
Organization Visibility
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
name="visibility"
|
|
||||||
value={config.visibility}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleChange({
|
|
||||||
target: { name: "visibility", value },
|
|
||||||
} as React.ChangeEvent<HTMLInputElement>)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full border border-input dark:bg-background dark:hover:bg-background">
|
|
||||||
<SelectValue placeholder="Select visibility" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="bg-background text-foreground border border-input shadow-sm">
|
|
||||||
{(["public", "private", "limited"] as GiteaOrgVisibility[]).map(
|
|
||||||
(option) => (
|
|
||||||
<SelectItem
|
|
||||||
key={option}
|
|
||||||
value={option}
|
|
||||||
className="cursor-pointer text-sm px-3 py-2 hover:bg-accent focus:bg-accent focus:text-accent-foreground"
|
|
||||||
>
|
|
||||||
{option.charAt(0).toUpperCase() + option.slice(1)}
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="starred-repos-org"
|
|
||||||
className="block text-sm font-medium mb-1.5"
|
|
||||||
>
|
|
||||||
Starred Repositories Organization
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="starred-repos-org"
|
|
||||||
name="starredReposOrg"
|
|
||||||
type="text"
|
|
||||||
value={config.starredReposOrg}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
||||||
placeholder="github"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Leave blank to use 'github'.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
|||||||
387
src/components/config/OrganizationStrategy.tsx
Normal file
387
src/components/config/OrganizationStrategy.tsx
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Info, GitBranch, FolderTree, Package, Star, Building2, User } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type MirrorStrategy = "preserve" | "single-org" | "flat-user";
|
||||||
|
|
||||||
|
interface OrganizationStrategyProps {
|
||||||
|
strategy: MirrorStrategy;
|
||||||
|
destinationOrg?: string;
|
||||||
|
starredReposOrg?: string;
|
||||||
|
onStrategyChange: (strategy: MirrorStrategy) => void;
|
||||||
|
onDestinationOrgChange: (org: string) => void;
|
||||||
|
onStarredReposOrgChange: (org: string) => void;
|
||||||
|
githubUsername?: string;
|
||||||
|
giteaUsername?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strategyConfig = {
|
||||||
|
preserve: {
|
||||||
|
title: "Mirror GitHub Structure",
|
||||||
|
icon: FolderTree,
|
||||||
|
description: "Keep the same organization structure as GitHub",
|
||||||
|
color: "text-blue-600",
|
||||||
|
bgColor: "bg-blue-50",
|
||||||
|
borderColor: "border-blue-200",
|
||||||
|
details: [
|
||||||
|
"Personal repos → Your Gitea username",
|
||||||
|
"Org repos → Same org name in Gitea",
|
||||||
|
"Team structure preserved"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"single-org": {
|
||||||
|
title: "Consolidate to One Org",
|
||||||
|
icon: Building2,
|
||||||
|
description: "Mirror all repositories into a single organization",
|
||||||
|
color: "text-purple-600",
|
||||||
|
bgColor: "bg-purple-50",
|
||||||
|
borderColor: "border-purple-200",
|
||||||
|
details: [
|
||||||
|
"All repos in one place",
|
||||||
|
"Simplified management",
|
||||||
|
"Custom organization name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"flat-user": {
|
||||||
|
title: "Flat User Structure",
|
||||||
|
icon: User,
|
||||||
|
description: "Mirror all repositories under your user account",
|
||||||
|
color: "text-green-600",
|
||||||
|
bgColor: "bg-green-50",
|
||||||
|
borderColor: "border-green-200",
|
||||||
|
details: [
|
||||||
|
"All repos under your username",
|
||||||
|
"No organizations needed",
|
||||||
|
"Simple and personal"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const StrategyVisualizer: React.FC<{
|
||||||
|
strategy: MirrorStrategy;
|
||||||
|
destinationOrg?: string;
|
||||||
|
starredReposOrg?: string;
|
||||||
|
githubUsername?: string;
|
||||||
|
giteaUsername?: string;
|
||||||
|
}> = ({ strategy, destinationOrg, starredReposOrg, githubUsername = "you", giteaUsername = "you" }) => {
|
||||||
|
const renderPreserveStructure = () => (
|
||||||
|
<div className="flex items-center justify-between gap-8 p-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-medium text-muted-foreground mb-3">GitHub</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
<span className="text-sm">{githubUsername}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||||
|
<Building2 className="h-4 w-4" />
|
||||||
|
<span className="text-sm">my-org/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||||
|
<Star className="h-4 w-4" />
|
||||||
|
<span className="text-sm">awesome/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-medium text-muted-foreground mb-3">Gitea</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-blue-50 rounded">
|
||||||
|
<User className="h-4 w-4 text-blue-600" />
|
||||||
|
<span className="text-sm">{giteaUsername}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-blue-50 rounded">
|
||||||
|
<Building2 className="h-4 w-4 text-blue-600" />
|
||||||
|
<span className="text-sm">my-org/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-blue-50 rounded">
|
||||||
|
<Building2 className="h-4 w-4 text-blue-600" />
|
||||||
|
<span className="text-sm">{starredReposOrg || "starred"}/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSingleOrg = () => (
|
||||||
|
<div className="flex items-center justify-between gap-8 p-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-medium text-muted-foreground mb-3">GitHub</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
<span className="text-sm">{githubUsername}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||||
|
<Building2 className="h-4 w-4" />
|
||||||
|
<span className="text-sm">my-org/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||||
|
<Star className="h-4 w-4" />
|
||||||
|
<span className="text-sm">awesome/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-medium text-muted-foreground mb-3">Gitea</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-purple-50 rounded">
|
||||||
|
<Building2 className="h-4 w-4 text-purple-600" />
|
||||||
|
<span className="text-sm">{destinationOrg || "github-mirrors"}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-purple-50 rounded">
|
||||||
|
<Building2 className="h-4 w-4 text-purple-600" />
|
||||||
|
<span className="text-sm">{destinationOrg || "github-mirrors"}/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-purple-50 rounded">
|
||||||
|
<Building2 className="h-4 w-4 text-purple-600" />
|
||||||
|
<span className="text-sm">{starredReposOrg || "starred"}/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderFlatUser = () => (
|
||||||
|
<div className="flex items-center justify-between gap-8 p-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-medium text-muted-foreground mb-3">GitHub</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
<span className="text-sm">{githubUsername}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||||
|
<Building2 className="h-4 w-4" />
|
||||||
|
<span className="text-sm">my-org/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded">
|
||||||
|
<Star className="h-4 w-4" />
|
||||||
|
<span className="text-sm">awesome/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-sm font-medium text-muted-foreground mb-3">Gitea</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-green-50 rounded">
|
||||||
|
<User className="h-4 w-4 text-green-600" />
|
||||||
|
<span className="text-sm">{giteaUsername}/my-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-green-50 rounded">
|
||||||
|
<User className="h-4 w-4 text-green-600" />
|
||||||
|
<span className="text-sm">{giteaUsername}/team-repo</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 p-2 bg-green-50 rounded">
|
||||||
|
<Building2 className="h-4 w-4 text-green-600" />
|
||||||
|
<span className="text-sm">{starredReposOrg || "starred"}/starred-repo</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
<div className="bg-muted/50 p-3 border-b">
|
||||||
|
<h4 className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<Package className="h-4 w-4" />
|
||||||
|
Repository Mapping Preview
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
{strategy === "preserve" && renderPreserveStructure()}
|
||||||
|
{strategy === "single-org" && renderSingleOrg()}
|
||||||
|
{strategy === "flat-user" && renderFlatUser()}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
||||||
|
strategy,
|
||||||
|
destinationOrg,
|
||||||
|
starredReposOrg,
|
||||||
|
onStrategyChange,
|
||||||
|
onDestinationOrgChange,
|
||||||
|
onStarredReposOrgChange,
|
||||||
|
githubUsername,
|
||||||
|
giteaUsername,
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-1">Organization Strategy</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Choose how your repositories will be organized in Gitea
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RadioGroup value={strategy} onValueChange={onStrategyChange}>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{(Object.entries(strategyConfig) as [MirrorStrategy, typeof strategyConfig.preserve][]).map(([key, config]) => {
|
||||||
|
const isSelected = strategy === key;
|
||||||
|
const Icon = config.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key}>
|
||||||
|
<label htmlFor={key} className="cursor-pointer">
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"relative",
|
||||||
|
isSelected && `${config.borderColor} border-2`,
|
||||||
|
!isSelected && "border-muted"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<RadioGroupItem
|
||||||
|
value={key}
|
||||||
|
id={key}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={cn(
|
||||||
|
"rounded-lg p-2",
|
||||||
|
isSelected ? config.bgColor : "bg-muted"
|
||||||
|
)}>
|
||||||
|
<Icon className={cn(
|
||||||
|
"h-5 w-5",
|
||||||
|
isSelected ? config.color : "text-muted-foreground"
|
||||||
|
)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h4 className="font-medium">{config.title}</h4>
|
||||||
|
{isSelected && (
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
Selected
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
|
{config.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
{config.details.map((detail, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-2">
|
||||||
|
<div className={cn(
|
||||||
|
"h-1.5 w-1.5 rounded-full",
|
||||||
|
isSelected ? config.bgColor : "bg-muted"
|
||||||
|
)} />
|
||||||
|
<span className="text-xs text-muted-foreground">{detail}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
{strategy === "single-org" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card className="p-4 border-purple-200 bg-purple-50/50">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="destinationOrg" className="flex items-center gap-2">
|
||||||
|
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>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="destinationOrg"
|
||||||
|
value={destinationOrg || ""}
|
||||||
|
onChange={(e) => onDestinationOrgChange(e.target.value)}
|
||||||
|
placeholder="github-mirrors"
|
||||||
|
className="mt-1.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card className="p-4 border-orange-200 bg-orange-50/50">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="starredReposOrg" className="flex items-center gap-2">
|
||||||
|
<Star className="h-4 w-4 text-orange-600" />
|
||||||
|
Starred Repositories 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="mt-1.5"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Keep starred repos organized separately from your own repositories
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<StrategyVisualizer
|
||||||
|
strategy={strategy}
|
||||||
|
destinationOrg={destinationOrg}
|
||||||
|
starredReposOrg={starredReposOrg}
|
||||||
|
githubUsername={githubUsername}
|
||||||
|
giteaUsername={giteaUsername}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
43
src/components/ui/radio-group.tsx
Normal file
43
src/components/ui/radio-group.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||||
|
import { CircleIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function RadioGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
data-slot="radio-group"
|
||||||
|
className={cn("grid gap-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RadioGroupItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
data-slot="radio-group-item"
|
||||||
|
className={cn(
|
||||||
|
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator
|
||||||
|
data-slot="radio-group-indicator"
|
||||||
|
className="relative flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem }
|
||||||
@@ -34,7 +34,6 @@ export const configSchema = z.object({
|
|||||||
excludeOrgs: z.array(z.string()).default([]),
|
excludeOrgs: z.array(z.string()).default([]),
|
||||||
mirrorPublicOrgs: z.boolean().default(false),
|
mirrorPublicOrgs: z.boolean().default(false),
|
||||||
publicOrgs: z.array(z.string()).default([]),
|
publicOrgs: z.array(z.string()).default([]),
|
||||||
preserveOrgStructure: z.boolean().default(false),
|
|
||||||
skipStarredIssues: z.boolean().default(false),
|
skipStarredIssues: z.boolean().default(false),
|
||||||
}),
|
}),
|
||||||
giteaConfig: z.object({
|
giteaConfig: z.object({
|
||||||
@@ -44,6 +43,8 @@ export const configSchema = z.object({
|
|||||||
organization: z.string().optional(),
|
organization: z.string().optional(),
|
||||||
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),
|
||||||
|
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user"]).optional(),
|
||||||
}),
|
}),
|
||||||
include: z.array(z.string()).default(["*"]),
|
include: z.array(z.string()).default(["*"]),
|
||||||
exclude: z.array(z.string()).default([]),
|
exclude: z.array(z.string()).default([]),
|
||||||
|
|||||||
@@ -1,6 +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 interface GiteaConfig {
|
export interface GiteaConfig {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -10,6 +11,7 @@ export interface GiteaConfig {
|
|||||||
visibility: GiteaOrgVisibility;
|
visibility: GiteaOrgVisibility;
|
||||||
starredReposOrg: string;
|
starredReposOrg: string;
|
||||||
preserveOrgStructure: boolean;
|
preserveOrgStructure: boolean;
|
||||||
|
mirrorStrategy?: MirrorStrategy; // New field for the strategy
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScheduleConfig {
|
export interface ScheduleConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user