mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-07 20:16:46 +03:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b53a29e71 | ||
|
|
64e73f9ca8 | ||
|
|
7d23894e5f | ||
|
|
8f2a4683c1 | ||
|
|
b5323ff8b4 | ||
|
|
7fee2adb51 | ||
|
|
af139ecb2d | ||
|
|
fb827724b6 | ||
|
|
2812b576d0 | ||
|
|
347188f43d | ||
|
|
56bee451de | ||
|
|
0e9d54b517 | ||
|
|
7a04665b70 | ||
|
|
3a3ff314e0 | ||
|
|
fed74ee901 | ||
|
|
85ea502276 | ||
|
|
ffb7bd3cb0 | ||
|
|
b39d7a2179 | ||
|
|
bf99a95dc6 | ||
|
|
2ea917fdaa |
2
.github/ci/values-ci.yaml
vendored
2
.github/ci/values-ci.yaml
vendored
@@ -37,7 +37,7 @@ gitea-mirror:
|
||||
type: "personal"
|
||||
privateRepositories: true
|
||||
skipForks: false
|
||||
skipStarredIssues: false
|
||||
starredCodeOnly: false
|
||||
gitea:
|
||||
url: "https://gitea.example.com"
|
||||
token: "not-used-in-template"
|
||||
|
||||
8
.github/workflows/docker-build.yml
vendored
8
.github/workflows/docker-build.yml
vendored
@@ -10,6 +10,10 @@ on:
|
||||
- 'package.json'
|
||||
- 'bun.lock*'
|
||||
- '.github/workflows/docker-build.yml'
|
||||
- 'docker-entrypoint.sh'
|
||||
- 'drizzle/**'
|
||||
- 'scripts/**'
|
||||
- 'src/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
@@ -17,6 +21,10 @@ on:
|
||||
- 'package.json'
|
||||
- 'bun.lock*'
|
||||
- '.github/workflows/docker-build.yml'
|
||||
- 'docker-entrypoint.sh'
|
||||
- 'drizzle/**'
|
||||
- 'scripts/**'
|
||||
- 'src/**'
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # Weekly security scan on Sunday at midnight
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ export async function POST({ request }: APIContext) {
|
||||
|
||||
### Advanced Options (UI Fields)
|
||||
- **skipForks**: Skip forked repositories (default: false)
|
||||
- **skipStarredIssues**: Skip issues for starred repositories (default: false) - enables "Lightweight mode" for starred repos
|
||||
- **starredCodeOnly**: Skip issues for starred repositories (default: false) - enables "Lightweight mode" for starred repos
|
||||
|
||||
### Repository Statuses
|
||||
Repositories can have the following statuses:
|
||||
|
||||
@@ -120,161 +120,13 @@ fi
|
||||
# Dependencies are already installed during the Docker build process
|
||||
|
||||
# Initialize the database if it doesn't exist
|
||||
# Note: Drizzle migrations will be run automatically when the app starts (see src/lib/db/index.ts)
|
||||
if [ ! -f "/app/data/gitea-mirror.db" ]; then
|
||||
echo "Initializing database..."
|
||||
if [ -f "dist/scripts/init-db.js" ]; then
|
||||
bun dist/scripts/init-db.js
|
||||
elif [ -f "dist/scripts/manage-db.js" ]; then
|
||||
bun dist/scripts/manage-db.js init
|
||||
elif [ -f "scripts/manage-db.ts" ]; then
|
||||
bun scripts/manage-db.ts init
|
||||
else
|
||||
echo "Warning: Could not find database initialization scripts in dist/scripts."
|
||||
echo "Creating and initializing database manually..."
|
||||
|
||||
# Create the database file
|
||||
touch /app/data/gitea-mirror.db
|
||||
|
||||
# Initialize the database with required tables
|
||||
sqlite3 /app/data/gitea-mirror.db <<EOF
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS configs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
github_config TEXT NOT NULL,
|
||||
gitea_config TEXT NOT NULL,
|
||||
include TEXT NOT NULL DEFAULT '["*"]',
|
||||
exclude TEXT NOT NULL DEFAULT '[]',
|
||||
schedule_config TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS repositories (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
config_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
clone_url TEXT NOT NULL,
|
||||
owner TEXT NOT NULL,
|
||||
organization TEXT,
|
||||
mirrored_location TEXT DEFAULT '',
|
||||
destination_org TEXT,
|
||||
is_private INTEGER NOT NULL DEFAULT 0,
|
||||
is_fork INTEGER NOT NULL DEFAULT 0,
|
||||
forked_from TEXT,
|
||||
has_issues INTEGER NOT NULL DEFAULT 0,
|
||||
is_starred INTEGER NOT NULL DEFAULT 0,
|
||||
is_archived INTEGER NOT NULL DEFAULT 0,
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
has_lfs INTEGER NOT NULL DEFAULT 0,
|
||||
has_submodules INTEGER NOT NULL DEFAULT 0,
|
||||
language TEXT,
|
||||
description TEXT,
|
||||
default_branch TEXT NOT NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'public',
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
last_mirrored INTEGER,
|
||||
error_message TEXT,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||
);
|
||||
|
||||
-- Uniqueness of (user_id, full_name) for repositories is enforced via drizzle migrations
|
||||
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
config_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
avatar_url TEXT NOT NULL,
|
||||
membership_role TEXT NOT NULL DEFAULT 'member',
|
||||
is_included INTEGER NOT NULL DEFAULT 1,
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
last_mirrored INTEGER,
|
||||
error_message TEXT,
|
||||
repository_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mirror_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
repository_id TEXT,
|
||||
repository_name TEXT,
|
||||
organization_id TEXT,
|
||||
organization_name TEXT,
|
||||
details TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
message TEXT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- New fields for job resilience
|
||||
job_type TEXT NOT NULL DEFAULT 'mirror',
|
||||
batch_id TEXT,
|
||||
total_items INTEGER,
|
||||
completed_items INTEGER DEFAULT 0,
|
||||
item_ids TEXT, -- JSON array as text
|
||||
completed_item_ids TEXT DEFAULT '[]', -- JSON array as text
|
||||
in_progress INTEGER NOT NULL DEFAULT 0, -- Boolean as integer
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
last_checkpoint TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_user_id ON mirror_jobs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_batch_id ON mirror_jobs(batch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_in_progress ON mirror_jobs(in_progress);
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_job_type ON mirror_jobs(job_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_timestamp ON mirror_jobs(timestamp);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
read INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_events_user_channel ON events(user_id, channel);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_read ON events(read);
|
||||
EOF
|
||||
echo "Database initialized with required tables."
|
||||
fi
|
||||
echo "Database not found. It will be created and initialized via Drizzle migrations on first app startup..."
|
||||
# Create empty database file so migrations can run
|
||||
touch /app/data/gitea-mirror.db
|
||||
else
|
||||
echo "Database already exists, checking for issues..."
|
||||
if [ -f "dist/scripts/fix-db-issues.js" ]; then
|
||||
bun dist/scripts/fix-db-issues.js
|
||||
elif [ -f "dist/scripts/manage-db.js" ]; then
|
||||
bun dist/scripts/manage-db.js fix
|
||||
elif [ -f "scripts/manage-db.ts" ]; then
|
||||
bun scripts/manage-db.ts fix
|
||||
fi
|
||||
|
||||
echo "Database exists, checking integrity..."
|
||||
echo "Database already exists, Drizzle will check for pending migrations on startup..."
|
||||
fi
|
||||
|
||||
# Extract version from package.json and set as environment variable
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
CREATE UNIQUE INDEX `uniq_repositories_user_full_name` ON `repositories` (`user_id`,`full_name`);
|
||||
-- Step 1: Remove duplicate repositories, keeping the most recently updated one
|
||||
-- This handles cases where users have duplicate entries from before the unique constraint
|
||||
DELETE FROM repositories
|
||||
WHERE rowid NOT IN (
|
||||
SELECT MAX(rowid)
|
||||
FROM repositories
|
||||
GROUP BY user_id, full_name
|
||||
);
|
||||
--> statement-breakpoint
|
||||
-- Step 2: Now create the unique index safely
|
||||
CREATE UNIQUE INDEX uniq_repositories_user_full_name ON repositories (user_id, full_name);
|
||||
@@ -175,7 +175,7 @@ These values populate a **ConfigMap** (non-secret) and a **Secret** (for tokens
|
||||
| `gitea-mirror.github.type` | `personal` | `GITHUB_TYPE` |
|
||||
| `gitea-mirror.github.privateRepositories` | `true` | `PRIVATE_REPOSITORIES` |
|
||||
| `gitea-mirror.github.skipForks` | `false` | `SKIP_FORKS` |
|
||||
| `gitea-mirror.github.skipStarredIssues` | `false` | `SKIP_STARRED_ISSUES` |
|
||||
| `gitea-mirror.github.starredCodeOnly` | `false` | `SKIP_STARRED_ISSUES` |
|
||||
| `gitea-mirror.github.mirrorStarred` | `false` | `MIRROR_STARRED` |
|
||||
|
||||
### Gitea
|
||||
|
||||
@@ -18,7 +18,7 @@ data:
|
||||
PRIVATE_REPOSITORIES: {{ $gm.github.privateRepositories | quote }}
|
||||
MIRROR_STARRED: {{ $gm.github.mirrorStarred | quote }}
|
||||
SKIP_FORKS: {{ $gm.github.skipForks | quote }}
|
||||
SKIP_STARRED_ISSUES: {{ $gm.github.skipStarredIssues | quote }}
|
||||
SKIP_STARRED_ISSUES: {{ $gm.github.starredCodeOnly | quote }}
|
||||
# Gitea Config
|
||||
GITEA_URL: {{ $gm.gitea.url | quote }}
|
||||
GITEA_USERNAME: {{ $gm.gitea.username | quote }}
|
||||
|
||||
@@ -49,7 +49,7 @@ spec:
|
||||
terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds }}
|
||||
containers:
|
||||
- name: gitea-mirror
|
||||
image: {{ .Values.image.registry }}/{{ .Values.image.repository }}:v{{ .Values.image.tag | default .Chart.AppVersion | toString }}
|
||||
image: {{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default (printf "v%s" .Chart.AppVersion) }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
|
||||
@@ -126,7 +126,7 @@ gitea-mirror:
|
||||
privateRepositories: true
|
||||
mirrorStarred: false
|
||||
skipForks: false
|
||||
skipStarredIssues: false
|
||||
starredCodeOnly: false
|
||||
|
||||
gitea:
|
||||
url: ""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gitea-mirror",
|
||||
"type": "module",
|
||||
"version": "3.7.1",
|
||||
"version": "3.8.4",
|
||||
"engines": {
|
||||
"bun": ">=1.2.9"
|
||||
},
|
||||
|
||||
@@ -67,21 +67,21 @@ export function AdvancedOptionsForm({
|
||||
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
id="skip-starred-issues"
|
||||
checked={config.skipStarredIssues}
|
||||
id="starred-code-only"
|
||||
checked={config.starredCodeOnly}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange("skipStarredIssues", Boolean(checked))
|
||||
handleChange("starredCodeOnly", Boolean(checked))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="skip-starred-issues"
|
||||
htmlFor="starred-code-only"
|
||||
className="ml-2 text-sm select-none"
|
||||
>
|
||||
Don't fetch issues for starred repos
|
||||
Code-only mode for starred repos
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground ml-6">
|
||||
Skip mirroring issues and pull requests for starred repositories
|
||||
Mirror only source code for starred repositories, skipping all metadata (issues, PRs, labels, milestones, wiki, releases)
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -71,7 +71,7 @@ export function ConfigTabs() {
|
||||
},
|
||||
advancedOptions: {
|
||||
skipForks: false,
|
||||
skipStarredIssues: false,
|
||||
starredCodeOnly: false,
|
||||
},
|
||||
});
|
||||
const { user } = useAuth();
|
||||
|
||||
@@ -89,10 +89,10 @@ export function GitHubMirrorSettings({
|
||||
// Calculate what content is included for starred repos
|
||||
const starredRepoContent = {
|
||||
code: true, // Always included
|
||||
releases: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorReleases,
|
||||
issues: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
|
||||
pullRequests: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.pullRequests,
|
||||
wiki: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
||||
releases: !advancedOptions.starredCodeOnly && mirrorOptions.mirrorReleases,
|
||||
issues: !advancedOptions.starredCodeOnly && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
|
||||
pullRequests: !advancedOptions.starredCodeOnly && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.pullRequests,
|
||||
wiki: !advancedOptions.starredCodeOnly && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
||||
};
|
||||
|
||||
const starredContentCount = Object.entries(starredRepoContent).filter(([key, value]) => key !== 'code' && value).length;
|
||||
@@ -168,7 +168,7 @@ export function GitHubMirrorSettings({
|
||||
className="h-8 text-xs font-normal min-w-[140px] md:min-w-[140px] justify-between"
|
||||
>
|
||||
<span>
|
||||
{advancedOptions.skipStarredIssues ? (
|
||||
{advancedOptions.starredCodeOnly ? (
|
||||
"Code only"
|
||||
) : starredContentCount === 0 ? (
|
||||
"Code only"
|
||||
@@ -206,8 +206,8 @@ export function GitHubMirrorSettings({
|
||||
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||
<Checkbox
|
||||
id="starred-lightweight"
|
||||
checked={advancedOptions.skipStarredIssues}
|
||||
onCheckedChange={(checked) => handleAdvancedChange('skipStarredIssues', !!checked)}
|
||||
checked={advancedOptions.starredCodeOnly}
|
||||
onCheckedChange={(checked) => handleAdvancedChange('starredCodeOnly', !!checked)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="starred-lightweight"
|
||||
@@ -222,7 +222,7 @@ export function GitHubMirrorSettings({
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{!advancedOptions.skipStarredIssues && (
|
||||
{!advancedOptions.starredCodeOnly && (
|
||||
<>
|
||||
<Separator className="my-2" />
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -27,7 +27,8 @@ export const githubConfigSchema = z.object({
|
||||
starredReposOrg: z.string().optional(),
|
||||
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).default("preserve"),
|
||||
defaultOrg: z.string().optional(),
|
||||
skipStarredIssues: z.boolean().default(false),
|
||||
starredCodeOnly: z.boolean().default(false),
|
||||
skipStarredIssues: z.boolean().optional(), // Deprecated: kept for backward compatibility, use starredCodeOnly instead
|
||||
starredDuplicateStrategy: z.enum(["suffix", "prefix", "owner-org"]).default("suffix").optional(),
|
||||
});
|
||||
|
||||
@@ -207,7 +208,7 @@ export const organizationSchema = z.object({
|
||||
configId: z.string(),
|
||||
name: z.string(),
|
||||
avatarUrl: z.string(),
|
||||
membershipRole: z.enum(["admin", "member", "owner"]).default("member"),
|
||||
membershipRole: z.enum(["member", "admin", "owner", "billing_manager"]).default("member"),
|
||||
isIncluded: z.boolean().default(true),
|
||||
destinationOrg: z.string().optional().nullable(),
|
||||
status: z
|
||||
|
||||
@@ -21,7 +21,7 @@ interface EnvConfig {
|
||||
mirrorOrganizations?: boolean;
|
||||
preserveOrgStructure?: boolean;
|
||||
onlyMirrorOrgs?: boolean;
|
||||
skipStarredIssues?: boolean;
|
||||
starredCodeOnly?: boolean;
|
||||
starredReposOrg?: string;
|
||||
mirrorStrategy?: 'preserve' | 'single-org' | 'flat-user' | 'mixed';
|
||||
};
|
||||
@@ -107,7 +107,7 @@ function parseEnvConfig(): EnvConfig {
|
||||
mirrorOrganizations: process.env.MIRROR_ORGANIZATIONS === 'true',
|
||||
preserveOrgStructure: process.env.PRESERVE_ORG_STRUCTURE === 'true',
|
||||
onlyMirrorOrgs: process.env.ONLY_MIRROR_ORGS === 'true',
|
||||
skipStarredIssues: process.env.SKIP_STARRED_ISSUES === 'true',
|
||||
starredCodeOnly: process.env.SKIP_STARRED_ISSUES === 'true',
|
||||
starredReposOrg: process.env.STARRED_REPOS_ORG,
|
||||
mirrorStrategy: process.env.MIRROR_STRATEGY as 'preserve' | 'single-org' | 'flat-user' | 'mixed',
|
||||
},
|
||||
@@ -253,7 +253,7 @@ export async function initializeConfigFromEnv(): Promise<void> {
|
||||
starredReposOrg: envConfig.github.starredReposOrg || existingConfig?.[0]?.githubConfig?.starredReposOrg || 'starred',
|
||||
mirrorStrategy,
|
||||
defaultOrg: envConfig.gitea.organization || existingConfig?.[0]?.githubConfig?.defaultOrg || 'github-mirrors',
|
||||
skipStarredIssues: envConfig.github.skipStarredIssues ?? existingConfig?.[0]?.githubConfig?.skipStarredIssues ?? false,
|
||||
starredCodeOnly: envConfig.github.starredCodeOnly ?? existingConfig?.[0]?.githubConfig?.starredCodeOnly ?? false,
|
||||
};
|
||||
|
||||
// Build Gitea config
|
||||
|
||||
@@ -8,10 +8,19 @@ import { createMockResponse, mockFetch } from "@/tests/mock-fetch";
|
||||
// Mock the isRepoPresentInGitea function
|
||||
const mockIsRepoPresentInGitea = mock(() => Promise.resolve(false));
|
||||
|
||||
let mockDbSelectResult: any[] = [];
|
||||
|
||||
// Mock the database module
|
||||
mock.module("@/lib/db", () => {
|
||||
return {
|
||||
db: {
|
||||
select: () => ({
|
||||
from: () => ({
|
||||
where: () => ({
|
||||
limit: () => Promise.resolve(mockDbSelectResult)
|
||||
})
|
||||
})
|
||||
}),
|
||||
update: () => ({
|
||||
set: () => ({
|
||||
where: () => Promise.resolve()
|
||||
@@ -63,6 +72,7 @@ describe("Gitea Repository Mirroring", () => {
|
||||
originalConsoleError = console.error;
|
||||
console.log = mock(() => {});
|
||||
console.error = mock(() => {});
|
||||
mockDbSelectResult = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -331,7 +341,7 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
||||
excludeOrgs: [],
|
||||
mirrorPublicOrgs: false,
|
||||
publicOrgs: [],
|
||||
skipStarredIssues: false,
|
||||
starredCodeOnly: false,
|
||||
mirrorStrategy: "preserve"
|
||||
},
|
||||
giteaConfig: {
|
||||
@@ -449,4 +459,37 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
||||
const result = getGiteaRepoOwner({ config: configWithFlatUser, repository: repo });
|
||||
expect(result).toBe("giteauser");
|
||||
});
|
||||
|
||||
test("getGiteaRepoOwnerAsync honors organization override for owner role", async () => {
|
||||
mockDbSelectResult = [
|
||||
{
|
||||
id: "org-id",
|
||||
userId: "user-id",
|
||||
configId: "config-id",
|
||||
name: "myorg",
|
||||
membershipRole: "owner",
|
||||
status: "imported",
|
||||
destinationOrg: "custom-org",
|
||||
avatarUrl: "https://example.com/avatar.png",
|
||||
isIncluded: true,
|
||||
repositoryCount: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
];
|
||||
|
||||
const configWithUser: Partial<Config> = {
|
||||
...baseConfig,
|
||||
userId: "user-id"
|
||||
};
|
||||
|
||||
const repo = { ...baseRepo, organization: "myorg" };
|
||||
|
||||
const result = await getGiteaRepoOwnerAsync({
|
||||
config: configWithUser,
|
||||
repository: repo
|
||||
});
|
||||
|
||||
expect(result).toBe("custom-org");
|
||||
});
|
||||
});
|
||||
|
||||
303
src/lib/gitea.ts
303
src/lib/gitea.ts
@@ -200,6 +200,96 @@ export const isRepoPresentInGitea = async ({
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a repository is currently being mirrored (in-progress state in database)
|
||||
* This prevents race conditions where multiple concurrent operations try to mirror the same repo
|
||||
*/
|
||||
export const isRepoCurrentlyMirroring = async ({
|
||||
config,
|
||||
repoName,
|
||||
expectedLocation,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
repoName: string;
|
||||
expectedLocation?: string; // Format: "owner/repo"
|
||||
}): Promise<boolean> => {
|
||||
try {
|
||||
if (!config.userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { or } = await import("drizzle-orm");
|
||||
|
||||
// Check database for any repository with "mirroring" or "syncing" status
|
||||
const inProgressRepos = await db
|
||||
.select()
|
||||
.from(repositories)
|
||||
.where(
|
||||
and(
|
||||
eq(repositories.userId, config.userId),
|
||||
eq(repositories.name, repoName),
|
||||
// Check for in-progress statuses
|
||||
or(
|
||||
eq(repositories.status, "mirroring"),
|
||||
eq(repositories.status, "syncing")
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if (inProgressRepos.length > 0) {
|
||||
// Check if any of the in-progress repos are stale (stuck for > 2 hours)
|
||||
const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
|
||||
const now = new Date().getTime();
|
||||
|
||||
const activeRepos = inProgressRepos.filter((repo) => {
|
||||
if (!repo.updatedAt) return true; // No timestamp, assume active
|
||||
const updatedTime = new Date(repo.updatedAt).getTime();
|
||||
const isStale = (now - updatedTime) > TWO_HOURS_MS;
|
||||
|
||||
if (isStale) {
|
||||
console.warn(
|
||||
`[Idempotency] Repository ${repo.name} has been in "${repo.status}" status for over 2 hours. ` +
|
||||
`Considering it stale and allowing retry.`
|
||||
);
|
||||
}
|
||||
|
||||
return !isStale;
|
||||
});
|
||||
|
||||
if (activeRepos.length === 0) {
|
||||
console.log(
|
||||
`[Idempotency] All in-progress operations for ${repoName} are stale (>2h). Allowing retry.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have an expected location, verify it matches
|
||||
if (expectedLocation) {
|
||||
const matchingRepo = activeRepos.find(
|
||||
(repo) => repo.mirroredLocation === expectedLocation
|
||||
);
|
||||
if (matchingRepo) {
|
||||
console.log(
|
||||
`[Idempotency] Repository ${repoName} is already being mirrored at ${expectedLocation}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`[Idempotency] Repository ${repoName} is already being mirrored (${activeRepos.length} in-progress operations found)`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error checking if repo is currently mirroring:", error);
|
||||
console.error("Error details:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to check if a repository exists in Gitea.
|
||||
* First checks the recorded mirroredLocation, then falls back to the expected location.
|
||||
@@ -276,11 +366,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) {
|
||||
// Extract GitHub owner from full_name (format: owner/repo)
|
||||
const githubOwner = repository.fullName.split('/')[0];
|
||||
|
||||
|
||||
targetRepoName = await generateUniqueRepoName({
|
||||
config,
|
||||
orgName: repoOwner,
|
||||
@@ -288,7 +378,7 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
githubOwner,
|
||||
strategy: config.githubConfig.starredDuplicateStrategy,
|
||||
});
|
||||
|
||||
|
||||
if (targetRepoName !== repository.name) {
|
||||
console.log(
|
||||
`Starred repo ${repository.fullName} will be mirrored as ${repoOwner}/${targetRepoName} to avoid naming conflict`
|
||||
@@ -296,6 +386,23 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
}
|
||||
}
|
||||
|
||||
// IDEMPOTENCY CHECK: Check if this repo is already being mirrored
|
||||
const expectedLocation = `${repoOwner}/${targetRepoName}`;
|
||||
const isCurrentlyMirroring = await isRepoCurrentlyMirroring({
|
||||
config,
|
||||
repoName: targetRepoName,
|
||||
expectedLocation,
|
||||
});
|
||||
|
||||
if (isCurrentlyMirroring) {
|
||||
console.log(
|
||||
`[Idempotency] Skipping ${repository.fullName} - already being mirrored to ${expectedLocation}`
|
||||
);
|
||||
|
||||
// Don't throw an error, just return to allow other repos to continue
|
||||
return;
|
||||
}
|
||||
|
||||
const isExisting = await isRepoPresentInGitea({
|
||||
config,
|
||||
owner: repoOwner,
|
||||
@@ -337,11 +444,30 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
|
||||
console.log(`Mirroring repository ${repository.name}`);
|
||||
|
||||
// DOUBLE-CHECK: Final idempotency check right before updating status
|
||||
// This catches race conditions in the small window between first check and status update
|
||||
const finalCheck = await isRepoCurrentlyMirroring({
|
||||
config,
|
||||
repoName: targetRepoName,
|
||||
expectedLocation,
|
||||
});
|
||||
|
||||
if (finalCheck) {
|
||||
console.log(
|
||||
`[Idempotency] Race condition detected - ${repository.fullName} is now being mirrored by another process. Skipping.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark repos as "mirroring" in DB
|
||||
// CRITICAL: Set mirroredLocation NOW (not after success) so idempotency checks work
|
||||
// This becomes the "target location" - where we intend to mirror to
|
||||
// Without this, the idempotency check can't detect concurrent operations on first mirror
|
||||
await db
|
||||
.update(repositories)
|
||||
.set({
|
||||
status: repoStatusEnum.parse("mirroring"),
|
||||
mirroredLocation: expectedLocation,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(repositories.id, repository.id!));
|
||||
@@ -423,12 +549,16 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
// Prepare migration payload
|
||||
// For private repos, use separate auth fields instead of embedding credentials in URL
|
||||
// This is required for Forgejo 12+ which rejects URLs with embedded credentials
|
||||
// Skip wiki for starred repos if starredCodeOnly is enabled
|
||||
const shouldMirrorWiki = config.giteaConfig?.wiki &&
|
||||
!(repository.isStarred && config.githubConfig?.starredCodeOnly);
|
||||
|
||||
const migratePayload: any = {
|
||||
clone_addr: cloneAddress,
|
||||
repo_name: targetRepoName,
|
||||
mirror: true,
|
||||
mirror_interval: config.giteaConfig?.mirrorInterval || "8h",
|
||||
wiki: config.giteaConfig?.wiki || false,
|
||||
wiki: shouldMirrorWiki || false,
|
||||
lfs: config.giteaConfig?.lfs || false,
|
||||
private: repository.isPrivate,
|
||||
repo_owner: repoOwner,
|
||||
@@ -457,8 +587,13 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
);
|
||||
|
||||
//mirror releases
|
||||
console.log(`[Metadata] Release mirroring check: mirrorReleases=${config.giteaConfig?.mirrorReleases}`);
|
||||
if (config.giteaConfig?.mirrorReleases) {
|
||||
// Skip releases for starred repos if starredCodeOnly is enabled
|
||||
const shouldMirrorReleases = config.giteaConfig?.mirrorReleases &&
|
||||
!(repository.isStarred && config.githubConfig?.starredCodeOnly);
|
||||
|
||||
console.log(`[Metadata] Release mirroring check: mirrorReleases=${config.giteaConfig?.mirrorReleases}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorReleases=${shouldMirrorReleases}`);
|
||||
|
||||
if (shouldMirrorReleases) {
|
||||
try {
|
||||
await mirrorGitHubReleasesToGitea({
|
||||
config,
|
||||
@@ -475,11 +610,11 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
}
|
||||
|
||||
// clone issues
|
||||
// Skip issues for starred repos if skipStarredIssues is enabled
|
||||
// Skip issues for starred repos if starredCodeOnly is enabled
|
||||
const shouldMirrorIssues = config.giteaConfig?.mirrorIssues &&
|
||||
!(repository.isStarred && config.githubConfig?.skipStarredIssues);
|
||||
!(repository.isStarred && config.githubConfig?.starredCodeOnly);
|
||||
|
||||
console.log(`[Metadata] Issue mirroring check: mirrorIssues=${config.giteaConfig?.mirrorIssues}, isStarred=${repository.isStarred}, skipStarredIssues=${config.githubConfig?.skipStarredIssues}, shouldMirrorIssues=${shouldMirrorIssues}`);
|
||||
console.log(`[Metadata] Issue mirroring check: mirrorIssues=${config.giteaConfig?.mirrorIssues}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorIssues=${shouldMirrorIssues}`);
|
||||
|
||||
if (shouldMirrorIssues) {
|
||||
try {
|
||||
@@ -498,8 +633,13 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
}
|
||||
|
||||
// Mirror pull requests if enabled
|
||||
console.log(`[Metadata] Pull request mirroring check: mirrorPullRequests=${config.giteaConfig?.mirrorPullRequests}`);
|
||||
if (config.giteaConfig?.mirrorPullRequests) {
|
||||
// Skip pull requests for starred repos if starredCodeOnly is enabled
|
||||
const shouldMirrorPullRequests = config.giteaConfig?.mirrorPullRequests &&
|
||||
!(repository.isStarred && config.githubConfig?.starredCodeOnly);
|
||||
|
||||
console.log(`[Metadata] Pull request mirroring check: mirrorPullRequests=${config.giteaConfig?.mirrorPullRequests}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorPullRequests=${shouldMirrorPullRequests}`);
|
||||
|
||||
if (shouldMirrorPullRequests) {
|
||||
try {
|
||||
await mirrorGitRepoPullRequestsToGitea({
|
||||
config,
|
||||
@@ -516,8 +656,13 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
}
|
||||
|
||||
// Mirror labels if enabled (and not already done via issues)
|
||||
console.log(`[Metadata] Label mirroring check: mirrorLabels=${config.giteaConfig?.mirrorLabels}, shouldMirrorIssues=${shouldMirrorIssues}`);
|
||||
if (config.giteaConfig?.mirrorLabels && !shouldMirrorIssues) {
|
||||
// Skip labels for starred repos if starredCodeOnly is enabled
|
||||
const shouldMirrorLabels = config.giteaConfig?.mirrorLabels && !shouldMirrorIssues &&
|
||||
!(repository.isStarred && config.githubConfig?.starredCodeOnly);
|
||||
|
||||
console.log(`[Metadata] Label mirroring check: mirrorLabels=${config.giteaConfig?.mirrorLabels}, shouldMirrorIssues=${shouldMirrorIssues}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorLabels=${shouldMirrorLabels}`);
|
||||
|
||||
if (shouldMirrorLabels) {
|
||||
try {
|
||||
await mirrorGitRepoLabelsToGitea({
|
||||
config,
|
||||
@@ -534,8 +679,13 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
}
|
||||
|
||||
// Mirror milestones if enabled
|
||||
console.log(`[Metadata] Milestone mirroring check: mirrorMilestones=${config.giteaConfig?.mirrorMilestones}`);
|
||||
if (config.giteaConfig?.mirrorMilestones) {
|
||||
// Skip milestones for starred repos if starredCodeOnly is enabled
|
||||
const shouldMirrorMilestones = config.giteaConfig?.mirrorMilestones &&
|
||||
!(repository.isStarred && config.githubConfig?.starredCodeOnly);
|
||||
|
||||
console.log(`[Metadata] Milestone mirroring check: mirrorMilestones=${config.giteaConfig?.mirrorMilestones}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorMilestones=${shouldMirrorMilestones}`);
|
||||
|
||||
if (shouldMirrorMilestones) {
|
||||
try {
|
||||
await mirrorGitRepoMilestonesToGitea({
|
||||
config,
|
||||
@@ -657,32 +807,32 @@ async function generateUniqueRepoName({
|
||||
strategy?: string;
|
||||
}): Promise<string> {
|
||||
const duplicateStrategy = strategy || "suffix";
|
||||
|
||||
|
||||
// First check if base name is available
|
||||
const baseExists = await isRepoPresentInGitea({
|
||||
config,
|
||||
owner: orgName,
|
||||
repoName: baseName,
|
||||
});
|
||||
|
||||
|
||||
if (!baseExists) {
|
||||
return baseName;
|
||||
}
|
||||
|
||||
|
||||
// Generate name based on strategy
|
||||
let candidateName: string;
|
||||
let attempt = 0;
|
||||
const maxAttempts = 10;
|
||||
|
||||
|
||||
while (attempt < maxAttempts) {
|
||||
switch (duplicateStrategy) {
|
||||
case "prefix":
|
||||
// Prefix with owner: owner-reponame
|
||||
candidateName = attempt === 0
|
||||
candidateName = attempt === 0
|
||||
? `${githubOwner}-${baseName}`
|
||||
: `${githubOwner}-${baseName}-${attempt}`;
|
||||
break;
|
||||
|
||||
|
||||
case "owner-org":
|
||||
// This would require creating sub-organizations, not supported in this PR
|
||||
// Fall back to suffix strategy
|
||||
@@ -694,24 +844,31 @@ async function generateUniqueRepoName({
|
||||
: `${baseName}-${githubOwner}-${attempt}`;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
const exists = await isRepoPresentInGitea({
|
||||
config,
|
||||
owner: orgName,
|
||||
repoName: candidateName,
|
||||
});
|
||||
|
||||
|
||||
if (!exists) {
|
||||
console.log(`Found unique name for duplicate starred repo: ${candidateName}`);
|
||||
return candidateName;
|
||||
}
|
||||
|
||||
|
||||
attempt++;
|
||||
}
|
||||
|
||||
// If all attempts failed, use timestamp as last resort
|
||||
const timestamp = Date.now();
|
||||
return `${baseName}-${githubOwner}-${timestamp}`;
|
||||
|
||||
// SECURITY FIX: Prevent infinite duplicate creation
|
||||
// Instead of falling back to timestamp (which creates infinite duplicates),
|
||||
// throw an error to prevent hundreds of duplicate repos
|
||||
console.error(`Failed to find unique name for ${baseName} after ${maxAttempts} attempts`);
|
||||
console.error(`Organization: ${orgName}, GitHub Owner: ${githubOwner}, Strategy: ${duplicateStrategy}`);
|
||||
throw new Error(
|
||||
`Unable to generate unique repository name for "${baseName}". ` +
|
||||
`All ${maxAttempts} naming attempts resulted in conflicts. ` +
|
||||
`Please manually resolve the naming conflict or adjust your duplicate strategy.`
|
||||
);
|
||||
}
|
||||
|
||||
export async function mirrorGitHubRepoToGiteaOrg({
|
||||
@@ -741,11 +898,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) {
|
||||
// Extract GitHub owner from full_name (format: owner/repo)
|
||||
const githubOwner = repository.fullName.split('/')[0];
|
||||
|
||||
|
||||
targetRepoName = await generateUniqueRepoName({
|
||||
config,
|
||||
orgName,
|
||||
@@ -753,7 +910,7 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
githubOwner,
|
||||
strategy: config.githubConfig.starredDuplicateStrategy,
|
||||
});
|
||||
|
||||
|
||||
if (targetRepoName !== repository.name) {
|
||||
console.log(
|
||||
`Starred repo ${repository.fullName} will be mirrored as ${orgName}/${targetRepoName} to avoid naming conflict`
|
||||
@@ -761,6 +918,23 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
}
|
||||
}
|
||||
|
||||
// IDEMPOTENCY CHECK: Check if this repo is already being mirrored
|
||||
const expectedLocation = `${orgName}/${targetRepoName}`;
|
||||
const isCurrentlyMirroring = await isRepoCurrentlyMirroring({
|
||||
config,
|
||||
repoName: targetRepoName,
|
||||
expectedLocation,
|
||||
});
|
||||
|
||||
if (isCurrentlyMirroring) {
|
||||
console.log(
|
||||
`[Idempotency] Skipping ${repository.fullName} - already being mirrored to ${expectedLocation}`
|
||||
);
|
||||
|
||||
// Don't throw an error, just return to allow other repos to continue
|
||||
return;
|
||||
}
|
||||
|
||||
const isExisting = await isRepoPresentInGitea({
|
||||
config,
|
||||
owner: orgName,
|
||||
@@ -807,11 +981,30 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
// Use clean clone URL without embedded credentials (Forgejo 12+ security requirement)
|
||||
const cloneAddress = repository.cloneUrl;
|
||||
|
||||
// DOUBLE-CHECK: Final idempotency check right before updating status
|
||||
// This catches race conditions in the small window between first check and status update
|
||||
const finalCheck = await isRepoCurrentlyMirroring({
|
||||
config,
|
||||
repoName: targetRepoName,
|
||||
expectedLocation,
|
||||
});
|
||||
|
||||
if (finalCheck) {
|
||||
console.log(
|
||||
`[Idempotency] Race condition detected - ${repository.fullName} is now being mirrored by another process. Skipping.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark repos as "mirroring" in DB
|
||||
// CRITICAL: Set mirroredLocation NOW (not after success) so idempotency checks work
|
||||
// This becomes the "target location" - where we intend to mirror to
|
||||
// Without this, the idempotency check can't detect concurrent operations on first mirror
|
||||
await db
|
||||
.update(repositories)
|
||||
.set({
|
||||
status: repoStatusEnum.parse("mirroring"),
|
||||
mirroredLocation: expectedLocation,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(repositories.id, repository.id!));
|
||||
@@ -824,13 +1017,17 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
// Prepare migration payload
|
||||
// For private repos, use separate auth fields instead of embedding credentials in URL
|
||||
// This is required for Forgejo 12+ which rejects URLs with embedded credentials
|
||||
// Skip wiki for starred repos if starredCodeOnly is enabled
|
||||
const shouldMirrorWiki = config.giteaConfig?.wiki &&
|
||||
!(repository.isStarred && config.githubConfig?.starredCodeOnly);
|
||||
|
||||
const migratePayload: any = {
|
||||
clone_addr: cloneAddress,
|
||||
uid: giteaOrgId,
|
||||
repo_name: targetRepoName,
|
||||
mirror: true,
|
||||
mirror_interval: config.giteaConfig?.mirrorInterval || "8h",
|
||||
wiki: config.giteaConfig?.wiki || false,
|
||||
wiki: shouldMirrorWiki || false,
|
||||
lfs: config.giteaConfig?.lfs || false,
|
||||
private: repository.isPrivate,
|
||||
};
|
||||
@@ -856,8 +1053,13 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
);
|
||||
|
||||
//mirror releases
|
||||
console.log(`[Metadata] Release mirroring check: mirrorReleases=${config.giteaConfig?.mirrorReleases}`);
|
||||
if (config.giteaConfig?.mirrorReleases) {
|
||||
// Skip releases for starred repos if starredCodeOnly is enabled
|
||||
const shouldMirrorReleases = config.giteaConfig?.mirrorReleases &&
|
||||
!(repository.isStarred && config.githubConfig?.starredCodeOnly);
|
||||
|
||||
console.log(`[Metadata] Release mirroring check: mirrorReleases=${config.giteaConfig?.mirrorReleases}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorReleases=${shouldMirrorReleases}`);
|
||||
|
||||
if (shouldMirrorReleases) {
|
||||
try {
|
||||
await mirrorGitHubReleasesToGitea({
|
||||
config,
|
||||
@@ -874,11 +1076,11 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
}
|
||||
|
||||
// Clone issues
|
||||
// Skip issues for starred repos if skipStarredIssues is enabled
|
||||
// Skip issues for starred repos if starredCodeOnly is enabled
|
||||
const shouldMirrorIssues = config.giteaConfig?.mirrorIssues &&
|
||||
!(repository.isStarred && config.githubConfig?.skipStarredIssues);
|
||||
!(repository.isStarred && config.githubConfig?.starredCodeOnly);
|
||||
|
||||
console.log(`[Metadata] Issue mirroring check: mirrorIssues=${config.giteaConfig?.mirrorIssues}, isStarred=${repository.isStarred}, skipStarredIssues=${config.githubConfig?.skipStarredIssues}, shouldMirrorIssues=${shouldMirrorIssues}`);
|
||||
console.log(`[Metadata] Issue mirroring check: mirrorIssues=${config.giteaConfig?.mirrorIssues}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorIssues=${shouldMirrorIssues}`);
|
||||
|
||||
if (shouldMirrorIssues) {
|
||||
try {
|
||||
@@ -897,8 +1099,13 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
}
|
||||
|
||||
// Mirror pull requests if enabled
|
||||
console.log(`[Metadata] Pull request mirroring check: mirrorPullRequests=${config.giteaConfig?.mirrorPullRequests}`);
|
||||
if (config.giteaConfig?.mirrorPullRequests) {
|
||||
// Skip pull requests for starred repos if starredCodeOnly is enabled
|
||||
const shouldMirrorPullRequests = config.giteaConfig?.mirrorPullRequests &&
|
||||
!(repository.isStarred && config.githubConfig?.starredCodeOnly);
|
||||
|
||||
console.log(`[Metadata] Pull request mirroring check: mirrorPullRequests=${config.giteaConfig?.mirrorPullRequests}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorPullRequests=${shouldMirrorPullRequests}`);
|
||||
|
||||
if (shouldMirrorPullRequests) {
|
||||
try {
|
||||
await mirrorGitRepoPullRequestsToGitea({
|
||||
config,
|
||||
@@ -915,8 +1122,13 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
}
|
||||
|
||||
// Mirror labels if enabled (and not already done via issues)
|
||||
console.log(`[Metadata] Label mirroring check: mirrorLabels=${config.giteaConfig?.mirrorLabels}, shouldMirrorIssues=${shouldMirrorIssues}`);
|
||||
if (config.giteaConfig?.mirrorLabels && !shouldMirrorIssues) {
|
||||
// Skip labels for starred repos if starredCodeOnly is enabled
|
||||
const shouldMirrorLabels = config.giteaConfig?.mirrorLabels && !shouldMirrorIssues &&
|
||||
!(repository.isStarred && config.githubConfig?.starredCodeOnly);
|
||||
|
||||
console.log(`[Metadata] Label mirroring check: mirrorLabels=${config.giteaConfig?.mirrorLabels}, shouldMirrorIssues=${shouldMirrorIssues}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorLabels=${shouldMirrorLabels}`);
|
||||
|
||||
if (shouldMirrorLabels) {
|
||||
try {
|
||||
await mirrorGitRepoLabelsToGitea({
|
||||
config,
|
||||
@@ -933,8 +1145,13 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
}
|
||||
|
||||
// Mirror milestones if enabled
|
||||
console.log(`[Metadata] Milestone mirroring check: mirrorMilestones=${config.giteaConfig?.mirrorMilestones}`);
|
||||
if (config.giteaConfig?.mirrorMilestones) {
|
||||
// Skip milestones for starred repos if starredCodeOnly is enabled
|
||||
const shouldMirrorMilestones = config.giteaConfig?.mirrorMilestones &&
|
||||
!(repository.isStarred && config.githubConfig?.starredCodeOnly);
|
||||
|
||||
console.log(`[Metadata] Milestone mirroring check: mirrorMilestones=${config.giteaConfig?.mirrorMilestones}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorMilestones=${shouldMirrorMilestones}`);
|
||||
|
||||
if (shouldMirrorMilestones) {
|
||||
try {
|
||||
await mirrorGitRepoMilestonesToGitea({
|
||||
config,
|
||||
|
||||
@@ -54,7 +54,7 @@ export function mapUiToDbConfig(
|
||||
defaultOrg: giteaConfig.organization,
|
||||
|
||||
// Advanced options
|
||||
skipStarredIssues: advancedOptions.skipStarredIssues,
|
||||
starredCodeOnly: advancedOptions.starredCodeOnly,
|
||||
};
|
||||
|
||||
// Map Gitea config to match database schema
|
||||
@@ -152,7 +152,8 @@ export function mapDbToUiConfig(dbConfig: any): {
|
||||
// Map advanced options
|
||||
const advancedOptions: AdvancedOptions = {
|
||||
skipForks: !(dbConfig.githubConfig?.includeForks ?? true), // Invert includeForks to get skipForks
|
||||
skipStarredIssues: dbConfig.githubConfig?.skipStarredIssues || false,
|
||||
// Support both old (skipStarredIssues) and new (starredCodeOnly) field names for backward compatibility
|
||||
starredCodeOnly: dbConfig.githubConfig?.starredCodeOnly ?? (dbConfig.githubConfig as any)?.skipStarredIssues ?? false,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -55,7 +55,7 @@ export interface MirrorOptions {
|
||||
|
||||
export interface AdvancedOptions {
|
||||
skipForks: boolean;
|
||||
skipStarredIssues: boolean;
|
||||
starredCodeOnly: boolean;
|
||||
}
|
||||
|
||||
export interface SaveConfigApiRequest {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { RepoStatus } from "./Repository";
|
||||
export const membershipRoleEnum = z.enum([
|
||||
"member",
|
||||
"admin",
|
||||
"owner",
|
||||
"billing_manager",
|
||||
]);
|
||||
|
||||
|
||||
@@ -9,28 +9,28 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.3.4",
|
||||
"@astrojs/react": "^4.3.0",
|
||||
"@astrojs/mdx": "^4.3.6",
|
||||
"@astrojs/react": "^4.4.0",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@splinetool/react-spline": "^4.1.0",
|
||||
"@splinetool/runtime": "^1.10.52",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@splinetool/runtime": "^1.10.73",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"astro": "^5.13.4",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"astro": "^5.14.3",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.542.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"lucide-react": "^0.544.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.12"
|
||||
"tailwindcss": "^4.1.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tw-animate-css": "^1.3.7"
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.15.0"
|
||||
"packageManager": "pnpm@10.18.0"
|
||||
}
|
||||
1622
www/pnpm-lock.yaml
generated
1622
www/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user