import { repoDb } from "../scoped-db"; import { locate } from "../scoped-db/locators"; import { DEFAULT_THRESHOLDS } from "./decide"; import type { Thresholds } from "./decide"; import { notifyMergeFixResult } from "../notify/mergefix"; import type { MergeFixMode } from "./proposal"; export type RepoMergeFixSettings = { queueEnabled: boolean; allowPrivate: boolean; autoApply: boolean; thresholds: Thresholds; }; type SettingsRow = { queue_enabled: number; allow_private: number; auto_apply: number; min_confidence: number; min_margin: number; }; export async function loadRepoSettings(repoId: string): Promise { const row = await repoDb(repoId).first("SELECT * FROM repo_mergefix_settings WHERE repo_id = ?", repoId); return { queueEnabled: row?.queue_enabled === 1, allowPrivate: row?.allow_private === 1, autoApply: row?.auto_apply === 1, thresholds: row ? { confidence: row.min_confidence, margin: row.min_margin } : DEFAULT_THRESHOLDS, }; } export async function saveRepoSettings(repoId: string, settings: RepoMergeFixSettings): Promise { await repoDb(repoId).run( `INSERT INTO repo_mergefix_settings (repo_id, queue_enabled, allow_private, auto_apply, min_confidence, min_margin, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (repo_id) DO UPDATE SET queue_enabled = excluded.queue_enabled, allow_private = excluded.allow_private, auto_apply = excluded.auto_apply, min_confidence = excluded.min_confidence, min_margin = excluded.min_margin, updated_at = excluded.updated_at`, repoId, settings.queueEnabled ? 1 : 0, settings.allowPrivate ? 1 : 0, settings.autoApply ? 1 : 0, settings.thresholds.confidence, settings.thresholds.margin, Date.now(), ); } export type RunRow = { id: string; pull_issue_id: string; repo_id: string; account_id: string; actor_id: string; trigger: "button" | "queue"; status: "queued" | "running" | "applying" | "applied" | "suggested" | "failed"; head_sha: string; base_sha: string | null; commit_sha: string | null; calls: number; result_json: string | null; error: string | null; queue_method: "merge" | "squash" | null; requeue_status: "waiting" | "enqueued" | "gave_up" | null; requeue_attempts: number; requeue_error: string | null; mode: MergeFixMode; stale: number; applied_by: string | null; applied_files: string | null; created_at: number; updated_at: number; }; /** Runs are addressed by id alone (workflow params), so their repository comes from the locator. */ export async function loadRun(id: string): Promise { const repoId = await locate("mergefix_run", id); return repoId ? repoDb(repoId).first("SELECT * FROM mergefix_runs WHERE id = ?", id) : null; } export async function latestRuns(pull: { repo_id: string; issue_id: string }, limit = 5): Promise { return repoDb(pull.repo_id).all( "SELECT * FROM mergefix_runs WHERE pull_issue_id = ? ORDER BY created_at DESC LIMIT ?", pull.issue_id, limit, ); } export const ACTIVE_STATUSES: RunRow["status"][] = ["queued", "running", "applying"]; /** Status transitions are compare-and-set on `from`, so a late writer can never resurrect a settled run. */ export async function updateRun( id: string, from: RunRow["status"][], fields: Partial>, ): Promise { const repoId = await locate("mergefix_run", id); if (!repoId) return false; const entries = Object.entries({ ...fields, updated_at: Date.now() }); const result = await repoDb(repoId).run( `UPDATE mergefix_runs SET ${entries.map(([key]) => `${key} = ?`).join(", ")} WHERE id = ? AND status IN (${from.map(() => "?").join(", ")})`, ...entries.map(([, value]) => value ?? null), id, ...from, ); const changed = (result.meta.changes ?? 0) > 0; if (changed) await notifyMergeFixResult(repoId, id, fields.status); return changed; }