Added some basic options

This commit is contained in:
Arunavo Ray
2025-06-15 12:15:14 +05:30
parent 544b60f881
commit f4df7c3d19
10 changed files with 701 additions and 118 deletions

121
CLAUDE.md Normal file
View 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/`

View File

@@ -14,11 +14,11 @@
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-label": "^2.1.6",
"@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-select": "^2.2.4",
"@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-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.6",

View File

@@ -41,11 +41,11 @@
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-label": "^2.1.6",
"@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-select": "^2.2.4",
"@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-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.6",

View File

@@ -5,7 +5,6 @@ import { ScheduleConfigForm } from './ScheduleConfigForm';
import { DatabaseCleanupConfigForm } from './DatabaseCleanupConfigForm';
import { MirrorOptionsForm } from './MirrorOptionsForm';
import { AdvancedOptionsForm } from './AdvancedOptionsForm';
// Removed Tabs import as we're switching to grid layout
import type {
ConfigApiResponse,
GiteaConfig,
@@ -679,6 +678,7 @@ export function ConfigTabs() {
}
onAutoSave={autoSaveGiteaConfig}
isAutoSaving={isAutoSavingGitea}
githubUsername={config.githubConfig.username}
/>
</div>

View File

@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -16,20 +16,62 @@ import {
} from "../ui/select";
import { Checkbox } from "../ui/checkbox";
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 { Info } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { OrganizationStrategy } from "./OrganizationStrategy";
import { Separator } from "../ui/separator";
interface GiteaConfigFormProps {
config: GiteaConfig;
setConfig: React.Dispatch<React.SetStateAction<GiteaConfig>>;
onAutoSave?: (giteaConfig: GiteaConfig) => Promise<void>;
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);
// 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 = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
@@ -168,124 +210,65 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving }:
</p>
</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>
<label
htmlFor="organization"
htmlFor="visibility"
className="block text-sm font-medium mb-1.5"
>
Destination organisation (optional)
Organization Visibility
</label>
<input
id="organization"
name="organization"
type="text"
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) =>
<Select
name="visibility"
value={config.visibility}
onValueChange={(value) =>
handleChange({
target: {
name: "preserveOrgStructure",
type: "checkbox",
checked: Boolean(checked),
value: "",
},
target: { name: "visibility", value },
} as React.ChangeEvent<HTMLInputElement>)
}
/>
<label
htmlFor="preserve-org-structure"
className="ml-2 text-sm select-none flex items-center"
>
Mirror GitHub org / team hierarchy
<Tooltip>
<TooltipTrigger asChild>
<span
className="ml-1 cursor-pointer align-middle text-muted-foreground"
role="button"
tabIndex={0}
>
<Info size={16} />
</span>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs text-xs">
Creates nested orgs or prefixes in Gitea so the layout matches GitHub.
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.
</TooltipContent>
</Tooltip>
</label>
</div>
<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>
<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>
<p className="text-xs text-muted-foreground mt-1">
Visibility for newly created organizations
</p>
</div>
</CardContent>

View 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>
);
};

View 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 }

View 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 }

View File

@@ -34,7 +34,6 @@ export const configSchema = z.object({
excludeOrgs: z.array(z.string()).default([]),
mirrorPublicOrgs: z.boolean().default(false),
publicOrgs: z.array(z.string()).default([]),
preserveOrgStructure: z.boolean().default(false),
skipStarredIssues: z.boolean().default(false),
}),
giteaConfig: z.object({
@@ -44,6 +43,8 @@ export const configSchema = z.object({
organization: z.string().optional(),
visibility: z.enum(["public", "private", "limited"]).default("public"),
starredReposOrg: z.string().default("github"),
preserveOrgStructure: z.boolean().default(false),
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user"]).optional(),
}),
include: z.array(z.string()).default(["*"]),
exclude: z.array(z.string()).default([]),

View File

@@ -1,6 +1,7 @@
import { type Config as ConfigType } from "@/lib/db/schema";
export type GiteaOrgVisibility = "public" | "private" | "limited";
export type MirrorStrategy = "preserve" | "single-org" | "flat-user";
export interface GiteaConfig {
url: string;
@@ -10,6 +11,7 @@ export interface GiteaConfig {
visibility: GiteaOrgVisibility;
starredReposOrg: string;
preserveOrgStructure: boolean;
mirrorStrategy?: MirrorStrategy; // New field for the strategy
}
export interface ScheduleConfig {