mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-08-11 03:12:54 +02:00
Config saves now preserve every field the settings form doesn't expose (mirror interval and other env-only options) instead of resetting them to defaults. Fixes #338
This commit is contained in:
@@ -191,3 +191,69 @@ test("DB row missing starredDuplicateStrategy defaults to suffix on read", () =>
|
||||
const ui = mapDbToUiConfig({ githubConfig: { owner: "octo", token: "" } });
|
||||
expect(ui.githubConfig.starredDuplicateStrategy).toBe("suffix");
|
||||
});
|
||||
|
||||
// Regression for #338: saving any setting from the Configuration page reset
|
||||
// giteaConfig.mirrorInterval (set via GITEA_MIRROR_INTERVAL) back to "8h"
|
||||
// because the mapper hardcoded the default instead of preserving the stored value.
|
||||
test("mapUiToDbConfig preserves env-configured mirrorInterval on save", () => {
|
||||
const ui = buildMinimalUiConfigs();
|
||||
const db = mapUiToDbConfig(ui.githubConfig, ui.giteaConfig, ui.mirrorOptions, ui.advancedOptions, {
|
||||
giteaConfig: { mirrorInterval: "10m" },
|
||||
});
|
||||
expect(db.giteaConfig.mirrorInterval).toBe("10m");
|
||||
});
|
||||
|
||||
test("mapUiToDbConfig defaults mirrorInterval to 8h without existing config", () => {
|
||||
const ui = buildMinimalUiConfigs();
|
||||
const db = mapUiToDbConfig(ui.githubConfig, ui.giteaConfig, ui.mirrorOptions, ui.advancedOptions);
|
||||
expect(db.giteaConfig.mirrorInterval).toBe("8h");
|
||||
});
|
||||
|
||||
test("mapUiToDbConfig preserves non-UI fields from existing config on save", () => {
|
||||
const ui = buildMinimalUiConfigs();
|
||||
const db = mapUiToDbConfig(ui.githubConfig, ui.giteaConfig, ui.mirrorOptions, ui.advancedOptions, {
|
||||
githubConfig: { type: "organization", includeArchived: true, includePublic: false },
|
||||
giteaConfig: {
|
||||
createOrg: false,
|
||||
templateOwner: "templates",
|
||||
templateRepo: "base",
|
||||
addTopics: false,
|
||||
topicPrefix: "gh-",
|
||||
preserveVisibility: true,
|
||||
},
|
||||
});
|
||||
expect(db.githubConfig.type).toBe("organization");
|
||||
expect(db.githubConfig.includeArchived).toBe(true);
|
||||
expect(db.githubConfig.includePublic).toBe(false);
|
||||
expect(db.giteaConfig.createOrg).toBe(false);
|
||||
expect(db.giteaConfig.templateOwner).toBe("templates");
|
||||
expect(db.giteaConfig.templateRepo).toBe("base");
|
||||
expect(db.giteaConfig.addTopics).toBe(false);
|
||||
expect(db.giteaConfig.topicPrefix).toBe("gh-");
|
||||
expect(db.giteaConfig.preserveVisibility).toBe(true);
|
||||
});
|
||||
|
||||
test("mapUiToDbConfig keeps env-configured full-copy forkStrategy when forks are included", () => {
|
||||
const ui = buildMinimalUiConfigs();
|
||||
const db = mapUiToDbConfig(ui.githubConfig, ui.giteaConfig, ui.mirrorOptions, ui.advancedOptions, {
|
||||
giteaConfig: { forkStrategy: "full-copy" },
|
||||
});
|
||||
expect(db.giteaConfig.forkStrategy).toBe("full-copy");
|
||||
});
|
||||
|
||||
test("mapUiToDbConfig lets skipForks override a stored forkStrategy", () => {
|
||||
const ui = buildMinimalUiConfigs();
|
||||
const withSkip: AdvancedOptions = { ...ui.advancedOptions, skipForks: true };
|
||||
const db = mapUiToDbConfig(ui.githubConfig, ui.giteaConfig, ui.mirrorOptions, withSkip, {
|
||||
giteaConfig: { forkStrategy: "full-copy" },
|
||||
});
|
||||
expect(db.giteaConfig.forkStrategy).toBe("skip");
|
||||
});
|
||||
|
||||
test("mapUiToDbConfig resets a stale skip forkStrategy to reference when skipForks is unchecked", () => {
|
||||
const ui = buildMinimalUiConfigs();
|
||||
const db = mapUiToDbConfig(ui.githubConfig, ui.giteaConfig, ui.mirrorOptions, ui.advancedOptions, {
|
||||
giteaConfig: { forkStrategy: "skip" },
|
||||
});
|
||||
expect(db.giteaConfig.forkStrategy).toBe("reference");
|
||||
});
|
||||
|
||||
@@ -51,28 +51,37 @@ function normalizeOrgList(orgs: string[] | undefined): string[] {
|
||||
|
||||
/**
|
||||
* Maps UI config structure to database schema structure
|
||||
*
|
||||
* `existing` is the stored DB config (if any). Fields the Configuration form
|
||||
* doesn't expose (mirrorInterval, topics, templates, ...) are preserved from it
|
||||
* so a UI save can't silently reset values configured via environment variables
|
||||
* (e.g. GITEA_MIRROR_INTERVAL, see issue #338).
|
||||
*/
|
||||
export function mapUiToDbConfig(
|
||||
githubConfig: GitHubConfig,
|
||||
giteaConfig: GiteaConfig,
|
||||
mirrorOptions: MirrorOptions,
|
||||
advancedOptions: AdvancedOptions
|
||||
advancedOptions: AdvancedOptions,
|
||||
existing?: {
|
||||
githubConfig?: Partial<DbGitHubConfig>;
|
||||
giteaConfig?: Partial<DbGiteaConfig>;
|
||||
}
|
||||
): { githubConfig: DbGitHubConfig; giteaConfig: DbGiteaConfig } {
|
||||
// Map GitHub config to match database schema fields
|
||||
const dbGithubConfig: DbGitHubConfig = {
|
||||
// Map username to owner field
|
||||
owner: githubConfig.username,
|
||||
type: "personal", // Default to personal, could be made configurable
|
||||
type: existing?.githubConfig?.type || "personal", // Not in UI; preserve stored value
|
||||
token: githubConfig.token || "",
|
||||
|
||||
|
||||
// Map checkbox fields with proper names
|
||||
includeStarred: githubConfig.mirrorStarred,
|
||||
includePrivate: githubConfig.privateRepositories,
|
||||
includeCollaboratorRepos: githubConfig.includeCollaboratorRepos ?? true,
|
||||
includeForks: !advancedOptions.skipForks, // Note: UI has skipForks, DB has includeForks
|
||||
skipForks: advancedOptions.skipForks, // Add skipForks field
|
||||
includeArchived: false, // Not in UI yet, default to false
|
||||
includePublic: true, // Not in UI yet, default to true
|
||||
includeArchived: existing?.githubConfig?.includeArchived ?? false, // Not in UI; preserve stored value
|
||||
includePublic: existing?.githubConfig?.includePublic ?? true, // Not in UI; preserve stored value
|
||||
|
||||
// Organization related fields — opt-in allowlist (empty = all org repos)
|
||||
includeOrganizations: normalizeOrgList(githubConfig.includeOrganizations),
|
||||
@@ -102,28 +111,35 @@ export function mapUiToDbConfig(
|
||||
organization: giteaConfig.organization, // Add organization field
|
||||
preserveOrgStructure: giteaConfig.mirrorStrategy === "preserve" || giteaConfig.mirrorStrategy === "mixed", // Add preserveOrgStructure field
|
||||
|
||||
// Mirror interval and options
|
||||
mirrorInterval: "8h", // Default value, could be made configurable
|
||||
// Mirror interval — not in UI; preserve the stored value so a save doesn't
|
||||
// reset an env-configured GITEA_MIRROR_INTERVAL back to the default (#338)
|
||||
mirrorInterval: existing?.giteaConfig?.mirrorInterval || "8h",
|
||||
lfs: mirrorOptions.mirrorLFS || false, // LFS mirroring option
|
||||
wiki: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
||||
|
||||
|
||||
// Visibility settings
|
||||
visibility: giteaConfig.visibility || "default",
|
||||
preserveVisibility: false, // This should be a separate field, not the same as preserveOrgStructure
|
||||
|
||||
// Organization creation
|
||||
createOrg: true, // Default to true
|
||||
|
||||
// Template settings (not in UI yet)
|
||||
templateOwner: undefined,
|
||||
templateRepo: undefined,
|
||||
|
||||
// Topics
|
||||
addTopics: true, // Default to true
|
||||
topicPrefix: undefined,
|
||||
|
||||
// Fork strategy
|
||||
forkStrategy: advancedOptions.skipForks ? "skip" : "reference",
|
||||
preserveVisibility: existing?.giteaConfig?.preserveVisibility ?? false, // Not in UI; preserve stored value
|
||||
|
||||
// Organization creation — not in UI; preserve stored value
|
||||
createOrg: existing?.giteaConfig?.createOrg ?? true,
|
||||
|
||||
// Template settings (not in UI yet) — preserve stored values
|
||||
templateOwner: existing?.giteaConfig?.templateOwner,
|
||||
templateRepo: existing?.giteaConfig?.templateRepo,
|
||||
|
||||
// Topics — not in UI; preserve stored values
|
||||
addTopics: existing?.giteaConfig?.addTopics ?? true,
|
||||
topicPrefix: existing?.giteaConfig?.topicPrefix,
|
||||
|
||||
// Fork strategy — skipForks is the only UI control; keep an env-configured
|
||||
// "full-copy" instead of downgrading it, but reset a stale "skip" once the
|
||||
// user unchecks skipForks.
|
||||
forkStrategy: advancedOptions.skipForks
|
||||
? "skip"
|
||||
: existing?.giteaConfig?.forkStrategy && existing.giteaConfig.forkStrategy !== "skip"
|
||||
? existing.giteaConfig.forkStrategy
|
||||
: "reference",
|
||||
|
||||
// Mirror options from UI
|
||||
issueConcurrency: giteaConfig.issueConcurrency ?? 3,
|
||||
|
||||
@@ -94,38 +94,48 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
|
||||
const existingConfig = existingConfigResult[0];
|
||||
|
||||
// Parse the stored configs once — used both to preserve fields the
|
||||
// Configuration form doesn't expose (e.g. env-configured mirrorInterval,
|
||||
// see issue #338) and to preserve tokens when the form submits them empty.
|
||||
let existingGithub: Record<string, any> | undefined;
|
||||
let existingGitea: Record<string, any> | undefined;
|
||||
if (existingConfig) {
|
||||
try {
|
||||
existingGithub =
|
||||
typeof existingConfig.githubConfig === "string"
|
||||
? JSON.parse(existingConfig.githubConfig)
|
||||
: existingConfig.githubConfig;
|
||||
|
||||
existingGitea =
|
||||
typeof existingConfig.giteaConfig === "string"
|
||||
? JSON.parse(existingConfig.giteaConfig)
|
||||
: existingConfig.giteaConfig;
|
||||
} catch (parseError) {
|
||||
console.error("Failed to parse existing config:", parseError);
|
||||
}
|
||||
}
|
||||
|
||||
// Map UI structure to database schema structure first
|
||||
const { githubConfig: mappedGithubConfig, giteaConfig: mappedGiteaConfig } = mapUiToDbConfig(
|
||||
githubConfig,
|
||||
giteaConfig,
|
||||
mirrorOptions,
|
||||
advancedOptions
|
||||
advancedOptions,
|
||||
{ githubConfig: existingGithub, giteaConfig: existingGitea }
|
||||
);
|
||||
|
||||
|
||||
// Preserve tokens if fields are empty
|
||||
if (existingConfig) {
|
||||
try {
|
||||
const existingGithub =
|
||||
typeof existingConfig.githubConfig === "string"
|
||||
? JSON.parse(existingConfig.githubConfig)
|
||||
: existingConfig.githubConfig;
|
||||
|
||||
const existingGitea =
|
||||
typeof existingConfig.giteaConfig === "string"
|
||||
? JSON.parse(existingConfig.giteaConfig)
|
||||
: existingConfig.giteaConfig;
|
||||
|
||||
// Decrypt existing tokens before preserving
|
||||
if (!mappedGithubConfig.token && existingGithub.token) {
|
||||
mappedGithubConfig.token = decrypt(existingGithub.token);
|
||||
}
|
||||
|
||||
if (!mappedGiteaConfig.token && existingGitea.token) {
|
||||
mappedGiteaConfig.token = decrypt(existingGitea.token);
|
||||
}
|
||||
} catch (tokenError) {
|
||||
console.error("Failed to preserve tokens:", tokenError);
|
||||
try {
|
||||
// Decrypt existing tokens before preserving
|
||||
if (!mappedGithubConfig.token && existingGithub?.token) {
|
||||
mappedGithubConfig.token = decrypt(existingGithub.token);
|
||||
}
|
||||
|
||||
if (!mappedGiteaConfig.token && existingGitea?.token) {
|
||||
mappedGiteaConfig.token = decrypt(existingGitea.token);
|
||||
}
|
||||
} catch (tokenError) {
|
||||
console.error("Failed to preserve tokens:", tokenError);
|
||||
}
|
||||
|
||||
// Encrypt tokens before saving
|
||||
|
||||
Reference in New Issue
Block a user