mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-03-14 22:43:02 +03:00
Compare commits
2 Commits
main
...
codex/issu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62c97ca04c | ||
|
|
dd87ddfa3d |
346
src/lib/gitea.ts
346
src/lib/gitea.ts
@@ -374,6 +374,161 @@ export const checkRepoLocation = async ({
|
||||
return { present: false, actualOwner: expectedOwner };
|
||||
};
|
||||
|
||||
const sanitizeTopicForGitea = (topic: string): string =>
|
||||
topic
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+/, "")
|
||||
.replace(/-+$/, "");
|
||||
|
||||
const normalizeTopicsForGitea = (
|
||||
topics: string[],
|
||||
topicPrefix?: string
|
||||
): string[] => {
|
||||
const normalizedPrefix = topicPrefix ? sanitizeTopicForGitea(topicPrefix) : "";
|
||||
const transformedTopics = topics
|
||||
.map((topic) => sanitizeTopicForGitea(topic))
|
||||
.filter((topic) => topic.length > 0)
|
||||
.map((topic) => (normalizedPrefix ? `${normalizedPrefix}-${topic}` : topic));
|
||||
|
||||
return [...new Set(transformedTopics)];
|
||||
};
|
||||
|
||||
const getSourceRepositoryCoordinates = (repository: Repository) => {
|
||||
const delimiterIndex = repository.fullName.indexOf("/");
|
||||
if (
|
||||
delimiterIndex > 0 &&
|
||||
delimiterIndex < repository.fullName.length - 1
|
||||
) {
|
||||
return {
|
||||
owner: repository.fullName.slice(0, delimiterIndex),
|
||||
repo: repository.fullName.slice(delimiterIndex + 1),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
owner: repository.owner,
|
||||
repo: repository.name,
|
||||
};
|
||||
};
|
||||
|
||||
const fetchGitHubTopics = async ({
|
||||
octokit,
|
||||
repository,
|
||||
}: {
|
||||
octokit: Octokit;
|
||||
repository: Repository;
|
||||
}): Promise<string[] | null> => {
|
||||
const { owner, repo } = getSourceRepositoryCoordinates(repository);
|
||||
|
||||
try {
|
||||
const response = await octokit.request("GET /repos/{owner}/{repo}/topics", {
|
||||
owner,
|
||||
repo,
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
},
|
||||
});
|
||||
|
||||
const names = (response.data as { names?: unknown }).names;
|
||||
if (!Array.isArray(names)) {
|
||||
console.warn(
|
||||
`[Metadata] Unexpected topics payload for ${repository.fullName}; skipping topic sync.`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return names.filter((topic): topic is string => typeof topic === "string");
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[Metadata] Failed to fetch topics from GitHub for ${repository.fullName}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const syncRepositoryMetadataToGitea = async ({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
giteaOwner,
|
||||
giteaRepoName,
|
||||
giteaToken,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
octokit: Octokit;
|
||||
repository: Repository;
|
||||
giteaOwner: string;
|
||||
giteaRepoName: string;
|
||||
giteaToken: string;
|
||||
}): Promise<void> => {
|
||||
const giteaBaseUrl = config.giteaConfig?.url;
|
||||
if (!giteaBaseUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const repoApiUrl = `${giteaBaseUrl}/api/v1/repos/${giteaOwner}/${giteaRepoName}`;
|
||||
const authHeaders = {
|
||||
Authorization: `token ${giteaToken}`,
|
||||
};
|
||||
const description = repository.description?.trim() || "";
|
||||
|
||||
try {
|
||||
await httpPatch(
|
||||
repoApiUrl,
|
||||
{ description },
|
||||
authHeaders
|
||||
);
|
||||
console.log(
|
||||
`[Metadata] Synced description for ${repository.fullName} to ${giteaOwner}/${giteaRepoName}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[Metadata] Failed to sync description for ${repository.fullName} to ${giteaOwner}/${giteaRepoName}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
if (config.giteaConfig?.addTopics === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceTopics = await fetchGitHubTopics({ octokit, repository });
|
||||
if (sourceTopics === null) {
|
||||
console.warn(
|
||||
`[Metadata] Skipping topic sync for ${repository.fullName} because GitHub topics could not be fetched.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const topics = normalizeTopicsForGitea(
|
||||
sourceTopics,
|
||||
config.giteaConfig?.topicPrefix
|
||||
);
|
||||
|
||||
try {
|
||||
await httpPut(
|
||||
`${repoApiUrl}/topics`,
|
||||
{ topics },
|
||||
authHeaders
|
||||
);
|
||||
console.log(
|
||||
`[Metadata] Synced ${topics.length} topic(s) for ${repository.fullName} to ${giteaOwner}/${giteaRepoName}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[Metadata] Failed to sync topics for ${repository.fullName} to ${giteaOwner}/${giteaRepoName}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const mirrorGithubRepoToGitea = async ({
|
||||
octokit,
|
||||
repository,
|
||||
@@ -465,36 +620,66 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
});
|
||||
|
||||
if (isExisting) {
|
||||
console.log(
|
||||
`Repository ${targetRepoName} already exists in Gitea under ${repoOwner}. 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: `${repoOwner}/${targetRepoName}`,
|
||||
})
|
||||
.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 under ${repoOwner} and database status was updated.`,
|
||||
status: "mirrored",
|
||||
const { getGiteaRepoInfo, handleExistingNonMirrorRepo } = await import("./gitea-enhanced");
|
||||
const existingRepoInfo = await getGiteaRepoInfo({
|
||||
config,
|
||||
owner: repoOwner,
|
||||
repoName: targetRepoName,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Repository ${repository.name} database status updated to mirrored`
|
||||
);
|
||||
return;
|
||||
if (existingRepoInfo && !existingRepoInfo.mirror) {
|
||||
console.log(`Repository ${targetRepoName} exists but is not a mirror. Handling...`);
|
||||
await handleExistingNonMirrorRepo({
|
||||
config,
|
||||
repository,
|
||||
repoInfo: existingRepoInfo,
|
||||
strategy: "delete", // Can be configured: "skip", "delete", or "rename"
|
||||
});
|
||||
} else if (existingRepoInfo?.mirror) {
|
||||
console.log(
|
||||
`Repository ${targetRepoName} already exists in Gitea under ${repoOwner}. Updating database status.`
|
||||
);
|
||||
|
||||
await syncRepositoryMetadataToGitea({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
giteaOwner: repoOwner,
|
||||
giteaRepoName: targetRepoName,
|
||||
giteaToken: decryptedConfig.giteaConfig.token,
|
||||
});
|
||||
|
||||
// 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: `${repoOwner}/${targetRepoName}`,
|
||||
})
|
||||
.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 under ${repoOwner} and database status was updated.`,
|
||||
status: "mirrored",
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Repository ${repository.name} database status updated to mirrored`
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
console.warn(
|
||||
`[Mirror] Repository ${repoOwner}/${targetRepoName} exists but mirror status could not be verified. Continuing with mirror creation flow.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Mirroring repository ${repository.name}`);
|
||||
@@ -648,6 +833,15 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
}
|
||||
);
|
||||
|
||||
await syncRepositoryMetadataToGitea({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
giteaOwner: repoOwner,
|
||||
giteaRepoName: targetRepoName,
|
||||
giteaToken: decryptedConfig.giteaConfig.token,
|
||||
});
|
||||
|
||||
const metadataState = parseRepositoryMetadataState(repository.metadata);
|
||||
let metadataUpdated = false;
|
||||
const skipMetadataForStarred =
|
||||
@@ -1094,36 +1288,66 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
});
|
||||
|
||||
if (isExisting) {
|
||||
console.log(
|
||||
`Repository ${targetRepoName} 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}/${targetRepoName}`,
|
||||
})
|
||||
.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 ${targetRepoName} already exists in Gitea organization ${orgName}`,
|
||||
details: `Repository ${targetRepoName} was found to already exist in Gitea organization ${orgName} and database status was updated.`,
|
||||
status: "mirrored",
|
||||
const { getGiteaRepoInfo, handleExistingNonMirrorRepo } = await import("./gitea-enhanced");
|
||||
const existingRepoInfo = await getGiteaRepoInfo({
|
||||
config,
|
||||
owner: orgName,
|
||||
repoName: targetRepoName,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Repository ${targetRepoName} database status updated to mirrored in organization ${orgName}`
|
||||
);
|
||||
return;
|
||||
if (existingRepoInfo && !existingRepoInfo.mirror) {
|
||||
console.log(`Repository ${targetRepoName} exists but is not a mirror. Handling...`);
|
||||
await handleExistingNonMirrorRepo({
|
||||
config,
|
||||
repository,
|
||||
repoInfo: existingRepoInfo,
|
||||
strategy: "delete", // Can be configured: "skip", "delete", or "rename"
|
||||
});
|
||||
} else if (existingRepoInfo?.mirror) {
|
||||
console.log(
|
||||
`Repository ${targetRepoName} already exists in Gitea organization ${orgName}. Updating database status.`
|
||||
);
|
||||
|
||||
await syncRepositoryMetadataToGitea({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
giteaOwner: orgName,
|
||||
giteaRepoName: targetRepoName,
|
||||
giteaToken: decryptedConfig.giteaConfig.token,
|
||||
});
|
||||
|
||||
// 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}/${targetRepoName}`,
|
||||
})
|
||||
.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 ${targetRepoName} already exists in Gitea organization ${orgName}`,
|
||||
details: `Repository ${targetRepoName} was found to already exist in Gitea organization ${orgName} and database status was updated.`,
|
||||
status: "mirrored",
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Repository ${targetRepoName} database status updated to mirrored in organization ${orgName}`
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
console.warn(
|
||||
`[Mirror] Repository ${orgName}/${targetRepoName} exists but mirror status could not be verified. Continuing with mirror creation flow.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
@@ -1182,6 +1406,7 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
wiki: shouldMirrorWiki || false,
|
||||
lfs: config.giteaConfig?.lfs || false,
|
||||
private: repository.isPrivate,
|
||||
description: repository.description?.trim() || "",
|
||||
};
|
||||
|
||||
// Add authentication for private repositories
|
||||
@@ -1204,6 +1429,15 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
}
|
||||
);
|
||||
|
||||
await syncRepositoryMetadataToGitea({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
giteaOwner: orgName,
|
||||
giteaRepoName: targetRepoName,
|
||||
giteaToken: decryptedConfig.giteaConfig.token,
|
||||
});
|
||||
|
||||
const metadataState = parseRepositoryMetadataState(repository.metadata);
|
||||
let metadataUpdated = false;
|
||||
const skipMetadataForStarred =
|
||||
|
||||
Reference in New Issue
Block a user