diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 952e597..1ee7560 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -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..." diff --git a/scripts/cleanup-duplicate-repos.ts b/scripts/cleanup-duplicate-repos.ts new file mode 100644 index 0000000..471bd0c --- /dev/null +++ b/scripts/cleanup-duplicate-repos.ts @@ -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=] + */ + +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(); + + 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); +}); diff --git a/scripts/investigate-repo.ts b/scripts/investigate-repo.ts new file mode 100644 index 0000000..8eeab35 --- /dev/null +++ b/scripts/investigate-repo.ts @@ -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); +}); diff --git a/scripts/repair-mirrored-repos.ts b/scripts/repair-mirrored-repos.ts new file mode 100644 index 0000000..2b01a07 --- /dev/null +++ b/scripts/repair-mirrored-repos.ts @@ -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=] + */ + +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 { + 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 { + 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); +}); diff --git a/src/lib/gitea.ts b/src/lib/gitea.ts index 3bcb6d7..1beafad 100644 --- a/src/lib/gitea.ts +++ b/src/lib/gitea.ts @@ -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; }