mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-12 14:36:48 +03:00
🎉 Gitea Mirror: Added
This commit is contained in:
90
src/lib/api.ts
Normal file
90
src/lib/api.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// Base API URL
|
||||
const API_BASE = "/api";
|
||||
|
||||
// Helper function for API requests
|
||||
async function apiRequest<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${API_BASE}${endpoint}`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({
|
||||
message: "An unknown error occurred",
|
||||
}));
|
||||
throw new Error(error.message || "An unknown error occurred");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Auth API
|
||||
export const authApi = {
|
||||
login: async (username: string, password: string) => {
|
||||
const res = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include", // Send cookies
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Login failed");
|
||||
return await res.json(); // returns user
|
||||
},
|
||||
|
||||
register: async (username: string, email: string, password: string) => {
|
||||
const res = await fetch(`${API_BASE}/auth/register`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ username, email, password }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Registration failed");
|
||||
return await res.json(); // returns user
|
||||
},
|
||||
|
||||
getCurrentUser: async () => {
|
||||
const res = await fetch(`${API_BASE}/auth`, {
|
||||
method: "GET",
|
||||
credentials: "include", // Send cookies
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Not authenticated");
|
||||
return await res.json();
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
await fetch(`${API_BASE}/auth/logout`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// GitHub API
|
||||
export const githubApi = {
|
||||
testConnection: (token: string) =>
|
||||
apiRequest<{ success: boolean }>("/github/test-connection", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ token }),
|
||||
}),
|
||||
};
|
||||
|
||||
// Gitea API
|
||||
export const giteaApi = {
|
||||
testConnection: (url: string, token: string) =>
|
||||
apiRequest<{ success: boolean }>("/gitea/test-connection", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ url, token }),
|
||||
}),
|
||||
};
|
||||
28
src/lib/config.ts
Normal file
28
src/lib/config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Application configuration
|
||||
*/
|
||||
|
||||
// Environment variables
|
||||
export const ENV = {
|
||||
// Node environment (development, production, test)
|
||||
NODE_ENV: process.env.NODE_ENV || "development",
|
||||
|
||||
// Database URL - use SQLite by default
|
||||
get DATABASE_URL() {
|
||||
// If explicitly set, use the provided DATABASE_URL
|
||||
if (process.env.DATABASE_URL) {
|
||||
return process.env.DATABASE_URL;
|
||||
}
|
||||
|
||||
// Otherwise, use the default database
|
||||
return "sqlite://data/gitea-mirror.db";
|
||||
},
|
||||
|
||||
// JWT secret for authentication
|
||||
JWT_SECRET:
|
||||
process.env.JWT_SECRET || "your-secret-key-change-this-in-production",
|
||||
|
||||
// Server host and port
|
||||
HOST: process.env.HOST || "localhost",
|
||||
PORT: parseInt(process.env.PORT || "3000", 10),
|
||||
};
|
||||
177
src/lib/db/index.ts
Normal file
177
src/lib/db/index.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { z } from "zod";
|
||||
import { createClient } from "@libsql/client";
|
||||
import { drizzle } from "drizzle-orm/libsql";
|
||||
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
||||
|
||||
import path from "path";
|
||||
import { configSchema } from "./schema";
|
||||
|
||||
// Define the database URL - for development we'll use a local SQLite file
|
||||
const dataDir = path.join(process.cwd(), "data");
|
||||
const dbUrl =
|
||||
process.env.DATABASE_URL || `file:${path.join(dataDir, "gitea-mirror.db")}`;
|
||||
|
||||
// Create a client connection to the database
|
||||
export const client = createClient({ url: dbUrl });
|
||||
|
||||
// Create a drizzle instance
|
||||
export const db = drizzle(client);
|
||||
|
||||
// Define the tables
|
||||
export const users = sqliteTable("users", {
|
||||
id: text("id").primaryKey(),
|
||||
username: text("username").notNull(),
|
||||
password: text("password").notNull(),
|
||||
email: text("email").notNull(),
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(new Date()),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(new Date()),
|
||||
});
|
||||
|
||||
const githubSchema = configSchema.shape.githubConfig;
|
||||
const giteaSchema = configSchema.shape.giteaConfig;
|
||||
const scheduleSchema = configSchema.shape.scheduleConfig;
|
||||
|
||||
export const configs = sqliteTable("configs", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
name: text("name").notNull(),
|
||||
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
|
||||
|
||||
githubConfig: text("github_config", { mode: "json" })
|
||||
.$type<z.infer<typeof githubSchema>>()
|
||||
.notNull(),
|
||||
|
||||
giteaConfig: text("gitea_config", { mode: "json" })
|
||||
.$type<z.infer<typeof giteaSchema>>()
|
||||
.notNull(),
|
||||
|
||||
include: text("include", { mode: "json" })
|
||||
.$type<string[]>()
|
||||
.notNull()
|
||||
.default(["*"]),
|
||||
|
||||
exclude: text("exclude", { mode: "json" })
|
||||
.$type<string[]>()
|
||||
.notNull()
|
||||
.default([]),
|
||||
|
||||
scheduleConfig: text("schedule_config", { mode: "json" })
|
||||
.$type<z.infer<typeof scheduleSchema>>()
|
||||
.notNull(),
|
||||
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(new Date()),
|
||||
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(new Date()),
|
||||
});
|
||||
|
||||
export const repositories = sqliteTable("repositories", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
configId: text("config_id")
|
||||
.notNull()
|
||||
.references(() => configs.id),
|
||||
name: text("name").notNull(),
|
||||
fullName: text("full_name").notNull(),
|
||||
url: text("url").notNull(),
|
||||
cloneUrl: text("clone_url").notNull(),
|
||||
owner: text("owner").notNull(),
|
||||
organization: text("organization"),
|
||||
mirroredLocation: text("mirrored_location").default(""),
|
||||
|
||||
isPrivate: integer("is_private", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
isForked: integer("is_fork", { mode: "boolean" }).notNull().default(false),
|
||||
forkedFrom: text("forked_from"),
|
||||
|
||||
hasIssues: integer("has_issues", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
isStarred: integer("is_starred", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
isArchived: integer("is_archived", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
size: integer("size").notNull().default(0),
|
||||
hasLFS: integer("has_lfs", { mode: "boolean" }).notNull().default(false),
|
||||
hasSubmodules: integer("has_submodules", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
defaultBranch: text("default_branch").notNull(),
|
||||
visibility: text("visibility").notNull().default("public"),
|
||||
|
||||
status: text("status").notNull().default("imported"),
|
||||
lastMirrored: integer("last_mirrored", { mode: "timestamp" }),
|
||||
errorMessage: text("error_message"),
|
||||
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(new Date()),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(new Date()),
|
||||
});
|
||||
|
||||
export const mirrorJobs = sqliteTable("mirror_jobs", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
repositoryId: text("repository_id"),
|
||||
repositoryName: text("repository_name"),
|
||||
organizationId: text("organization_id"),
|
||||
organizationName: text("organization_name"),
|
||||
details: text("details"),
|
||||
status: text("status").notNull().default("imported"),
|
||||
message: text("message").notNull(),
|
||||
timestamp: integer("timestamp", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(new Date()),
|
||||
});
|
||||
|
||||
export const organizations = sqliteTable("organizations", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
configId: text("config_id")
|
||||
.notNull()
|
||||
.references(() => configs.id),
|
||||
name: text("name").notNull(),
|
||||
|
||||
avatarUrl: text("avatar_url").notNull(),
|
||||
|
||||
membershipRole: text("membership_role").notNull().default("member"),
|
||||
|
||||
isIncluded: integer("is_included", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(true),
|
||||
|
||||
status: text("status").notNull().default("imported"),
|
||||
lastMirrored: integer("last_mirrored", { mode: "timestamp" }),
|
||||
errorMessage: text("error_message"),
|
||||
|
||||
repositoryCount: integer("repository_count").notNull().default(0),
|
||||
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(new Date()),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(new Date()),
|
||||
});
|
||||
27
src/lib/db/migrations/add-mirrored-location.ts
Normal file
27
src/lib/db/migrations/add-mirrored-location.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { client } from "@/lib/db";
|
||||
|
||||
/**
|
||||
* Migration script to add the mirrored_location column to the repositories table
|
||||
*/
|
||||
export async function addMirroredLocationColumn() {
|
||||
try {
|
||||
console.log("Starting migration: Adding mirrored_location column to repositories table");
|
||||
|
||||
// Check if the column already exists
|
||||
const tableInfo = await client.execute(`PRAGMA table_info(repositories)`);
|
||||
const columnExists = tableInfo.rows.some((row: any) => row.name === "mirrored_location");
|
||||
|
||||
if (columnExists) {
|
||||
console.log("Column mirrored_location already exists, skipping migration");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the mirrored_location column
|
||||
await client.execute(`ALTER TABLE repositories ADD COLUMN mirrored_location TEXT DEFAULT ''`);
|
||||
|
||||
console.log("Migration completed successfully: mirrored_location column added");
|
||||
} catch (error) {
|
||||
console.error("Migration failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
75
src/lib/db/schema.sql
Normal file
75
src/lib/db/schema.sql
Normal file
@@ -0,0 +1,75 @@
|
||||
-- Users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- Configurations table
|
||||
CREATE TABLE IF NOT EXISTS configs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
github_config TEXT NOT NULL,
|
||||
gitea_config TEXT NOT NULL,
|
||||
schedule_config TEXT NOT NULL,
|
||||
include TEXT NOT NULL,
|
||||
exclude TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Repositories table
|
||||
CREATE TABLE IF NOT EXISTS repositories (
|
||||
id TEXT PRIMARY KEY,
|
||||
config_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
is_private BOOLEAN NOT NULL,
|
||||
is_fork BOOLEAN NOT NULL,
|
||||
owner TEXT NOT NULL,
|
||||
organization TEXT,
|
||||
mirrored_location TEXT DEFAULT '',
|
||||
has_issues BOOLEAN NOT NULL,
|
||||
is_starred BOOLEAN NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
error_message TEXT,
|
||||
last_mirrored DATETIME,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (config_id) REFERENCES configs (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Organizations table
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
config_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
is_included BOOLEAN NOT NULL,
|
||||
repository_count INTEGER NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (config_id) REFERENCES configs (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Mirror jobs table
|
||||
CREATE TABLE IF NOT EXISTS mirror_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
config_id TEXT NOT NULL,
|
||||
repository_id TEXT,
|
||||
status TEXT NOT NULL,
|
||||
started_at DATETIME NOT NULL,
|
||||
completed_at DATETIME,
|
||||
log TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (config_id) REFERENCES configs (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (repository_id) REFERENCES repositories (id) ON DELETE SET NULL
|
||||
);
|
||||
142
src/lib/db/schema.ts
Normal file
142
src/lib/db/schema.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { z } from "zod";
|
||||
import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
|
||||
import { membershipRoleEnum } from "@/types/organizations";
|
||||
|
||||
// User schema
|
||||
export const userSchema = z.object({
|
||||
id: z.string().uuid().optional(),
|
||||
username: z.string().min(3),
|
||||
password: z.string().min(8).optional(), // Hashed password
|
||||
email: z.string().email(),
|
||||
createdAt: z.date().default(() => new Date()),
|
||||
updatedAt: z.date().default(() => new Date()),
|
||||
});
|
||||
|
||||
export type User = z.infer<typeof userSchema>;
|
||||
|
||||
// Configuration schema
|
||||
export const configSchema = z.object({
|
||||
id: z.string().uuid().optional(),
|
||||
userId: z.string().uuid(),
|
||||
name: z.string().min(1),
|
||||
isActive: z.boolean().default(true),
|
||||
githubConfig: z.object({
|
||||
username: z.string().min(1),
|
||||
token: z.string().optional(),
|
||||
skipForks: z.boolean().default(false),
|
||||
privateRepositories: z.boolean().default(false),
|
||||
mirrorIssues: z.boolean().default(false),
|
||||
mirrorStarred: z.boolean().default(false),
|
||||
useSpecificUser: z.boolean().default(false),
|
||||
singleRepo: z.string().optional(),
|
||||
includeOrgs: z.array(z.string()).default([]),
|
||||
excludeOrgs: z.array(z.string()).default([]),
|
||||
mirrorPublicOrgs: z.boolean().default(false),
|
||||
publicOrgs: z.array(z.string()).default([]),
|
||||
preserveOrgStructure: z.boolean().default(false),
|
||||
skipStarredIssues: z.boolean().default(false),
|
||||
}),
|
||||
giteaConfig: z.object({
|
||||
username: z.string().min(1),
|
||||
url: z.string().url(),
|
||||
token: z.string().min(1),
|
||||
organization: z.string().optional(),
|
||||
visibility: z.enum(["public", "private", "limited"]).default("public"),
|
||||
starredReposOrg: z.string().default("github"),
|
||||
}),
|
||||
include: z.array(z.string()).default(["*"]),
|
||||
exclude: z.array(z.string()).default([]),
|
||||
scheduleConfig: z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
interval: z.number().min(1).default(3600), // in seconds
|
||||
lastRun: z.date().optional(),
|
||||
nextRun: z.date().optional(),
|
||||
}),
|
||||
createdAt: z.date().default(() => new Date()),
|
||||
updatedAt: z.date().default(() => new Date()),
|
||||
});
|
||||
|
||||
export type Config = z.infer<typeof configSchema>;
|
||||
|
||||
// Repository schema
|
||||
export const repositorySchema = z.object({
|
||||
id: z.string().uuid().optional(),
|
||||
userId: z.string().uuid().optional(),
|
||||
configId: z.string().uuid(),
|
||||
|
||||
name: z.string().min(1),
|
||||
fullName: z.string().min(1),
|
||||
url: z.string().url(),
|
||||
cloneUrl: z.string().url(),
|
||||
|
||||
owner: z.string().min(1),
|
||||
organization: z.string().optional(),
|
||||
|
||||
isPrivate: z.boolean().default(false),
|
||||
isForked: z.boolean().default(false),
|
||||
forkedFrom: z.string().optional(),
|
||||
|
||||
hasIssues: z.boolean().default(false),
|
||||
isStarred: z.boolean().default(false),
|
||||
isArchived: z.boolean().default(false),
|
||||
|
||||
size: z.number(),
|
||||
hasLFS: z.boolean().default(false),
|
||||
hasSubmodules: z.boolean().default(false),
|
||||
|
||||
defaultBranch: z.string(),
|
||||
visibility: repositoryVisibilityEnum.default("public"),
|
||||
|
||||
status: repoStatusEnum.default("imported"),
|
||||
lastMirrored: z.date().optional(),
|
||||
errorMessage: z.string().optional(),
|
||||
|
||||
mirroredLocation: z.string().default(""), // Store the full Gitea path where repo was mirrored
|
||||
|
||||
createdAt: z.date().default(() => new Date()),
|
||||
updatedAt: z.date().default(() => new Date()),
|
||||
});
|
||||
|
||||
export type Repository = z.infer<typeof repositorySchema>;
|
||||
|
||||
// Mirror job schema
|
||||
export const mirrorJobSchema = z.object({
|
||||
id: z.string().uuid().optional(),
|
||||
userId: z.string().uuid().optional(),
|
||||
repositoryId: z.string().uuid().optional(),
|
||||
repositoryName: z.string().optional(),
|
||||
organizationId: z.string().uuid().optional(),
|
||||
organizationName: z.string().optional(),
|
||||
details: z.string().optional(),
|
||||
status: repoStatusEnum.default("imported"),
|
||||
message: z.string(),
|
||||
timestamp: z.date().default(() => new Date()),
|
||||
});
|
||||
|
||||
export type MirrorJob = z.infer<typeof mirrorJobSchema>;
|
||||
|
||||
// Organization schema
|
||||
export const organizationSchema = z.object({
|
||||
id: z.string().uuid().optional(),
|
||||
userId: z.string().uuid().optional(),
|
||||
configId: z.string().uuid(),
|
||||
|
||||
avatarUrl: z.string().url(),
|
||||
|
||||
name: z.string().min(1),
|
||||
|
||||
membershipRole: membershipRoleEnum.default("member"),
|
||||
|
||||
isIncluded: z.boolean().default(false),
|
||||
|
||||
status: repoStatusEnum.default("imported"),
|
||||
lastMirrored: z.date().optional(),
|
||||
errorMessage: z.string().optional(),
|
||||
|
||||
repositoryCount: z.number().default(0),
|
||||
|
||||
createdAt: z.date().default(() => new Date()),
|
||||
updatedAt: z.date().default(() => new Date()),
|
||||
});
|
||||
|
||||
export type Organization = z.infer<typeof organizationSchema>;
|
||||
962
src/lib/gitea.ts
Normal file
962
src/lib/gitea.ts
Normal file
@@ -0,0 +1,962 @@
|
||||
import {
|
||||
repoStatusEnum,
|
||||
type RepositoryVisibility,
|
||||
type RepoStatus,
|
||||
} from "@/types/Repository";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import type { Config } from "@/types/config";
|
||||
import type { Organization, Repository } from "./db/schema";
|
||||
import superagent from "superagent";
|
||||
import { createMirrorJob } from "./helpers";
|
||||
import { db, organizations, repositories } from "./db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const getGiteaRepoOwner = ({
|
||||
config,
|
||||
repository,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
repository: Repository;
|
||||
}): string => {
|
||||
if (!config.githubConfig || !config.giteaConfig) {
|
||||
throw new Error("GitHub or Gitea config is required.");
|
||||
}
|
||||
|
||||
if (!config.giteaConfig.username) {
|
||||
throw new Error("Gitea username is required.");
|
||||
}
|
||||
|
||||
// if the config has preserveOrgStructure set to true, then use the org name as the owner
|
||||
if (config.githubConfig.preserveOrgStructure && repository.organization) {
|
||||
return repository.organization;
|
||||
}
|
||||
|
||||
// if the config has preserveOrgStructure set to false, then use the gitea username as the owner
|
||||
return config.giteaConfig.username;
|
||||
};
|
||||
|
||||
export const isRepoPresentInGitea = async ({
|
||||
config,
|
||||
owner,
|
||||
repoName,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
owner: string;
|
||||
repoName: string;
|
||||
}): Promise<boolean> => {
|
||||
try {
|
||||
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
|
||||
throw new Error("Gitea config is required.");
|
||||
}
|
||||
|
||||
// Check if the repository exists at the specified owner location
|
||||
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 if repo exists in Gitea:", 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.
|
||||
*/
|
||||
export const checkRepoLocation = async ({
|
||||
config,
|
||||
repository,
|
||||
expectedOwner,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
repository: Repository;
|
||||
expectedOwner: string;
|
||||
}): Promise<{ present: boolean; actualOwner: string }> => {
|
||||
// First check if we have a recorded mirroredLocation and if the repo exists there
|
||||
if (repository.mirroredLocation && repository.mirroredLocation.trim() !== "") {
|
||||
const [mirroredOwner] = repository.mirroredLocation.split('/');
|
||||
if (mirroredOwner) {
|
||||
const mirroredPresent = await isRepoPresentInGitea({
|
||||
config,
|
||||
owner: mirroredOwner,
|
||||
repoName: repository.name,
|
||||
});
|
||||
|
||||
if (mirroredPresent) {
|
||||
console.log(`Repository found at recorded mirrored location: ${repository.mirroredLocation}`);
|
||||
return { present: true, actualOwner: mirroredOwner };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not found at the recorded location, check the expected location
|
||||
const present = await isRepoPresentInGitea({
|
||||
config,
|
||||
owner: expectedOwner,
|
||||
repoName: repository.name,
|
||||
});
|
||||
|
||||
if (present) {
|
||||
return { present: true, actualOwner: expectedOwner };
|
||||
}
|
||||
|
||||
// Repository not found at any location
|
||||
return { present: false, actualOwner: expectedOwner };
|
||||
};
|
||||
|
||||
export const mirrorGithubRepoToGitea = async ({
|
||||
octokit,
|
||||
repository,
|
||||
config,
|
||||
}: {
|
||||
octokit: Octokit;
|
||||
repository: Repository;
|
||||
config: Partial<Config>;
|
||||
}): Promise<any> => {
|
||||
try {
|
||||
if (!config.userId || !config.githubConfig || !config.giteaConfig) {
|
||||
throw new Error("github config and gitea config are required.");
|
||||
}
|
||||
|
||||
if (!config.giteaConfig.username) {
|
||||
throw new Error("Gitea username is required.");
|
||||
}
|
||||
|
||||
const isExisting = await isRepoPresentInGitea({
|
||||
config,
|
||||
owner: config.giteaConfig.username,
|
||||
repoName: repository.name,
|
||||
});
|
||||
|
||||
if (isExisting) {
|
||||
console.log(
|
||||
`Repository ${repository.name} already exists in Gitea. Skipping migration.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Mirroring repository ${repository.name}`);
|
||||
|
||||
// Mark repos as "mirroring" in DB
|
||||
await db
|
||||
.update(repositories)
|
||||
.set({
|
||||
status: repoStatusEnum.parse("mirroring"),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(repositories.id, repository.id!));
|
||||
|
||||
// Append log for "mirroring" status
|
||||
await createMirrorJob({
|
||||
userId: config.userId,
|
||||
repositoryId: repository.id,
|
||||
repositoryName: repository.name,
|
||||
message: `Started mirroring repository: ${repository.name}`,
|
||||
details: `Repository ${repository.name} is now in the mirroring state.`,
|
||||
status: "mirroring",
|
||||
});
|
||||
|
||||
let cloneAddress = repository.cloneUrl;
|
||||
|
||||
// If the repository is private, inject the GitHub token into the clone URL
|
||||
if (repository.isPrivate) {
|
||||
if (!config.githubConfig.token) {
|
||||
throw new Error(
|
||||
"GitHub token is required to mirror private repositories."
|
||||
);
|
||||
}
|
||||
|
||||
cloneAddress = repository.cloneUrl.replace(
|
||||
"https://",
|
||||
`https://${config.githubConfig.token}@`
|
||||
);
|
||||
}
|
||||
|
||||
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;
|
||||
|
||||
const response = await superagent
|
||||
.post(apiUrl)
|
||||
.set("Authorization", `token ${config.giteaConfig.token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.send({
|
||||
clone_addr: cloneAddress,
|
||||
repo_name: repository.name,
|
||||
mirror: true,
|
||||
private: repository.isPrivate,
|
||||
repo_owner: config.giteaConfig.username,
|
||||
description: "",
|
||||
service: "git",
|
||||
});
|
||||
|
||||
// clone issues
|
||||
if (config.githubConfig.mirrorIssues) {
|
||||
await mirrorGitRepoIssuesToGitea({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
isRepoInOrg: false,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Repository ${repository.name} mirrored successfully`);
|
||||
|
||||
// Mark repos as "mirrored" in DB
|
||||
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: `Successfully mirrored repository: ${repository.name}`,
|
||||
details: `Repository ${repository.name} was mirrored to Gitea.`,
|
||||
status: "mirrored",
|
||||
});
|
||||
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error while mirroring repository ${repository.name}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
|
||||
// Mark repos as "failed" in DB
|
||||
await db
|
||||
.update(repositories)
|
||||
.set({
|
||||
status: repoStatusEnum.parse("failed"),
|
||||
updatedAt: new Date(),
|
||||
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
.where(eq(repositories.id, repository.id!));
|
||||
|
||||
// Append log for failure
|
||||
await createMirrorJob({
|
||||
userId: config.userId ?? "", // userId is going to be there anyways
|
||||
repositoryId: repository.id,
|
||||
repositoryName: repository.name,
|
||||
message: `Failed to mirror repository: ${repository.name}`,
|
||||
details: `Repository ${repository.name} failed to mirror. Error: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
status: "failed",
|
||||
});
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to mirror repository: ${error.message}`);
|
||||
}
|
||||
throw new Error("Failed to mirror repository: An unknown error occurred.");
|
||||
}
|
||||
};
|
||||
|
||||
export async function getOrCreateGiteaOrg({
|
||||
orgName,
|
||||
orgId,
|
||||
config,
|
||||
}: {
|
||||
orgId?: string; //db id
|
||||
orgName: string;
|
||||
config: Partial<Config>;
|
||||
}): Promise<number> {
|
||||
if (
|
||||
!config.giteaConfig?.url ||
|
||||
!config.giteaConfig?.token ||
|
||||
!config.userId
|
||||
) {
|
||||
throw new Error("Gitea config is required.");
|
||||
}
|
||||
|
||||
try {
|
||||
const orgRes = await fetch(
|
||||
`${config.giteaConfig.url}/api/v1/orgs/${orgName}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${config.giteaConfig.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (orgRes.ok) {
|
||||
const org = await orgRes.json();
|
||||
|
||||
await createMirrorJob({
|
||||
userId: config.userId,
|
||||
organizationId: org.id,
|
||||
organizationName: orgName,
|
||||
status: "imported",
|
||||
message: `Organization ${orgName} fetched successfully`,
|
||||
details: `Organization ${orgName} was fetched from GitHub`,
|
||||
});
|
||||
return org.id;
|
||||
}
|
||||
|
||||
const createRes = await fetch(`${config.giteaConfig.url}/api/v1/orgs`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `token ${config.giteaConfig.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: orgName,
|
||||
full_name: `${orgName} Org`,
|
||||
description: `Mirrored organization from GitHub ${orgName}`,
|
||||
visibility: "public",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!createRes.ok) {
|
||||
throw new Error(`Failed to create Gitea org: ${await createRes.text()}`);
|
||||
}
|
||||
|
||||
await createMirrorJob({
|
||||
userId: config.userId,
|
||||
organizationName: orgName,
|
||||
status: "imported",
|
||||
message: `Organization ${orgName} created successfully`,
|
||||
details: `Organization ${orgName} was created in Gitea`,
|
||||
});
|
||||
|
||||
const newOrg = await createRes.json();
|
||||
return newOrg.id;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Unknown error occurred in getOrCreateGiteaOrg.";
|
||||
|
||||
await createMirrorJob({
|
||||
userId: config.userId,
|
||||
organizationId: orgId,
|
||||
organizationName: orgName,
|
||||
message: `Failed to create or fetch Gitea organization: ${orgName}`,
|
||||
status: "failed",
|
||||
details: `Error: ${errorMessage}`,
|
||||
});
|
||||
|
||||
throw new Error(`Error in getOrCreateGiteaOrg: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function mirrorGitHubRepoToGiteaOrg({
|
||||
octokit,
|
||||
config,
|
||||
repository,
|
||||
giteaOrgId,
|
||||
orgName,
|
||||
}: {
|
||||
octokit: Octokit;
|
||||
config: Partial<Config>;
|
||||
repository: Repository;
|
||||
giteaOrgId: number;
|
||||
orgName: string;
|
||||
}) {
|
||||
try {
|
||||
if (
|
||||
!config.giteaConfig?.url ||
|
||||
!config.giteaConfig?.token ||
|
||||
!config.userId
|
||||
) {
|
||||
throw new Error("Gitea config is required.");
|
||||
}
|
||||
|
||||
const isExisting = await isRepoPresentInGitea({
|
||||
config,
|
||||
owner: orgName,
|
||||
repoName: repository.name,
|
||||
});
|
||||
|
||||
if (isExisting) {
|
||||
console.log(
|
||||
`Repository ${repository.name} already exists in Gitea. Skipping migration.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Mirroring repository ${repository.name} to organization ${orgName}`
|
||||
);
|
||||
|
||||
let cloneAddress = repository.cloneUrl;
|
||||
|
||||
if (repository.isPrivate) {
|
||||
if (!config.githubConfig?.token) {
|
||||
throw new Error(
|
||||
"GitHub token is required to mirror private repositories."
|
||||
);
|
||||
}
|
||||
|
||||
cloneAddress = repository.cloneUrl.replace(
|
||||
"https://",
|
||||
`https://${config.githubConfig.token}@`
|
||||
);
|
||||
}
|
||||
|
||||
// Mark repos as "mirroring" in DB
|
||||
await db
|
||||
.update(repositories)
|
||||
.set({
|
||||
status: repoStatusEnum.parse("mirroring"),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(repositories.id, repository.id!));
|
||||
|
||||
// Append log for "mirroring" status
|
||||
await createMirrorJob({
|
||||
userId: config.userId,
|
||||
repositoryId: repository.id,
|
||||
repositoryName: repository.name,
|
||||
message: `Started mirroring repository: ${repository.name}`,
|
||||
details: `Repository ${repository.name} is now in the mirroring state.`,
|
||||
status: "mirroring",
|
||||
});
|
||||
|
||||
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;
|
||||
|
||||
const migrateRes = await superagent
|
||||
.post(apiUrl)
|
||||
.set("Authorization", `token ${config.giteaConfig.token}`)
|
||||
.set("Content-Type", "application/json")
|
||||
.send({
|
||||
clone_addr: cloneAddress,
|
||||
uid: giteaOrgId,
|
||||
repo_name: repository.name,
|
||||
mirror: true,
|
||||
private: repository.isPrivate,
|
||||
});
|
||||
|
||||
// Clone issues
|
||||
if (config.githubConfig?.mirrorIssues) {
|
||||
await mirrorGitRepoIssuesToGitea({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
isRepoInOrg: true,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Repository ${repository.name} mirrored successfully to organization ${orgName}`
|
||||
);
|
||||
|
||||
// Mark repos as "mirrored" in DB
|
||||
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
|
||||
await createMirrorJob({
|
||||
userId: config.userId,
|
||||
repositoryId: repository.id,
|
||||
repositoryName: repository.name,
|
||||
message: `Repository ${repository.name} mirrored successfully`,
|
||||
details: `Repository ${repository.name} was mirrored to Gitea`,
|
||||
status: "mirrored",
|
||||
});
|
||||
|
||||
return migrateRes.body;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error while mirroring repository ${repository.name}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
// Mark repos as "failed" in DB
|
||||
await db
|
||||
.update(repositories)
|
||||
.set({
|
||||
status: repoStatusEnum.parse("failed"),
|
||||
updatedAt: new Date(),
|
||||
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
.where(eq(repositories.id, repository.id!));
|
||||
|
||||
// Append log for failure
|
||||
await createMirrorJob({
|
||||
userId: config.userId || "", // userId is going to be there anyways
|
||||
repositoryId: repository.id,
|
||||
repositoryName: repository.name,
|
||||
message: `Failed to mirror repository: ${repository.name}`,
|
||||
details: `Repository ${repository.name} failed to mirror. Error: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
status: "failed",
|
||||
});
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to mirror repository: ${error.message}`);
|
||||
}
|
||||
throw new Error("Failed to mirror repository: An unknown error occurred.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function mirrorGitHubOrgRepoToGiteaOrg({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
orgName,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
octokit: Octokit;
|
||||
repository: Repository;
|
||||
orgName: string;
|
||||
}) {
|
||||
try {
|
||||
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
|
||||
throw new Error("Gitea config is required.");
|
||||
}
|
||||
|
||||
const giteaOrgId = await getOrCreateGiteaOrg({
|
||||
orgName,
|
||||
config,
|
||||
});
|
||||
|
||||
await mirrorGitHubRepoToGiteaOrg({
|
||||
octokit,
|
||||
config,
|
||||
repository,
|
||||
giteaOrgId,
|
||||
orgName,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to mirror repository: ${error.message}`);
|
||||
}
|
||||
throw new Error("Failed to mirror repository: An unknown error occurred.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function mirrorGitHubOrgToGitea({
|
||||
organization,
|
||||
octokit,
|
||||
config,
|
||||
}: {
|
||||
organization: Organization;
|
||||
octokit: Octokit;
|
||||
config: Partial<Config>;
|
||||
}) {
|
||||
try {
|
||||
if (
|
||||
!config.userId ||
|
||||
!config.id ||
|
||||
!config.githubConfig?.token ||
|
||||
!config.giteaConfig?.url
|
||||
) {
|
||||
throw new Error("Config, GitHub token and Gitea URL are required.");
|
||||
}
|
||||
|
||||
console.log(`Mirroring organization ${organization.name}`);
|
||||
|
||||
//mark the org as "mirroring" in DB
|
||||
await db
|
||||
.update(organizations)
|
||||
.set({
|
||||
isIncluded: true,
|
||||
status: repoStatusEnum.parse("mirroring"),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(organizations.id, organization.id!));
|
||||
|
||||
// Append log for "mirroring" status
|
||||
await createMirrorJob({
|
||||
userId: config.userId,
|
||||
organizationId: organization.id,
|
||||
organizationName: organization.name,
|
||||
message: `Started mirroring organization: ${organization.name}`,
|
||||
details: `Organization ${organization.name} is now in the mirroring state.`,
|
||||
status: repoStatusEnum.parse("mirroring"),
|
||||
});
|
||||
|
||||
const giteaOrgId = await getOrCreateGiteaOrg({
|
||||
orgId: organization.id,
|
||||
orgName: organization.name,
|
||||
config,
|
||||
});
|
||||
|
||||
//query the db with the org name and get the repos
|
||||
const orgRepos = await db
|
||||
.select()
|
||||
.from(repositories)
|
||||
.where(eq(repositories.organization, organization.name));
|
||||
|
||||
for (const repo of orgRepos) {
|
||||
await mirrorGitHubRepoToGiteaOrg({
|
||||
octokit,
|
||||
config,
|
||||
repository: {
|
||||
...repo,
|
||||
status: repo.status as RepoStatus,
|
||||
visibility: repo.visibility as RepositoryVisibility,
|
||||
lastMirrored: repo.lastMirrored ?? undefined,
|
||||
errorMessage: repo.errorMessage ?? undefined,
|
||||
organization: repo.organization ?? undefined,
|
||||
forkedFrom: repo.forkedFrom ?? undefined,
|
||||
mirroredLocation: repo.mirroredLocation || "",
|
||||
},
|
||||
giteaOrgId,
|
||||
orgName: organization.name,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Organization ${organization.name} mirrored successfully`);
|
||||
|
||||
// Mark org as "mirrored" in DB
|
||||
await db
|
||||
.update(organizations)
|
||||
.set({
|
||||
status: repoStatusEnum.parse("mirrored"),
|
||||
updatedAt: new Date(),
|
||||
lastMirrored: new Date(),
|
||||
errorMessage: null,
|
||||
})
|
||||
.where(eq(organizations.id, organization.id!));
|
||||
|
||||
// Append log for "mirrored" status
|
||||
await createMirrorJob({
|
||||
userId: config.userId,
|
||||
organizationId: organization.id,
|
||||
organizationName: organization.name,
|
||||
message: `Successfully mirrored organization: ${organization.name}`,
|
||||
details: `Organization ${organization.name} was mirrored to Gitea.`,
|
||||
status: repoStatusEnum.parse("mirrored"),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error while mirroring organization ${organization.name}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
|
||||
// Mark org as "failed" in DB
|
||||
await db
|
||||
.update(organizations)
|
||||
.set({
|
||||
status: repoStatusEnum.parse("failed"),
|
||||
updatedAt: new Date(),
|
||||
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
||||
})
|
||||
.where(eq(organizations.id, organization.id!));
|
||||
|
||||
// Append log for failure
|
||||
await createMirrorJob({
|
||||
userId: config.userId || "", // userId is going to be there anyways
|
||||
organizationId: organization.id,
|
||||
organizationName: organization.name,
|
||||
message: `Failed to mirror organization: ${organization.name}`,
|
||||
details: `Organization ${organization.name} failed to mirror. Error: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
status: repoStatusEnum.parse("failed"),
|
||||
});
|
||||
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to mirror repository: ${error.message}`);
|
||||
}
|
||||
throw new Error("Failed to mirror repository: An unknown error occurred.");
|
||||
}
|
||||
}
|
||||
|
||||
export const syncGiteaRepo = async ({
|
||||
config,
|
||||
repository,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
repository: Repository;
|
||||
}) => {
|
||||
try {
|
||||
if (
|
||||
!config.userId ||
|
||||
!config.giteaConfig?.url ||
|
||||
!config.giteaConfig?.token ||
|
||||
!config.giteaConfig?.username
|
||||
) {
|
||||
throw new Error("Gitea config is required.");
|
||||
}
|
||||
|
||||
console.log(`Syncing repository ${repository.name}`);
|
||||
|
||||
// Mark repo as "syncing" in DB
|
||||
await db
|
||||
.update(repositories)
|
||||
.set({
|
||||
status: repoStatusEnum.parse("syncing"),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(repositories.id, repository.id!));
|
||||
|
||||
// Append log for "syncing" status
|
||||
await createMirrorJob({
|
||||
userId: config.userId,
|
||||
repositoryId: repository.id,
|
||||
repositoryName: repository.name,
|
||||
message: `Started syncing repository: ${repository.name}`,
|
||||
details: `Repository ${repository.name} is now in the syncing state.`,
|
||||
status: repoStatusEnum.parse("syncing"),
|
||||
});
|
||||
|
||||
// Get the expected owner based on current config
|
||||
const repoOwner = getGiteaRepoOwner({ config, repository });
|
||||
|
||||
// Check if repo exists at the expected location or alternate location
|
||||
const { present, actualOwner } = await checkRepoLocation({
|
||||
config,
|
||||
repository,
|
||||
expectedOwner: repoOwner
|
||||
});
|
||||
|
||||
if (!present) {
|
||||
throw new Error(`Repository ${repository.name} not found in Gitea at any expected location`);
|
||||
}
|
||||
|
||||
// Use the actual owner where the repo was found
|
||||
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/${actualOwner}/${repository.name}/mirror-sync`;
|
||||
|
||||
const response = await superagent
|
||||
.post(apiUrl)
|
||||
.set("Authorization", `token ${config.giteaConfig.token}`);
|
||||
|
||||
// Mark repo as "synced" in DB
|
||||
await db
|
||||
.update(repositories)
|
||||
.set({
|
||||
status: repoStatusEnum.parse("synced"),
|
||||
updatedAt: new Date(),
|
||||
lastMirrored: new Date(),
|
||||
errorMessage: null,
|
||||
mirroredLocation: `${actualOwner}/${repository.name}`,
|
||||
})
|
||||
.where(eq(repositories.id, repository.id!));
|
||||
|
||||
// Append log for "synced" status
|
||||
await createMirrorJob({
|
||||
userId: config.userId,
|
||||
repositoryId: repository.id,
|
||||
repositoryName: repository.name,
|
||||
message: `Successfully synced repository: ${repository.name}`,
|
||||
details: `Repository ${repository.name} was synced with Gitea.`,
|
||||
status: repoStatusEnum.parse("synced"),
|
||||
});
|
||||
|
||||
console.log(`Repository ${repository.name} synced successfully`);
|
||||
|
||||
return response.body;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error while syncing repository ${repository.name}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
|
||||
// Optional: update repo with error status
|
||||
await db
|
||||
.update(repositories)
|
||||
.set({
|
||||
status: repoStatusEnum.parse("failed"),
|
||||
updatedAt: new Date(),
|
||||
errorMessage: (error as Error).message,
|
||||
})
|
||||
.where(eq(repositories.id, repository.id!));
|
||||
|
||||
// Append log for "error" status
|
||||
if (config.userId && repository.id && repository.name) {
|
||||
await createMirrorJob({
|
||||
userId: config.userId,
|
||||
repositoryId: repository.id,
|
||||
repositoryName: repository.name,
|
||||
message: `Failed to sync repository: ${repository.name}`,
|
||||
details: (error as Error).message,
|
||||
status: repoStatusEnum.parse("failed"),
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to sync repository: ${error.message}`);
|
||||
}
|
||||
throw new Error("Failed to sync repository: An unknown error occurred.");
|
||||
}
|
||||
};
|
||||
|
||||
export const mirrorGitRepoIssuesToGitea = async ({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
isRepoInOrg,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
octokit: Octokit;
|
||||
repository: Repository;
|
||||
isRepoInOrg: boolean;
|
||||
}) => {
|
||||
//things covered here are- issue, title, body, labels, comments and assignees
|
||||
if (
|
||||
!config.githubConfig?.token ||
|
||||
!config.giteaConfig?.token ||
|
||||
!config.giteaConfig?.url ||
|
||||
!config.giteaConfig?.username
|
||||
) {
|
||||
throw new Error("Missing GitHub or Gitea configuration.");
|
||||
}
|
||||
|
||||
const repoOrigin = isRepoInOrg
|
||||
? repository.organization
|
||||
: config.githubConfig.username;
|
||||
|
||||
const [owner, repo] = repository.fullName.split("/");
|
||||
|
||||
// Fetch GitHub issues
|
||||
const issues = await octokit.paginate(
|
||||
octokit.rest.issues.listForRepo,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
state: "all",
|
||||
per_page: 100,
|
||||
},
|
||||
(res) => res.data
|
||||
);
|
||||
|
||||
console.log(`Mirroring ${issues.length} issues from ${repository.fullName}`);
|
||||
|
||||
// Get existing labels from Gitea
|
||||
const giteaLabelsRes = await superagent
|
||||
.get(
|
||||
`${config.giteaConfig.url}/api/v1/repos/${repoOrigin}/${repository.name}/labels`
|
||||
)
|
||||
.set("Authorization", `token ${config.giteaConfig.token}`);
|
||||
|
||||
const giteaLabels = giteaLabelsRes.body;
|
||||
const labelMap = new Map<string, number>(
|
||||
giteaLabels.map((label: any) => [label.name, label.id])
|
||||
);
|
||||
|
||||
for (const issue of issues) {
|
||||
if ((issue as any).pull_request) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const githubLabelNames =
|
||||
issue.labels
|
||||
?.map((l) => (typeof l === "string" ? l : l.name))
|
||||
.filter((l): l is string => !!l) || [];
|
||||
|
||||
const giteaLabelIds: number[] = [];
|
||||
|
||||
// Resolve or create labels in Gitea
|
||||
for (const name of githubLabelNames) {
|
||||
if (labelMap.has(name)) {
|
||||
giteaLabelIds.push(labelMap.get(name)!);
|
||||
} else {
|
||||
try {
|
||||
const created = await superagent
|
||||
.post(
|
||||
`${config.giteaConfig.url}/api/v1/repos/${repoOrigin}/${repository.name}/labels`
|
||||
)
|
||||
.set("Authorization", `token ${config.giteaConfig.token}`)
|
||||
.send({ name, color: "#ededed" }); // Default color
|
||||
|
||||
labelMap.set(name, created.body.id);
|
||||
giteaLabelIds.push(created.body.id);
|
||||
} catch (labelErr) {
|
||||
console.error(
|
||||
`Failed to create label "${name}" in Gitea: ${labelErr}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const originalAssignees =
|
||||
issue.assignees && issue.assignees.length > 0
|
||||
? `\n\nOriginally assigned to: ${issue.assignees
|
||||
.map((a) => `@${a.login}`)
|
||||
.join(", ")} on GitHub.`
|
||||
: "";
|
||||
|
||||
const issuePayload: any = {
|
||||
title: issue.title,
|
||||
body: `Originally created by @${
|
||||
issue.user?.login
|
||||
} on GitHub.${originalAssignees}\n\n${issue.body || ""}`,
|
||||
closed: issue.state === "closed",
|
||||
labels: giteaLabelIds,
|
||||
};
|
||||
|
||||
try {
|
||||
const createdIssue = await superagent
|
||||
.post(
|
||||
`${config.giteaConfig.url}/api/v1/repos/${repoOrigin}/${repository.name}/issues`
|
||||
)
|
||||
.set("Authorization", `token ${config.giteaConfig.token}`)
|
||||
.send(issuePayload);
|
||||
|
||||
// Clone comments
|
||||
const comments = await octokit.paginate(
|
||||
octokit.rest.issues.listComments,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
per_page: 100,
|
||||
},
|
||||
(res) => res.data
|
||||
);
|
||||
|
||||
for (const comment of comments) {
|
||||
try {
|
||||
await superagent
|
||||
.post(
|
||||
`${config.giteaConfig.url}/api/v1/repos/${repoOrigin}/${repository.name}/issues/${createdIssue.body.number}/comments`
|
||||
)
|
||||
.set("Authorization", `token ${config.giteaConfig.token}`)
|
||||
.send({
|
||||
body: `@${comment.user?.login} commented on GitHub:\n\n${comment.body}`,
|
||||
});
|
||||
} catch (commentErr) {
|
||||
console.error(
|
||||
`Failed to copy comment to Gitea for issue "${issue.title}": ${
|
||||
commentErr instanceof Error
|
||||
? commentErr.message
|
||||
: String(commentErr)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof Error && (err as any).response) {
|
||||
console.error(
|
||||
`Failed to create issue "${issue.title}" in Gitea: ${err.message}`
|
||||
);
|
||||
console.error(
|
||||
`Response body: ${JSON.stringify((err as any).response.body)}`
|
||||
);
|
||||
} else {
|
||||
console.error(
|
||||
`Failed to create issue "${issue.title}" in Gitea: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
265
src/lib/github.ts
Normal file
265
src/lib/github.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import type { GitOrg, MembershipRole } from "@/types/organizations";
|
||||
import type { GitRepo, RepoStatus } from "@/types/Repository";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import type { Config } from "@/types/config";
|
||||
|
||||
/**
|
||||
* Creates an authenticated Octokit instance
|
||||
*/
|
||||
export function createGitHubClient(token: string): Octokit {
|
||||
return new Octokit({
|
||||
auth: token,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone a repository from GitHub
|
||||
*/
|
||||
export async function getGithubRepoCloneUrl({
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
}: {
|
||||
octokit: Octokit;
|
||||
owner: string;
|
||||
repo: string;
|
||||
}): Promise<{ url: string; cloneUrl: string }> {
|
||||
const { data } = await octokit.repos.get({
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
|
||||
return {
|
||||
url: data.html_url,
|
||||
cloneUrl: data.clone_url,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user repositories from GitHub
|
||||
* todo: need to handle pagination and apply more filters based on user config
|
||||
*/
|
||||
export async function getGithubRepositories({
|
||||
octokit,
|
||||
config,
|
||||
}: {
|
||||
octokit: Octokit;
|
||||
config: Partial<Config>;
|
||||
}): Promise<GitRepo[]> {
|
||||
try {
|
||||
const repos = await octokit.paginate(
|
||||
octokit.repos.listForAuthenticatedUser,
|
||||
{ per_page: 100 }
|
||||
);
|
||||
|
||||
const includePrivate = config.githubConfig?.privateRepositories ?? false;
|
||||
const skipForks = config.githubConfig?.skipForks ?? false;
|
||||
|
||||
const filteredRepos = repos.filter((repo) => {
|
||||
const isPrivateAllowed = includePrivate || !repo.private;
|
||||
const isForkAllowed = !skipForks || !repo.fork;
|
||||
return isPrivateAllowed && isForkAllowed;
|
||||
});
|
||||
|
||||
return filteredRepos.map((repo) => ({
|
||||
name: repo.name,
|
||||
fullName: repo.full_name,
|
||||
url: repo.html_url,
|
||||
cloneUrl: repo.clone_url,
|
||||
|
||||
owner: repo.owner.login,
|
||||
organization:
|
||||
repo.owner.type === "Organization" ? repo.owner.login : undefined,
|
||||
|
||||
isPrivate: repo.private,
|
||||
isForked: repo.fork,
|
||||
forkedFrom: (repo as typeof repo & { parent?: { full_name: string } })
|
||||
.parent?.full_name,
|
||||
|
||||
hasIssues: repo.has_issues,
|
||||
isStarred: false,
|
||||
isArchived: repo.archived,
|
||||
|
||||
size: repo.size,
|
||||
hasLFS: false,
|
||||
hasSubmodules: false,
|
||||
|
||||
defaultBranch: repo.default_branch,
|
||||
visibility: (repo.visibility ?? "public") as GitRepo["visibility"],
|
||||
|
||||
status: "imported",
|
||||
lastMirrored: undefined,
|
||||
errorMessage: undefined,
|
||||
|
||||
createdAt: repo.created_at ? new Date(repo.created_at) : new Date(),
|
||||
updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(),
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Error fetching repositories: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGithubStarredRepositories({
|
||||
octokit,
|
||||
config,
|
||||
}: {
|
||||
octokit: Octokit;
|
||||
config: Partial<Config>;
|
||||
}) {
|
||||
try {
|
||||
const starredRepos = await octokit.paginate(
|
||||
octokit.activity.listReposStarredByAuthenticatedUser,
|
||||
{
|
||||
per_page: 100,
|
||||
}
|
||||
);
|
||||
|
||||
return starredRepos.map((repo) => ({
|
||||
name: repo.name,
|
||||
fullName: repo.full_name,
|
||||
url: repo.html_url,
|
||||
cloneUrl: repo.clone_url,
|
||||
|
||||
owner: repo.owner.login,
|
||||
organization:
|
||||
repo.owner.type === "Organization" ? repo.owner.login : undefined,
|
||||
|
||||
isPrivate: repo.private,
|
||||
isForked: repo.fork,
|
||||
forkedFrom: undefined,
|
||||
|
||||
hasIssues: repo.has_issues,
|
||||
isStarred: true,
|
||||
isArchived: repo.archived,
|
||||
|
||||
size: repo.size,
|
||||
hasLFS: false, // Placeholder
|
||||
hasSubmodules: false, // Placeholder
|
||||
|
||||
defaultBranch: repo.default_branch,
|
||||
visibility: (repo.visibility ?? "public") as GitRepo["visibility"],
|
||||
|
||||
status: "imported",
|
||||
lastMirrored: undefined,
|
||||
errorMessage: undefined,
|
||||
|
||||
createdAt: repo.created_at ? new Date(repo.created_at) : new Date(),
|
||||
updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(),
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Error fetching starred repositories: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user github organizations
|
||||
*/
|
||||
export async function getGithubOrganizations({
|
||||
octokit,
|
||||
config,
|
||||
}: {
|
||||
octokit: Octokit;
|
||||
config: Partial<Config>;
|
||||
}): Promise<GitOrg[]> {
|
||||
try {
|
||||
const { data: orgs } = await octokit.orgs.listForAuthenticatedUser({
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const organizations = await Promise.all(
|
||||
orgs.map(async (org) => {
|
||||
const [{ data: orgDetails }, { data: membership }] = await Promise.all([
|
||||
octokit.orgs.get({ org: org.login }),
|
||||
octokit.orgs.getMembershipForAuthenticatedUser({ org: org.login }),
|
||||
]);
|
||||
|
||||
const totalRepos =
|
||||
orgDetails.public_repos + (orgDetails.total_private_repos ?? 0);
|
||||
|
||||
return {
|
||||
name: org.login,
|
||||
avatarUrl: org.avatar_url,
|
||||
membershipRole: membership.role as MembershipRole,
|
||||
isIncluded: false,
|
||||
status: "imported" as RepoStatus,
|
||||
repositoryCount: totalRepos,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return organizations;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Error fetching organizations: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get repositories for a specific organization
|
||||
*/
|
||||
export async function getGithubOrganizationRepositories({
|
||||
octokit,
|
||||
organizationName,
|
||||
}: {
|
||||
octokit: Octokit;
|
||||
organizationName: string;
|
||||
}): Promise<GitRepo[]> {
|
||||
try {
|
||||
const repos = await octokit.paginate(octokit.repos.listForOrg, {
|
||||
org: organizationName,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
return repos.map((repo) => ({
|
||||
name: repo.name,
|
||||
fullName: repo.full_name,
|
||||
url: repo.html_url,
|
||||
cloneUrl: repo.clone_url ?? "",
|
||||
|
||||
owner: repo.owner.login,
|
||||
organization: repo.owner.login,
|
||||
|
||||
isPrivate: repo.private,
|
||||
isForked: repo.fork,
|
||||
forkedFrom: (repo as typeof repo & { parent?: { full_name: string } })
|
||||
.parent?.full_name,
|
||||
|
||||
hasIssues: repo.has_issues ?? false,
|
||||
isStarred: false, // Organization starred repos are separate API
|
||||
isArchived: repo.archived ?? false,
|
||||
|
||||
size: repo.size ?? 0,
|
||||
hasLFS: false,
|
||||
hasSubmodules: false,
|
||||
|
||||
defaultBranch: repo.default_branch ?? "main",
|
||||
visibility: (repo.visibility ?? "public") as GitRepo["visibility"],
|
||||
|
||||
status: "imported",
|
||||
lastMirrored: undefined,
|
||||
errorMessage: undefined,
|
||||
|
||||
createdAt: repo.created_at ? new Date(repo.created_at) : new Date(),
|
||||
updatedAt: repo.updated_at ? new Date(repo.updated_at) : new Date(),
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Error fetching organization repositories: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
53
src/lib/helpers.ts
Normal file
53
src/lib/helpers.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { RepoStatus } from "@/types/Repository";
|
||||
import { db, mirrorJobs } from "./db";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { redisPublisher } from "./redis";
|
||||
|
||||
export async function createMirrorJob({
|
||||
userId,
|
||||
organizationId,
|
||||
organizationName,
|
||||
repositoryId,
|
||||
repositoryName,
|
||||
message,
|
||||
status,
|
||||
details,
|
||||
}: {
|
||||
userId: string;
|
||||
organizationId?: string;
|
||||
organizationName?: string;
|
||||
repositoryId?: string;
|
||||
repositoryName?: string;
|
||||
details?: string;
|
||||
message: string;
|
||||
status: RepoStatus;
|
||||
}) {
|
||||
const jobId = uuidv4();
|
||||
const currentTimestamp = new Date();
|
||||
|
||||
const job = {
|
||||
id: jobId,
|
||||
userId,
|
||||
repositoryId,
|
||||
repositoryName,
|
||||
organizationId,
|
||||
organizationName,
|
||||
configId: uuidv4(),
|
||||
details,
|
||||
message: message,
|
||||
status: status,
|
||||
timestamp: currentTimestamp,
|
||||
};
|
||||
|
||||
try {
|
||||
await db.insert(mirrorJobs).values(job);
|
||||
|
||||
const channel = `mirror-status:${userId}`;
|
||||
await redisPublisher.publish(channel, JSON.stringify(job));
|
||||
|
||||
return jobId;
|
||||
} catch (error) {
|
||||
console.error("Error creating mirror job:", error);
|
||||
throw new Error("Error creating mirror job");
|
||||
}
|
||||
}
|
||||
4
src/lib/redis.ts
Normal file
4
src/lib/redis.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import Redis from "ioredis";
|
||||
|
||||
export const redisPublisher = new Redis(); // For publishing
|
||||
export const redisSubscriber = new Redis(); // For subscribing
|
||||
160
src/lib/rough.ts
Normal file
160
src/lib/rough.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
// this is a temporary file for testing purposes
|
||||
import type { Config } from "@/types/config";
|
||||
|
||||
export async function deleteAllReposInOrg({
|
||||
config,
|
||||
org,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
org: string;
|
||||
}) {
|
||||
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
|
||||
throw new Error("Gitea config is required.");
|
||||
}
|
||||
|
||||
// Step 1: Get all repositories in the organization
|
||||
const repoRes = await fetch(
|
||||
`${config.giteaConfig.url}/api/v1/orgs/${org}/repos`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${config.giteaConfig.token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!repoRes.ok) {
|
||||
console.error(
|
||||
`Failed to fetch repos for org ${org}: ${await repoRes.text()}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const repos = await repoRes.json();
|
||||
|
||||
// Step 2: Delete each repository
|
||||
for (const repo of repos) {
|
||||
const deleteRes = await fetch(
|
||||
`${config.giteaConfig.url}/api/v1/repos/${org}/${repo.name}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `token ${config.giteaConfig.token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!deleteRes.ok) {
|
||||
console.error(
|
||||
`Failed to delete repo ${repo.name}: ${await deleteRes.text()}`
|
||||
);
|
||||
} else {
|
||||
console.log(`Successfully deleted repo ${repo.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteOrg({
|
||||
config,
|
||||
org,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
org: string;
|
||||
}) {
|
||||
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
|
||||
throw new Error("Gitea config is required.");
|
||||
}
|
||||
|
||||
const deleteOrgRes = await fetch(
|
||||
`${config.giteaConfig.url}/api/v1/orgs/${org}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `token ${config.giteaConfig.token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!deleteOrgRes.ok) {
|
||||
console.error(`Failed to delete org ${org}: ${await deleteOrgRes.text()}`);
|
||||
} else {
|
||||
console.log(`Successfully deleted org ${org}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAllOrgs({
|
||||
config,
|
||||
orgs,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
orgs: string[];
|
||||
}) {
|
||||
for (const org of orgs) {
|
||||
console.log(`Starting deletion for org: ${org}`);
|
||||
|
||||
// First, delete all repositories in the organization
|
||||
await deleteAllReposInOrg({ config, org });
|
||||
|
||||
// Then, delete the organization itself
|
||||
await deleteOrg({ config, org });
|
||||
|
||||
console.log(`Finished deletion for org: ${org}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAllReposInGitea({
|
||||
config,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
}) {
|
||||
if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
|
||||
throw new Error("Gitea config is required.");
|
||||
}
|
||||
|
||||
console.log("Fetching all repositories...");
|
||||
|
||||
// Step 1: Get all repositories (user + org repos)
|
||||
const repoRes = await fetch(`${config.giteaConfig.url}/api/v1/user/repos`, {
|
||||
headers: {
|
||||
Authorization: `token ${config.giteaConfig.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!repoRes.ok) {
|
||||
console.error(`Failed to fetch repositories: ${await repoRes.text()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const repos = await repoRes.json();
|
||||
|
||||
if (repos.length === 0) {
|
||||
console.log("No repositories found to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${repos.length} repositories. Starting deletion...`);
|
||||
|
||||
// Step 2: Delete all repositories in parallel
|
||||
await Promise.allSettled(
|
||||
repos.map((repo: any) =>
|
||||
fetch(
|
||||
`${config.giteaConfig?.url}/api/v1/repos/${repo.owner.username}/${repo.name}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `token ${config.giteaConfig?.token}`,
|
||||
},
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
console.error(
|
||||
`Failed to delete repo ${repo.full_name}: ${await res.text()}`
|
||||
);
|
||||
} else {
|
||||
console.log(`Successfully deleted repo ${repo.full_name}`);
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
console.log("Finished deleting all repositories.");
|
||||
}
|
||||
98
src/lib/utils.ts
Normal file
98
src/lib/utils.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import axios from "axios";
|
||||
import type { AxiosError, AxiosRequestConfig } from "axios";
|
||||
import type { RepoStatus } from "@/types/Repository";
|
||||
|
||||
export const API_BASE = "/api";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatDate(date?: Date | string | null): string {
|
||||
if (!date) return "Never";
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(new Date(date));
|
||||
}
|
||||
|
||||
export function truncate(str: string, length: number): string {
|
||||
if (str.length <= length) return str;
|
||||
return str.slice(0, length) + "...";
|
||||
}
|
||||
|
||||
export function safeParse<T>(value: unknown): T | undefined {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return value as T;
|
||||
}
|
||||
|
||||
// Helper function for API requests
|
||||
|
||||
export async function apiRequest<T>(
|
||||
endpoint: string,
|
||||
options: AxiosRequestConfig = {}
|
||||
): Promise<T> {
|
||||
try {
|
||||
const response = await axios<T>(`${API_BASE}${endpoint}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(options.headers || {}),
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
const error = err as AxiosError<{ message?: string }>;
|
||||
|
||||
const message =
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
"An unknown error occurred";
|
||||
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
export const getStatusColor = (status: RepoStatus): string => {
|
||||
switch (status) {
|
||||
case "imported":
|
||||
return "bg-blue-500"; // Info/primary-like
|
||||
case "mirroring":
|
||||
return "bg-yellow-400"; // In progress
|
||||
case "mirrored":
|
||||
return "bg-emerald-500"; // Success
|
||||
case "failed":
|
||||
return "bg-rose-500"; // Error
|
||||
case "syncing":
|
||||
return "bg-indigo-500"; // Sync in progress
|
||||
case "synced":
|
||||
return "bg-teal-500"; // Sync complete
|
||||
default:
|
||||
return "bg-gray-400"; // Unknown/neutral
|
||||
}
|
||||
};
|
||||
|
||||
export const jsonResponse = ({
|
||||
data,
|
||||
status = 200,
|
||||
}: {
|
||||
data: unknown;
|
||||
status?: number;
|
||||
}): Response => {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user