mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-07 12:06:46 +03:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b747ba891 | ||
|
|
ddd67faeab | ||
|
|
832b57538d | ||
|
|
415bff8e41 | ||
|
|
13c3ddea04 | ||
|
|
b917b30830 |
30
README.md
30
README.md
@@ -513,6 +513,36 @@ Try the following steps:
|
|||||||
>
|
>
|
||||||
> This setup provides a complete containerized deployment for the Gitea Mirror application.
|
> This setup provides a complete containerized deployment for the Gitea Mirror application.
|
||||||
|
|
||||||
|
#### Docker Volume Types and Permissions
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Named Volumes vs Bind Mounts**: If you encounter SQLite permission errors even when using Docker, check your volume configuration:
|
||||||
|
|
||||||
|
**✅ Named Volumes (Recommended):**
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- gitea-mirror-data:/app/data # Docker manages permissions automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ Bind Mounts (Requires Manual Permission Setup):**
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- /host/path/to/data:/app/data # Host filesystem permissions apply
|
||||||
|
```
|
||||||
|
|
||||||
|
**If using bind mounts**, ensure the host directory is owned by UID 1001 (the `gitea-mirror` user):
|
||||||
|
```bash
|
||||||
|
# Set correct ownership for bind mount
|
||||||
|
sudo chown -R 1001:1001 /host/path/to/data
|
||||||
|
sudo chmod -R 755 /host/path/to/data
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why named volumes work better:**
|
||||||
|
- Docker automatically handles permissions
|
||||||
|
- Better portability across different hosts
|
||||||
|
- No manual permission setup required
|
||||||
|
- Used by our official docker-compose.yml
|
||||||
|
|
||||||
|
|
||||||
#### Database Maintenance
|
#### Database Maintenance
|
||||||
|
|
||||||
|
|||||||
@@ -232,6 +232,28 @@ else
|
|||||||
echo "❌ Startup recovery failed with exit code $RECOVERY_EXIT_CODE"
|
echo "❌ Startup recovery failed with exit code $RECOVERY_EXIT_CODE"
|
||||||
fi
|
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
|
# Function to handle shutdown signals
|
||||||
shutdown_handler() {
|
shutdown_handler() {
|
||||||
echo "🛑 Received shutdown signal, forwarding to application..."
|
echo "🛑 Received shutdown signal, forwarding to application..."
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gitea-mirror",
|
"name": "gitea-mirror",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.10.0",
|
"version": "2.11.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.2.9"
|
"bun": ">=1.2.9"
|
||||||
},
|
},
|
||||||
|
|||||||
129
scripts/cleanup-duplicate-repos.ts
Normal file
129
scripts/cleanup-duplicate-repos.ts
Normal 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
178
scripts/investigate-repo.ts
Normal 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);
|
||||||
|
});
|
||||||
277
scripts/repair-mirrored-repos.ts
Normal file
277
scripts/repair-mirrored-repos.ts
Normal 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);
|
||||||
|
});
|
||||||
@@ -37,7 +37,7 @@ export function VersionInfo() {
|
|||||||
return (
|
return (
|
||||||
<div className="text-xs text-muted-foreground text-center pt-2 pb-3 border-t border-border mt-2">
|
<div className="text-xs text-muted-foreground text-center pt-2 pb-3 border-t border-border mt-2">
|
||||||
{versionInfo.updateAvailable ? (
|
{versionInfo.updateAvailable ? (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col gap-1">
|
||||||
<span>v{versionInfo.current}</span>
|
<span>v{versionInfo.current}</span>
|
||||||
<span className="text-primary">v{versionInfo.latest} available</span>
|
<span className="text-primary">v{versionInfo.latest} available</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Search, RefreshCw, FlipHorizontal, Plus } from "lucide-react";
|
import { Search, RefreshCw, FlipHorizontal } from "lucide-react";
|
||||||
import type { MirrorJob, Organization } from "@/lib/db/schema";
|
import type { MirrorJob, Organization } from "@/lib/db/schema";
|
||||||
import { OrganizationList } from "./OrganizationsList";
|
import { OrganizationList } from "./OrganizationsList";
|
||||||
import AddOrganizationDialog from "./AddOrganizationDialog";
|
import AddOrganizationDialog from "./AddOrganizationDialog";
|
||||||
@@ -26,6 +26,7 @@ import { useFilterParams } from "@/hooks/useFilterParams";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
||||||
import { useNavigation } from "@/components/layout/MainLayout";
|
import { useNavigation } from "@/components/layout/MainLayout";
|
||||||
|
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
||||||
|
|
||||||
export function Organization() {
|
export function Organization() {
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
@@ -34,6 +35,7 @@ export function Organization() {
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { isGitHubConfigured } = useConfigStatus();
|
const { isGitHubConfigured } = useConfigStatus();
|
||||||
const { navigationKey } = useNavigation();
|
const { navigationKey } = useNavigation();
|
||||||
|
const { registerRefreshCallback } = useLiveRefresh();
|
||||||
const { filter, setFilter } = useFilterParams({
|
const { filter, setFilter } = useFilterParams({
|
||||||
searchTerm: "",
|
searchTerm: "",
|
||||||
membershipRole: "",
|
membershipRole: "",
|
||||||
@@ -62,19 +64,23 @@ export function Organization() {
|
|||||||
onMessage: handleNewMessage,
|
onMessage: handleNewMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchOrganizations = useCallback(async () => {
|
const fetchOrganizations = useCallback(async (isLiveRefresh = false) => {
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't fetch organizations if GitHub is not configured
|
// Don't fetch organizations if GitHub is not configured
|
||||||
if (!isGitHubConfigured) {
|
if (!isGitHubConfigured) {
|
||||||
setIsLoading(false);
|
if (!isLiveRefresh) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
if (!isLiveRefresh) {
|
||||||
|
setIsLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
const response = await apiRequest<OrganizationsApiResponse>(
|
const response = await apiRequest<OrganizationsApiResponse>(
|
||||||
`/github/organizations?userId=${user.id}`,
|
`/github/organizations?userId=${user.id}`,
|
||||||
@@ -87,27 +93,47 @@ export function Organization() {
|
|||||||
setOrganizations(response.organizations);
|
setOrganizations(response.organizations);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error fetching organizations");
|
if (!isLiveRefresh) {
|
||||||
|
toast.error(response.error || "Error fetching organizations");
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(
|
if (!isLiveRefresh) {
|
||||||
error instanceof Error ? error.message : "Error fetching organizations"
|
toast.error(
|
||||||
);
|
error instanceof Error ? error.message : "Error fetching organizations"
|
||||||
|
);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
if (!isLiveRefresh) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object
|
}, [user?.id, isGitHubConfigured]); // Only depend on user.id, not entire user object
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset loading state when component becomes active
|
// Reset loading state when component becomes active
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
fetchOrganizations();
|
fetchOrganizations(false); // Manual refresh, not live
|
||||||
}, [fetchOrganizations, navigationKey]); // Include navigationKey to trigger on navigation
|
}, [fetchOrganizations, navigationKey]); // Include navigationKey to trigger on navigation
|
||||||
|
|
||||||
|
// Register with global live refresh system
|
||||||
|
useEffect(() => {
|
||||||
|
// Only register for live refresh if GitHub is configured
|
||||||
|
if (!isGitHubConfigured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unregister = registerRefreshCallback(() => {
|
||||||
|
fetchOrganizations(true); // Live refresh
|
||||||
|
});
|
||||||
|
|
||||||
|
return unregister;
|
||||||
|
}, [registerRefreshCallback, fetchOrganizations, isGitHubConfigured]);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
const success = await fetchOrganizations();
|
const success = await fetchOrganizations(false);
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success("Organizations refreshed successfully.");
|
toast.success("Organizations refreshed successfully.");
|
||||||
}
|
}
|
||||||
@@ -140,6 +166,12 @@ export function Organization() {
|
|||||||
return updated ? updated : org;
|
return updated ? updated : org;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Refresh organization data to get updated repository breakdown
|
||||||
|
// Use a small delay to allow the backend to process the mirroring request
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchOrganizations(true);
|
||||||
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
toast.error(response.error || "Error starting mirror job");
|
toast.error(response.error || "Error starting mirror job");
|
||||||
}
|
}
|
||||||
@@ -258,12 +290,7 @@ export function Organization() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get unique organization names for combobox (since Organization has no owner field)
|
|
||||||
const ownerOptions = Array.from(
|
|
||||||
new Set(
|
|
||||||
organizations.map((org) => org.name).filter((v): v is string => !!v)
|
|
||||||
)
|
|
||||||
).sort();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-8">
|
<div className="flex flex-col gap-y-8">
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export function OrganizationList({
|
|||||||
htmlFor={`include-${org.id}`}
|
htmlFor={`include-${org.id}`}
|
||||||
className="ml-2 text-sm select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
className="ml-2 text-sm select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
>
|
>
|
||||||
Include in mirroring
|
Enable mirroring
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
|
|||||||
@@ -137,8 +137,32 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
|
|
||||||
if (isExisting) {
|
if (isExisting) {
|
||||||
console.log(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,8 +444,32 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
|||||||
|
|
||||||
if (isExisting) {
|
if (isExisting) {
|
||||||
console.log(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user