Compare commits

...

6 Commits

Author SHA1 Message Date
Arunavo Ray
e24b856416 chore: bump version to 2.12.0
- Fixed SQLite 'no such table: mirror_jobs' error during application startup
- Implemented automatic database table creation during database initialization
- Resolved database schema inconsistencies between development and production environments
- Enhanced database initialization process with automatic table creation and indexing
- Added comprehensive error handling for database table creation
- Integrated database repair functionality into application startup for better reliability
2025-06-02 15:08:10 +05:30
Arunavo Ray
612805f030 feat: add table creation and existence check for database initialization 2025-06-02 15:05:20 +05:30
Arunavo Ray
7705dffee0 chore: bump version to 2.11.2 2025-05-28 20:29:23 +05:30
Arunavo Ray
3dceb34174 feat: replace SiGitea icons with custom logo
- Replace SiGitea icon with custom logo.svg in LoginForm and Header components
- Add custom logo.svg file with theme-aware styling
- Update favicon.svg to use the same custom logo design
- Remove unused SiGitea imports and clean up dependencies
- Logo automatically adapts to light/dark themes via CSS media queries
2025-05-28 20:28:59 +05:30
Arunavo Ray
6b747ba891 chore: bump version to 2.11.1 2025-05-28 19:58:46 +05:30
Arunavo Ray
ddd67faeab fix: resolve repository mirroring status inconsistencies
- Fix 'already exists, skipping migration' logic that left repositories with incorrect 'imported' status
- Update database status to 'mirrored' when repository already exists in Gitea
- Add automatic startup repair to fix existing inconsistencies on container start
- Create diagnostic and repair tools for troubleshooting mirroring issues
- Ensure consistent state between Gitea and application database

Resolves issue where repositories showed successful mirroring logs but remained
in 'imported' status, causing UI confusion and preventing proper status tracking.

Changes:
- src/lib/gitea.ts: Fixed mirrorGithubRepoToGitea() and mirrorGitHubRepoToGiteaOrg()
- docker-entrypoint.sh: Added automatic repository status repair on startup
- scripts/investigate-repo.ts: New diagnostic tool for repository analysis
- scripts/repair-mirrored-repos.ts: New repair tool with startup mode support
- scripts/cleanup-duplicate-repos.ts: New tool for removing duplicate entries

Fixes multiple user reports of misleading 'successfully mirrored' logs
while repositories remained in inconsistent state.
2025-05-28 19:58:15 +05:30
12 changed files with 1286 additions and 18 deletions

View File

@@ -5,6 +5,18 @@ All notable changes to the Gitea Mirror project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.12.0] - 2025-01-27
### Fixed
- Fixed SQLite "no such table: mirror_jobs" error during application startup
- Implemented automatic database table creation during database initialization
- Resolved database schema inconsistencies between development and production environments
### Improved
- Enhanced database initialization process with automatic table creation and indexing
- Added comprehensive error handling for database table creation
- Integrated database repair functionality into application startup for better reliability
## [2.5.3] - 2025-05-22
### Added

View File

@@ -232,6 +232,28 @@ else
echo "❌ Startup recovery failed with exit code $RECOVERY_EXIT_CODE"
fi
# Run repository status repair to fix any inconsistent mirroring states
echo "Running repository status repair..."
if [ -f "dist/scripts/repair-mirrored-repos.js" ]; then
echo "Running repository repair using compiled script..."
bun dist/scripts/repair-mirrored-repos.js --startup
REPAIR_EXIT_CODE=$?
elif [ -f "scripts/repair-mirrored-repos.ts" ]; then
echo "Running repository repair using TypeScript script..."
bun scripts/repair-mirrored-repos.ts --startup
REPAIR_EXIT_CODE=$?
else
echo "Warning: Repository repair script not found. Skipping repair."
REPAIR_EXIT_CODE=0
fi
# Log repair result
if [ $REPAIR_EXIT_CODE -eq 0 ]; then
echo "✅ Repository status repair completed successfully"
else
echo "⚠️ Repository status repair completed with warnings (exit code $REPAIR_EXIT_CODE)"
fi
# Function to handle shutdown signals
shutdown_handler() {
echo "🛑 Received shutdown signal, forwarding to application..."

View File

@@ -1,7 +1,7 @@
{
"name": "gitea-mirror",
"type": "module",
"version": "2.11.0",
"version": "2.12.0",
"engines": {
"bun": ">=1.2.9"
},

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 749 B

After

Width:  |  Height:  |  Size: 21 KiB

200
public/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,129 @@
#!/usr/bin/env bun
/**
* Script to find and clean up duplicate repositories in the database
* Keeps the most recent entry and removes older duplicates
*
* Usage: bun scripts/cleanup-duplicate-repos.ts [--dry-run] [--repo-name=<name>]
*/
import { db, repositories, mirrorJobs } from "@/lib/db";
import { eq, and, desc } from "drizzle-orm";
const isDryRun = process.argv.includes("--dry-run");
const specificRepo = process.argv.find(arg => arg.startsWith("--repo-name="))?.split("=")[1];
async function findDuplicateRepositories() {
console.log("🔍 Finding duplicate repositories");
console.log("=" .repeat(40));
if (isDryRun) {
console.log("🔍 DRY RUN MODE - No changes will be made");
console.log("");
}
if (specificRepo) {
console.log(`🎯 Targeting specific repository: ${specificRepo}`);
console.log("");
}
try {
// Find all repositories, grouped by name and fullName
let allRepos = await db.select().from(repositories);
if (specificRepo) {
allRepos = allRepos.filter(repo => repo.name === specificRepo);
}
// Group repositories by name and fullName
const repoGroups = new Map<string, typeof allRepos>();
for (const repo of allRepos) {
const key = `${repo.name}|${repo.fullName}`;
if (!repoGroups.has(key)) {
repoGroups.set(key, []);
}
repoGroups.get(key)!.push(repo);
}
// Find groups with duplicates
const duplicateGroups = Array.from(repoGroups.entries())
.filter(([_, repos]) => repos.length > 1);
if (duplicateGroups.length === 0) {
console.log("✅ No duplicate repositories found");
return;
}
console.log(`📋 Found ${duplicateGroups.length} sets of duplicate repositories:`);
console.log("");
let totalDuplicates = 0;
let totalRemoved = 0;
for (const [key, repos] of duplicateGroups) {
const [name, fullName] = key.split("|");
console.log(`🔄 Processing duplicates for: ${name} (${fullName})`);
console.log(` Found ${repos.length} entries:`);
// Sort by updatedAt descending to keep the most recent
repos.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
const keepRepo = repos[0];
const removeRepos = repos.slice(1);
console.log(` ✅ Keeping: ID ${keepRepo.id} (Status: ${keepRepo.status}, Updated: ${new Date(keepRepo.updatedAt).toISOString()})`);
for (const repo of removeRepos) {
console.log(` ❌ Removing: ID ${repo.id} (Status: ${repo.status}, Updated: ${new Date(repo.updatedAt).toISOString()})`);
if (!isDryRun) {
try {
// First, delete related mirror jobs
await db
.delete(mirrorJobs)
.where(eq(mirrorJobs.repositoryId, repo.id!));
// Then delete the repository
await db
.delete(repositories)
.where(eq(repositories.id, repo.id!));
console.log(` 🗑️ Deleted repository and related mirror jobs`);
totalRemoved++;
} catch (error) {
console.log(` ❌ Error deleting repository: ${error instanceof Error ? error.message : String(error)}`);
}
} else {
console.log(` 🗑️ Would delete repository and related mirror jobs`);
totalRemoved++;
}
}
totalDuplicates += removeRepos.length;
console.log("");
}
console.log("📊 Cleanup Summary:");
console.log(` Duplicate sets found: ${duplicateGroups.length}`);
console.log(` Total duplicates: ${totalDuplicates}`);
console.log(` ${isDryRun ? 'Would remove' : 'Removed'}: ${totalRemoved}`);
if (isDryRun && totalRemoved > 0) {
console.log("");
console.log("💡 To apply these changes, run the script without --dry-run");
}
} catch (error) {
console.error("❌ Error during cleanup process:", error);
}
}
// Run the cleanup
findDuplicateRepositories().then(() => {
console.log("Cleanup process complete.");
process.exit(0);
}).catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});

178
scripts/investigate-repo.ts Normal file
View File

@@ -0,0 +1,178 @@
#!/usr/bin/env bun
/**
* Script to investigate a specific repository's mirroring status
* Usage: bun scripts/investigate-repo.ts [repository-name]
*/
import { db, repositories, mirrorJobs, configs } from "@/lib/db";
import { eq, desc, and } from "drizzle-orm";
const repoName = process.argv[2] || "EruditionPaper";
async function investigateRepository() {
console.log(`🔍 Investigating repository: ${repoName}`);
console.log("=" .repeat(50));
try {
// Find the repository in the database
const repos = await db
.select()
.from(repositories)
.where(eq(repositories.name, repoName));
if (repos.length === 0) {
console.log(`❌ Repository "${repoName}" not found in database`);
return;
}
const repo = repos[0];
console.log(`✅ Found repository: ${repo.name}`);
console.log(` ID: ${repo.id}`);
console.log(` Full Name: ${repo.fullName}`);
console.log(` Owner: ${repo.owner}`);
console.log(` Organization: ${repo.organization || "None"}`);
console.log(` Status: ${repo.status}`);
console.log(` Is Private: ${repo.isPrivate}`);
console.log(` Is Forked: ${repo.isForked}`);
console.log(` Mirrored Location: ${repo.mirroredLocation || "Not set"}`);
console.log(` Last Mirrored: ${repo.lastMirrored ? new Date(repo.lastMirrored).toISOString() : "Never"}`);
console.log(` Error Message: ${repo.errorMessage || "None"}`);
console.log(` Created At: ${new Date(repo.createdAt).toISOString()}`);
console.log(` Updated At: ${new Date(repo.updatedAt).toISOString()}`);
console.log("\n📋 Recent Mirror Jobs:");
console.log("-".repeat(30));
// Find recent mirror jobs for this repository
const jobs = await db
.select()
.from(mirrorJobs)
.where(eq(mirrorJobs.repositoryId, repo.id))
.orderBy(desc(mirrorJobs.timestamp))
.limit(10);
if (jobs.length === 0) {
console.log(" No mirror jobs found for this repository");
} else {
jobs.forEach((job, index) => {
console.log(` ${index + 1}. ${new Date(job.timestamp).toISOString()}`);
console.log(` Status: ${job.status}`);
console.log(` Message: ${job.message}`);
if (job.details) {
console.log(` Details: ${job.details}`);
}
console.log("");
});
}
// Get user configuration
console.log("⚙️ User Configuration:");
console.log("-".repeat(20));
const config = await db
.select()
.from(configs)
.where(eq(configs.id, repo.configId))
.limit(1);
if (config.length > 0) {
const userConfig = config[0];
console.log(` User ID: ${userConfig.userId}`);
console.log(` GitHub Username: ${userConfig.githubConfig?.username || "Not set"}`);
console.log(` Gitea URL: ${userConfig.giteaConfig?.url || "Not set"}`);
console.log(` Gitea Username: ${userConfig.giteaConfig?.username || "Not set"}`);
console.log(` Preserve Org Structure: ${userConfig.githubConfig?.preserveOrgStructure || false}`);
console.log(` Mirror Issues: ${userConfig.githubConfig?.mirrorIssues || false}`);
}
// Check for any active jobs
console.log("\n🔄 Active Jobs:");
console.log("-".repeat(15));
const activeJobs = await db
.select()
.from(mirrorJobs)
.where(
and(
eq(mirrorJobs.repositoryId, repo.id),
eq(mirrorJobs.inProgress, true)
)
);
if (activeJobs.length === 0) {
console.log(" No active jobs found");
} else {
activeJobs.forEach((job, index) => {
console.log(` ${index + 1}. Job ID: ${job.id}`);
console.log(` Type: ${job.jobType || "mirror"}`);
console.log(` Batch ID: ${job.batchId || "None"}`);
console.log(` Started: ${job.startedAt ? new Date(job.startedAt).toISOString() : "Unknown"}`);
console.log(` Last Checkpoint: ${job.lastCheckpoint ? new Date(job.lastCheckpoint).toISOString() : "None"}`);
console.log(` Progress: ${job.completedItems || 0}/${job.totalItems || 0}`);
console.log("");
});
}
// Check if repository exists in Gitea
if (config.length > 0) {
const userConfig = config[0];
console.log("\n🔗 Gitea Repository Check:");
console.log("-".repeat(25));
try {
const giteaUrl = userConfig.giteaConfig?.url;
const giteaToken = userConfig.giteaConfig?.token;
const giteaUsername = userConfig.giteaConfig?.username;
if (giteaUrl && giteaToken && giteaUsername) {
const checkUrl = `${giteaUrl}/api/v1/repos/${giteaUsername}/${repo.name}`;
console.log(` Checking: ${checkUrl}`);
const response = await fetch(checkUrl, {
headers: {
Authorization: `token ${giteaToken}`,
},
});
console.log(` Response Status: ${response.status} ${response.statusText}`);
if (response.ok) {
const repoData = await response.json();
console.log(` ✅ Repository exists in Gitea`);
console.log(` Name: ${repoData.name}`);
console.log(` Full Name: ${repoData.full_name}`);
console.log(` Private: ${repoData.private}`);
console.log(` Mirror: ${repoData.mirror}`);
console.log(` Clone URL: ${repoData.clone_url}`);
console.log(` Created: ${new Date(repoData.created_at).toISOString()}`);
console.log(` Updated: ${new Date(repoData.updated_at).toISOString()}`);
if (repoData.mirror_updated) {
console.log(` Mirror Updated: ${new Date(repoData.mirror_updated).toISOString()}`);
}
} else {
console.log(` ❌ Repository not found in Gitea`);
const errorText = await response.text();
console.log(` Error: ${errorText}`);
}
} else {
console.log(" ⚠️ Missing Gitea configuration");
}
} catch (error) {
console.log(` ❌ Error checking Gitea: ${error instanceof Error ? error.message : String(error)}`);
}
}
} catch (error) {
console.error("❌ Error investigating repository:", error);
}
}
// Run the investigation
investigateRepository().then(() => {
console.log("Investigation complete.");
process.exit(0);
}).catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});

View File

@@ -0,0 +1,277 @@
#!/usr/bin/env bun
/**
* Script to repair repositories that exist in Gitea but have incorrect status in the database
* This fixes the issue where repositories show as "imported" but are actually mirrored in Gitea
*
* Usage: bun scripts/repair-mirrored-repos.ts [--dry-run] [--repo-name=<name>]
*/
import { db, repositories, configs } from "@/lib/db";
import { eq, and, or } from "drizzle-orm";
import { createMirrorJob } from "@/lib/helpers";
import { repoStatusEnum } from "@/types/Repository";
const isDryRun = process.argv.includes("--dry-run");
const specificRepo = process.argv.find(arg => arg.startsWith("--repo-name="))?.split("=")[1];
const isStartupMode = process.argv.includes("--startup");
async function checkRepoInGitea(config: any, owner: string, repoName: string): Promise<boolean> {
try {
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
return false;
}
const response = await fetch(
`${config.giteaConfig.url}/api/v1/repos/${owner}/${repoName}`,
{
headers: {
Authorization: `token ${config.giteaConfig.token}`,
},
}
);
return response.ok;
} catch (error) {
console.error(`Error checking repo ${owner}/${repoName} in Gitea:`, error);
return false;
}
}
async function getRepoDetailsFromGitea(config: any, owner: string, repoName: string): Promise<any> {
try {
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
return null;
}
const response = await fetch(
`${config.giteaConfig.url}/api/v1/repos/${owner}/${repoName}`,
{
headers: {
Authorization: `token ${config.giteaConfig.token}`,
},
}
);
if (response.ok) {
return await response.json();
}
return null;
} catch (error) {
console.error(`Error getting repo details for ${owner}/${repoName}:`, error);
return null;
}
}
async function repairMirroredRepositories() {
if (!isStartupMode) {
console.log("🔧 Repairing mirrored repositories database status");
console.log("=" .repeat(60));
if (isDryRun) {
console.log("🔍 DRY RUN MODE - No changes will be made");
console.log("");
}
if (specificRepo) {
console.log(`🎯 Targeting specific repository: ${specificRepo}`);
console.log("");
}
}
try {
// Find repositories that might need repair
let query = db
.select()
.from(repositories)
.where(
or(
eq(repositories.status, "imported"),
eq(repositories.status, "failed")
)
);
if (specificRepo) {
query = query.where(eq(repositories.name, specificRepo));
}
const repos = await query;
if (repos.length === 0) {
if (!isStartupMode) {
console.log("✅ No repositories found that need repair");
}
return;
}
if (!isStartupMode) {
console.log(`📋 Found ${repos.length} repositories to check:`);
console.log("");
}
let repairedCount = 0;
let skippedCount = 0;
let errorCount = 0;
for (const repo of repos) {
if (!isStartupMode) {
console.log(`🔍 Checking repository: ${repo.name}`);
console.log(` Current status: ${repo.status}`);
console.log(` Mirrored location: ${repo.mirroredLocation || "Not set"}`);
}
try {
// Get user configuration
const config = await db
.select()
.from(configs)
.where(eq(configs.id, repo.configId))
.limit(1);
if (config.length === 0) {
if (!isStartupMode) {
console.log(` ❌ No configuration found for repository`);
}
errorCount++;
continue;
}
const userConfig = config[0];
const giteaUsername = userConfig.giteaConfig?.username;
if (!giteaUsername) {
if (!isStartupMode) {
console.log(` ❌ No Gitea username in configuration`);
}
errorCount++;
continue;
}
// Check if repository exists in Gitea (try both user and organization)
let existsInGitea = false;
let actualOwner = giteaUsername;
let giteaRepoDetails = null;
// First check user location
existsInGitea = await checkRepoInGitea(userConfig, giteaUsername, repo.name);
if (existsInGitea) {
giteaRepoDetails = await getRepoDetailsFromGitea(userConfig, giteaUsername, repo.name);
}
// If not found in user location and repo has organization, check organization
if (!existsInGitea && repo.organization) {
existsInGitea = await checkRepoInGitea(userConfig, repo.organization, repo.name);
if (existsInGitea) {
actualOwner = repo.organization;
giteaRepoDetails = await getRepoDetailsFromGitea(userConfig, repo.organization, repo.name);
}
}
if (!existsInGitea) {
if (!isStartupMode) {
console.log(` ⏭️ Repository not found in Gitea - skipping`);
}
skippedCount++;
continue;
}
if (!isStartupMode) {
console.log(` ✅ Repository found in Gitea at: ${actualOwner}/${repo.name}`);
if (giteaRepoDetails) {
console.log(` 📊 Gitea details:`);
console.log(` Mirror: ${giteaRepoDetails.mirror}`);
console.log(` Created: ${new Date(giteaRepoDetails.created_at).toISOString()}`);
console.log(` Updated: ${new Date(giteaRepoDetails.updated_at).toISOString()}`);
if (giteaRepoDetails.mirror_updated) {
console.log(` Mirror Updated: ${new Date(giteaRepoDetails.mirror_updated).toISOString()}`);
}
}
} else if (repairedCount === 0) {
// In startup mode, only log the first repair to indicate activity
console.log(`Repairing repository status inconsistencies...`);
}
if (!isDryRun) {
// Update repository status in database
const mirrorUpdated = giteaRepoDetails?.mirror_updated
? new Date(giteaRepoDetails.mirror_updated)
: new Date();
await db
.update(repositories)
.set({
status: repoStatusEnum.parse("mirrored"),
updatedAt: new Date(),
lastMirrored: mirrorUpdated,
errorMessage: null,
mirroredLocation: `${actualOwner}/${repo.name}`,
})
.where(eq(repositories.id, repo.id!));
// Create a mirror job log entry
await createMirrorJob({
userId: userConfig.userId || "",
repositoryId: repo.id,
repositoryName: repo.name,
message: `Repository status repaired - found existing mirror in Gitea`,
details: `Repository ${repo.name} was found to already exist in Gitea at ${actualOwner}/${repo.name} and database status was updated from ${repo.status} to mirrored.`,
status: "mirrored",
});
if (!isStartupMode) {
console.log(` 🔧 Repaired: Updated status to 'mirrored'`);
}
} else {
if (!isStartupMode) {
console.log(` 🔧 Would repair: Update status from '${repo.status}' to 'mirrored'`);
}
}
repairedCount++;
} catch (error) {
if (!isStartupMode) {
console.log(` ❌ Error processing repository: ${error instanceof Error ? error.message : String(error)}`);
}
errorCount++;
}
if (!isStartupMode) {
console.log("");
}
}
if (isStartupMode) {
// In startup mode, only log if there were repairs or errors
if (repairedCount > 0) {
console.log(`Repaired ${repairedCount} repository status inconsistencies`);
}
if (errorCount > 0) {
console.log(`Warning: ${errorCount} repositories had errors during repair`);
}
} else {
console.log("📊 Repair Summary:");
console.log(` Repaired: ${repairedCount}`);
console.log(` Skipped: ${skippedCount}`);
console.log(` Errors: ${errorCount}`);
if (isDryRun && repairedCount > 0) {
console.log("");
console.log("💡 To apply these changes, run the script without --dry-run");
}
}
} catch (error) {
console.error("❌ Error during repair process:", error);
}
}
// Run the repair
repairMirroredRepositories().then(() => {
console.log("Repair process complete.");
process.exit(0);
}).catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});

View File

@@ -1,13 +1,13 @@
'use client';
import * as React from 'react';
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { SiGitea } from 'react-icons/si';
import { toast, Toaster } from 'sonner';
import { showErrorToast } from '@/lib/utils';
import { FlipHorizontal } from 'lucide-react';
export function LoginForm() {
const [isLoading, setIsLoading] = useState(false);
@@ -60,7 +60,7 @@ export function LoginForm() {
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<SiGitea className="h-10 w-10" />
<img src="/logo.svg" alt="Gitea Mirror" className="h-10 w-10" />
</div>
<CardTitle className="text-2xl">Gitea Mirror</CardTitle>
<CardDescription>

View File

@@ -1,6 +1,6 @@
import { useAuth } from "@/hooks/useAuth";
import { Button } from "@/components/ui/button";
import { SiGitea } from "react-icons/si";
import { ModeToggle } from "@/components/theme/ModeToggle";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { toast } from "sonner";
@@ -64,7 +64,7 @@ export function Header({ currentPage, onNavigate }: HeaderProps) {
}}
className="flex items-center gap-2 py-1 hover:opacity-80 transition-opacity"
>
<SiGitea className="h-6 w-6" />
<img src="/logo.svg" alt="Gitea Mirror" className="h-6 w-6" />
<span className="text-xl font-bold">Gitea Mirror</span>
</button>

View File

@@ -25,11 +25,222 @@ let sqlite: Database;
try {
sqlite = new Database(dbPath);
console.log("Successfully connected to SQLite database using Bun's native driver");
// Ensure all required tables exist
ensureTablesExist(sqlite);
} catch (error) {
console.error("Error opening database:", error);
throw error;
}
/**
* Ensure all required tables exist in the database
*/
function ensureTablesExist(db: Database) {
const requiredTables = [
"users",
"configs",
"repositories",
"organizations",
"mirror_jobs",
"events",
];
for (const table of requiredTables) {
try {
// Check if table exists
const result = db.query(`SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`).get();
if (!result) {
console.warn(`⚠️ Table '${table}' is missing. Creating it now...`);
createTable(db, table);
console.log(`✅ Table '${table}' created successfully`);
}
} catch (error) {
console.error(`❌ Error checking/creating table '${table}':`, error);
throw error;
}
}
}
/**
* Create a specific table with its schema
*/
function createTable(db: Database, tableName: string) {
switch (tableName) {
case "users":
db.exec(`
CREATE TABLE 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
)
`);
break;
case "configs":
db.exec(`
CREATE TABLE 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,
cleanup_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)
)
`);
break;
case "repositories":
db.exec(`
CREATE TABLE 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 '',
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,
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)
)
`);
// Create indexes for repositories
db.exec(`
CREATE INDEX IF NOT EXISTS idx_repositories_user_id ON repositories(user_id);
CREATE INDEX IF NOT EXISTS idx_repositories_config_id ON repositories(config_id);
CREATE INDEX IF NOT EXISTS idx_repositories_status ON repositories(status);
CREATE INDEX IF NOT EXISTS idx_repositories_owner ON repositories(owner);
CREATE INDEX IF NOT EXISTS idx_repositories_organization ON repositories(organization);
CREATE INDEX IF NOT EXISTS idx_repositories_is_fork ON repositories(is_fork);
CREATE INDEX IF NOT EXISTS idx_repositories_is_starred ON repositories(is_starred);
`);
break;
case "organizations":
db.exec(`
CREATE TABLE 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 indexes for organizations
db.exec(`
CREATE INDEX IF NOT EXISTS idx_organizations_user_id ON organizations(user_id);
CREATE INDEX IF NOT EXISTS idx_organizations_config_id ON organizations(config_id);
CREATE INDEX IF NOT EXISTS idx_organizations_status ON organizations(status);
CREATE INDEX IF NOT EXISTS idx_organizations_is_included ON organizations(is_included);
`);
break;
case "mirror_jobs":
db.exec(`
CREATE TABLE 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 indexes for mirror_jobs
db.exec(`
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);
`);
break;
case "events":
db.exec(`
CREATE TABLE 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 indexes for events
db.exec(`
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);
`);
break;
default:
throw new Error(`Unknown table: ${tableName}`);
}
}
// Create drizzle instance with the SQLite client
export const db = drizzle({ client: sqlite });

View File

@@ -137,8 +137,32 @@ export const mirrorGithubRepoToGitea = async ({
if (isExisting) {
console.log(
`Repository ${repository.name} already exists in Gitea. Skipping migration.`
`Repository ${repository.name} already exists in Gitea. Updating database status.`
);
// Update database to reflect that the repository is already mirrored
await db
.update(repositories)
.set({
status: repoStatusEnum.parse("mirrored"),
updatedAt: new Date(),
lastMirrored: new Date(),
errorMessage: null,
mirroredLocation: `${config.giteaConfig.username}/${repository.name}`,
})
.where(eq(repositories.id, repository.id!));
// Append log for "mirrored" status
await createMirrorJob({
userId: config.userId,
repositoryId: repository.id,
repositoryName: repository.name,
message: `Repository ${repository.name} already exists in Gitea`,
details: `Repository ${repository.name} was found to already exist in Gitea and database status was updated.`,
status: "mirrored",
});
console.log(`Repository ${repository.name} database status updated to mirrored`);
return;
}
@@ -420,8 +444,32 @@ export async function mirrorGitHubRepoToGiteaOrg({
if (isExisting) {
console.log(
`Repository ${repository.name} already exists in Gitea. Skipping migration.`
`Repository ${repository.name} already exists in Gitea organization ${orgName}. Updating database status.`
);
// Update database to reflect that the repository is already mirrored
await db
.update(repositories)
.set({
status: repoStatusEnum.parse("mirrored"),
updatedAt: new Date(),
lastMirrored: new Date(),
errorMessage: null,
mirroredLocation: `${orgName}/${repository.name}`,
})
.where(eq(repositories.id, repository.id!));
// Create a mirror job log entry
await createMirrorJob({
userId: config.userId,
repositoryId: repository.id,
repositoryName: repository.name,
message: `Repository ${repository.name} already exists in Gitea organization ${orgName}`,
details: `Repository ${repository.name} was found to already exist in Gitea organization ${orgName} and database status was updated.`,
status: "mirrored",
});
console.log(`Repository ${repository.name} database status updated to mirrored in organization ${orgName}`);
return;
}