mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-03-26 23:57:58 +03:00
Compare commits
1 Commits
codex/fix-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01a8025140 |
@@ -555,63 +555,6 @@ describe("Enhanced Gitea Operations", () => {
|
|||||||
expect(releaseCall.octokit).toBeDefined();
|
expect(releaseCall.octokit).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("prefers recorded mirroredLocation when owner resolution changes", async () => {
|
|
||||||
mockGetGiteaRepoOwnerAsync.mockImplementation(() => Promise.resolve("ceph"));
|
|
||||||
|
|
||||||
const config: Partial<Config> = {
|
|
||||||
userId: "user123",
|
|
||||||
githubConfig: {
|
|
||||||
username: "testuser",
|
|
||||||
token: "github-token",
|
|
||||||
privateRepositories: false,
|
|
||||||
mirrorStarred: true,
|
|
||||||
},
|
|
||||||
giteaConfig: {
|
|
||||||
url: "https://gitea.example.com",
|
|
||||||
token: "encrypted-token",
|
|
||||||
defaultOwner: "testuser",
|
|
||||||
mirrorReleases: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const repository: Repository = {
|
|
||||||
id: "repo789",
|
|
||||||
name: "test-repo",
|
|
||||||
fullName: "ceph/test-repo",
|
|
||||||
owner: "ceph",
|
|
||||||
cloneUrl: "https://github.com/ceph/test-repo.git",
|
|
||||||
isPrivate: false,
|
|
||||||
isStarred: true,
|
|
||||||
status: repoStatusEnum.parse("mirrored"),
|
|
||||||
visibility: "public",
|
|
||||||
userId: "user123",
|
|
||||||
mirroredLocation: "starred/test-repo",
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await syncGiteaRepoEnhanced(
|
|
||||||
{ config, repository },
|
|
||||||
{
|
|
||||||
getGiteaRepoOwnerAsync: mockGetGiteaRepoOwnerAsync,
|
|
||||||
mirrorGitHubReleasesToGitea: mockMirrorGitHubReleasesToGitea,
|
|
||||||
mirrorGitRepoIssuesToGitea: mockMirrorGitRepoIssuesToGitea,
|
|
||||||
mirrorGitRepoPullRequestsToGitea: mockMirrorGitRepoPullRequestsToGitea,
|
|
||||||
mirrorGitRepoLabelsToGitea: mockMirrorGitRepoLabelsToGitea,
|
|
||||||
mirrorGitRepoMilestonesToGitea: mockMirrorGitRepoMilestonesToGitea,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: true });
|
|
||||||
|
|
||||||
const mirrorSyncCalls = mockHttpPost.mock.calls.filter((call) =>
|
|
||||||
String(call[0]).includes("/mirror-sync")
|
|
||||||
);
|
|
||||||
expect(mirrorSyncCalls).toHaveLength(1);
|
|
||||||
expect(String(mirrorSyncCalls[0][0])).toContain("/api/v1/repos/starred/test-repo/mirror-sync");
|
|
||||||
expect(String(mirrorSyncCalls[0][0])).not.toContain("/api/v1/repos/ceph/test-repo/mirror-sync");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("blocks sync when pre-sync snapshot fails and blocking is enabled", async () => {
|
test("blocks sync when pre-sync snapshot fails and blocking is enabled", async () => {
|
||||||
mockShouldCreatePreSyncBackup = true;
|
mockShouldCreatePreSyncBackup = true;
|
||||||
mockShouldBlockSyncOnBackupFailure = true;
|
mockShouldBlockSyncOnBackupFailure = true;
|
||||||
|
|||||||
@@ -52,41 +52,6 @@ interface GiteaRepoInfo {
|
|||||||
private: boolean;
|
private: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SyncTargetCandidate {
|
|
||||||
owner: string;
|
|
||||||
repoName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMirroredLocation(location?: string | null): SyncTargetCandidate | null {
|
|
||||||
if (!location) return null;
|
|
||||||
|
|
||||||
const trimmed = location.trim();
|
|
||||||
if (!trimmed) return null;
|
|
||||||
|
|
||||||
const slashIndex = trimmed.indexOf("/");
|
|
||||||
if (slashIndex <= 0 || slashIndex === trimmed.length - 1) return null;
|
|
||||||
|
|
||||||
const owner = trimmed.slice(0, slashIndex).trim();
|
|
||||||
const repoName = trimmed.slice(slashIndex + 1).trim();
|
|
||||||
if (!owner || !repoName) return null;
|
|
||||||
|
|
||||||
return { owner, repoName };
|
|
||||||
}
|
|
||||||
|
|
||||||
function dedupeSyncTargets(targets: SyncTargetCandidate[]): SyncTargetCandidate[] {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const deduped: SyncTargetCandidate[] = [];
|
|
||||||
|
|
||||||
for (const target of targets) {
|
|
||||||
const key = `${target.owner}/${target.repoName}`.toLowerCase();
|
|
||||||
if (seen.has(key)) continue;
|
|
||||||
seen.add(key);
|
|
||||||
deduped.push(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
return deduped;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a repository exists in Gitea and return its details
|
* Check if a repository exists in Gitea and return its details
|
||||||
*/
|
*/
|
||||||
@@ -320,78 +285,19 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
})
|
})
|
||||||
.where(eq(repositories.id, repository.id!));
|
.where(eq(repositories.id, repository.id!));
|
||||||
|
|
||||||
// Resolve sync target in a backward-compatible order:
|
// Get the expected owner
|
||||||
// 1) recorded mirroredLocation (actual historical mirror location)
|
|
||||||
// 2) owner derived from current strategy/config
|
|
||||||
const dependencies = deps ?? (await import("./gitea"));
|
const dependencies = deps ?? (await import("./gitea"));
|
||||||
const expectedOwner = await dependencies.getGiteaRepoOwnerAsync({ config, repository });
|
const repoOwner = await dependencies.getGiteaRepoOwnerAsync({ config, repository });
|
||||||
const recordedTarget = parseMirroredLocation(repository.mirroredLocation);
|
|
||||||
const candidateTargets = dedupeSyncTargets([
|
|
||||||
...(recordedTarget ? [recordedTarget] : []),
|
|
||||||
{ owner: expectedOwner, repoName: repository.name },
|
|
||||||
]);
|
|
||||||
|
|
||||||
let repoOwner = expectedOwner;
|
// Check if repo exists and get its info
|
||||||
let repoName = repository.name;
|
const repoInfo = await getGiteaRepoInfo({
|
||||||
let repoInfo: GiteaRepoInfo | null = null;
|
config,
|
||||||
let firstNonMirrorTarget: SyncTargetCandidate | null = null;
|
owner: repoOwner,
|
||||||
|
repoName: repository.name,
|
||||||
for (const target of candidateTargets) {
|
});
|
||||||
const candidateInfo = await getGiteaRepoInfo({
|
|
||||||
config,
|
|
||||||
owner: target.owner,
|
|
||||||
repoName: target.repoName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!candidateInfo) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!candidateInfo.mirror) {
|
|
||||||
if (!firstNonMirrorTarget) {
|
|
||||||
firstNonMirrorTarget = target;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
repoOwner = target.owner;
|
|
||||||
repoName = target.repoName;
|
|
||||||
repoInfo = candidateInfo;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!repoInfo) {
|
if (!repoInfo) {
|
||||||
if (firstNonMirrorTarget) {
|
throw new Error(`Repository ${repository.name} not found in Gitea at ${repoOwner}/${repository.name}`);
|
||||||
console.warn(
|
|
||||||
`[Sync] Repository ${repository.name} exists at ${firstNonMirrorTarget.owner}/${firstNonMirrorTarget.repoName} but is not configured as a mirror`
|
|
||||||
);
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(repositories)
|
|
||||||
.set({
|
|
||||||
status: repoStatusEnum.parse("failed"),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
errorMessage: "Repository exists in Gitea but is not configured as a mirror. Manual intervention required.",
|
|
||||||
})
|
|
||||||
.where(eq(repositories.id, repository.id!));
|
|
||||||
|
|
||||||
await createMirrorJob({
|
|
||||||
userId: config.userId,
|
|
||||||
repositoryId: repository.id,
|
|
||||||
repositoryName: repository.name,
|
|
||||||
message: `Cannot sync ${repository.name}: Not a mirror repository`,
|
|
||||||
details: `Repository ${repository.name} exists in Gitea but is not configured as a mirror. You may need to delete and recreate it as a mirror, or manually configure it as a mirror in Gitea.`,
|
|
||||||
status: "failed",
|
|
||||||
});
|
|
||||||
|
|
||||||
throw new Error(`Repository ${repository.name} is not a mirror. Cannot sync.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`Repository ${repository.name} not found in Gitea. Tried locations: ${candidateTargets
|
|
||||||
.map((t) => `${t.owner}/${t.repoName}`)
|
|
||||||
.join(", ")}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a mirror repository
|
// Check if it's a mirror repository
|
||||||
@@ -436,7 +342,7 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
giteaUrl: config.giteaConfig.url,
|
giteaUrl: config.giteaConfig.url,
|
||||||
giteaToken: decryptedConfig.giteaConfig.token,
|
giteaToken: decryptedConfig.giteaConfig.token,
|
||||||
giteaOwner: repoOwner,
|
giteaOwner: repoOwner,
|
||||||
giteaRepo: repoName,
|
giteaRepo: repository.name,
|
||||||
octokit: fpOctokit,
|
octokit: fpOctokit,
|
||||||
githubOwner: repository.owner,
|
githubOwner: repository.owner,
|
||||||
githubRepo: repository.name,
|
githubRepo: repository.name,
|
||||||
@@ -501,13 +407,13 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
if (shouldBackupForStrategy(backupStrategy, forcePushDetected)) {
|
if (shouldBackupForStrategy(backupStrategy, forcePushDetected)) {
|
||||||
const cloneUrl =
|
const cloneUrl =
|
||||||
repoInfo.clone_url ||
|
repoInfo.clone_url ||
|
||||||
`${config.giteaConfig.url.replace(/\/$/, "")}/${repoOwner}/${repoName}.git`;
|
`${config.giteaConfig.url.replace(/\/$/, "")}/${repoOwner}/${repository.name}.git`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const backupResult = await createPreSyncBundleBackup({
|
const backupResult = await createPreSyncBundleBackup({
|
||||||
config,
|
config,
|
||||||
owner: repoOwner,
|
owner: repoOwner,
|
||||||
repoName,
|
repoName: repository.name,
|
||||||
cloneUrl,
|
cloneUrl,
|
||||||
force: true, // Strategy already decided to backup; skip legacy gate
|
force: true, // Strategy already decided to backup; skip legacy gate
|
||||||
});
|
});
|
||||||
@@ -558,22 +464,22 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
// Update mirror interval if needed
|
// Update mirror interval if needed
|
||||||
if (config.giteaConfig?.mirrorInterval) {
|
if (config.giteaConfig?.mirrorInterval) {
|
||||||
try {
|
try {
|
||||||
console.log(`[Sync] Updating mirror interval for ${repoOwner}/${repoName} to ${config.giteaConfig.mirrorInterval}`);
|
console.log(`[Sync] Updating mirror interval for ${repository.name} to ${config.giteaConfig.mirrorInterval}`);
|
||||||
const updateUrl = `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}`;
|
const updateUrl = `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repository.name}`;
|
||||||
await httpPatch(updateUrl, {
|
await httpPatch(updateUrl, {
|
||||||
mirror_interval: config.giteaConfig.mirrorInterval,
|
mirror_interval: config.giteaConfig.mirrorInterval,
|
||||||
}, {
|
}, {
|
||||||
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||||
});
|
});
|
||||||
console.log(`[Sync] Successfully updated mirror interval for ${repoOwner}/${repoName}`);
|
console.log(`[Sync] Successfully updated mirror interval for ${repository.name}`);
|
||||||
} catch (updateError) {
|
} catch (updateError) {
|
||||||
console.warn(`[Sync] Failed to update mirror interval for ${repoOwner}/${repoName}:`, updateError);
|
console.warn(`[Sync] Failed to update mirror interval for ${repository.name}:`, updateError);
|
||||||
// Continue with sync even if interval update fails
|
// Continue with sync even if interval update fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the sync
|
// Perform the sync
|
||||||
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/mirror-sync`;
|
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repository.name}/mirror-sync`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await httpPost(apiUrl, undefined, {
|
const response = await httpPost(apiUrl, undefined, {
|
||||||
@@ -630,7 +536,7 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
octokit,
|
octokit,
|
||||||
repository,
|
repository,
|
||||||
giteaOwner: repoOwner,
|
giteaOwner: repoOwner,
|
||||||
giteaRepoName: repoName,
|
giteaRepoName: repository.name,
|
||||||
});
|
});
|
||||||
metadataState.components.releases = true;
|
metadataState.components.releases = true;
|
||||||
metadataUpdated = true;
|
metadataUpdated = true;
|
||||||
@@ -662,7 +568,7 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
octokit,
|
octokit,
|
||||||
repository,
|
repository,
|
||||||
giteaOwner: repoOwner,
|
giteaOwner: repoOwner,
|
||||||
giteaRepoName: repoName,
|
giteaRepoName: repository.name,
|
||||||
});
|
});
|
||||||
metadataState.components.issues = true;
|
metadataState.components.issues = true;
|
||||||
metadataState.components.labels = true;
|
metadataState.components.labels = true;
|
||||||
@@ -695,7 +601,7 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
octokit,
|
octokit,
|
||||||
repository,
|
repository,
|
||||||
giteaOwner: repoOwner,
|
giteaOwner: repoOwner,
|
||||||
giteaRepoName: repoName,
|
giteaRepoName: repository.name,
|
||||||
});
|
});
|
||||||
metadataState.components.pullRequests = true;
|
metadataState.components.pullRequests = true;
|
||||||
metadataUpdated = true;
|
metadataUpdated = true;
|
||||||
@@ -725,7 +631,7 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
octokit,
|
octokit,
|
||||||
repository,
|
repository,
|
||||||
giteaOwner: repoOwner,
|
giteaOwner: repoOwner,
|
||||||
giteaRepoName: repoName,
|
giteaRepoName: repository.name,
|
||||||
});
|
});
|
||||||
metadataState.components.labels = true;
|
metadataState.components.labels = true;
|
||||||
metadataUpdated = true;
|
metadataUpdated = true;
|
||||||
@@ -764,7 +670,7 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
octokit,
|
octokit,
|
||||||
repository,
|
repository,
|
||||||
giteaOwner: repoOwner,
|
giteaOwner: repoOwner,
|
||||||
giteaRepoName: repoName,
|
giteaRepoName: repository.name,
|
||||||
});
|
});
|
||||||
metadataState.components.milestones = true;
|
metadataState.components.milestones = true;
|
||||||
metadataUpdated = true;
|
metadataUpdated = true;
|
||||||
@@ -802,7 +708,7 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
lastMirrored: new Date(),
|
lastMirrored: new Date(),
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
mirroredLocation: `${repoOwner}/${repoName}`,
|
mirroredLocation: `${repoOwner}/${repository.name}`,
|
||||||
metadata: metadataUpdated
|
metadata: metadataUpdated
|
||||||
? serializeRepositoryMetadataState(metadataState)
|
? serializeRepositoryMetadataState(metadataState)
|
||||||
: repository.metadata ?? null,
|
: repository.metadata ?? null,
|
||||||
@@ -814,7 +720,7 @@ export async function syncGiteaRepoEnhanced({
|
|||||||
repositoryId: repository.id,
|
repositoryId: repository.id,
|
||||||
repositoryName: repository.name,
|
repositoryName: repository.name,
|
||||||
message: `Sync requested for repository: ${repository.name}`,
|
message: `Sync requested for repository: ${repository.name}`,
|
||||||
details: `Mirror sync was requested for ${repoOwner}/${repoName}.`,
|
details: `Mirror sync was requested for ${repository.name}.`,
|
||||||
status: "synced",
|
status: "synced",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ try {
|
|||||||
activityData = jobs.flatMap((job: any) => {
|
activityData = jobs.flatMap((job: any) => {
|
||||||
// Check if log exists before parsing
|
// Check if log exists before parsing
|
||||||
if (!job.log) {
|
if (!job.log) {
|
||||||
|
console.warn(`Job ${job.id} has no log data`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ try {
|
|||||||
activityData = jobs.flatMap((job: any) => {
|
activityData = jobs.flatMap((job: any) => {
|
||||||
// Check if log exists before parsing
|
// Check if log exists before parsing
|
||||||
if (!job.log) {
|
if (!job.log) {
|
||||||
|
console.warn(`Job ${job.id} has no log data`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -67,4 +68,4 @@ try {
|
|||||||
<body>
|
<body>
|
||||||
<App page='dashboard' client:load />
|
<App page='dashboard' client:load />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
10
www/pnpm-lock.yaml
generated
10
www/pnpm-lock.yaml
generated
@@ -1801,8 +1801,8 @@ packages:
|
|||||||
sisteransi@1.0.5:
|
sisteransi@1.0.5:
|
||||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||||
|
|
||||||
smol-toml@1.6.0:
|
smol-toml@1.6.1:
|
||||||
resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==}
|
resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
source-map-js@1.2.1:
|
||||||
@@ -2109,7 +2109,7 @@ snapshots:
|
|||||||
remark-rehype: 11.1.2
|
remark-rehype: 11.1.2
|
||||||
remark-smartypants: 3.0.2
|
remark-smartypants: 3.0.2
|
||||||
shiki: 4.0.2
|
shiki: 4.0.2
|
||||||
smol-toml: 1.6.0
|
smol-toml: 1.6.1
|
||||||
unified: 11.0.5
|
unified: 11.0.5
|
||||||
unist-util-remove-position: 5.0.0
|
unist-util-remove-position: 5.0.0
|
||||||
unist-util-visit: 5.1.0
|
unist-util-visit: 5.1.0
|
||||||
@@ -2895,7 +2895,7 @@ snapshots:
|
|||||||
rehype: 13.0.2
|
rehype: 13.0.2
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
shiki: 4.0.2
|
shiki: 4.0.2
|
||||||
smol-toml: 1.6.0
|
smol-toml: 1.6.1
|
||||||
svgo: 4.0.1
|
svgo: 4.0.1
|
||||||
tinyclip: 0.1.12
|
tinyclip: 0.1.12
|
||||||
tinyexec: 1.0.2
|
tinyexec: 1.0.2
|
||||||
@@ -4247,7 +4247,7 @@ snapshots:
|
|||||||
|
|
||||||
sisteransi@1.0.5: {}
|
sisteransi@1.0.5: {}
|
||||||
|
|
||||||
smol-toml@1.6.0: {}
|
smol-toml@1.6.1: {}
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user