/** * @file Client calls for the better-conflicts and history-rewrite surface * (server/lib/git-conflicts.js, server/lib/git-rewrite.js): the merge editor's * stage read and save, the reflog, remotes, the rewrite plans, and the rewrite * / remote writes — each a POST /git/ like `api.lanes.gitAction`. Kept * out of api.ts so its own surface stays in one file. Every write fires the * shared `agenthub:git-changed` event once (`emitGitChanged`, gitChanged.ts), * as `api.lanes.gitAction` does, so every open git view reads again. * @author Nguyễn Ngọc Trí Vĩ */ import { gitUrl, request, type GitTarget } from "./api"; import { emitGitChanged } from "./gitChanged"; /** One conflicted file's three sides and the working file it saves over. */ export interface ConflictSides { available: boolean; path: string; operation: "merge" | "cherry-pick" | "revert" | "rebase" | null; branch: string | null; incoming: { sha: string; short: string; subject: string } | null; base: string | null; ours: string | null; theirs: string | null; working: string | null; sha: string | null; usable: boolean; binary: boolean; tooBig: boolean; } export interface ReflogEntry { index: number; selector: string; sha: string; short: string; action: string; message: string; time: number | null; subject: string; } export interface GitRemote { name: string; url: string; pushUrl: string; defaultBranch: string | null; /** Its fetched remote-tracking branches (every one, a local copy or not). */ branches?: string[]; } export interface PlanCommit { sha: string; short: string; subject: string; message: string; pushed: boolean; } interface PlanCommon { available: boolean; head: string; branch: string | null; subdir: boolean; operation: string | null; dirty: boolean; } export interface RebasePlan extends PlanCommon { kind: "rebase"; onto: string; commits: PlanCommit[]; behind: number; pushed: number; } export interface InteractivePlan extends PlanCommon { kind: "interactive"; base: string; merges: boolean; commits: PlanCommit[]; pushed: number; } export interface ResetPlan extends PlanCommon { kind: "reset"; target: string; related: boolean; dropped: number; added: number; pushed: number; } export interface PushPlan extends PlanCommon { kind: "push"; remote: string; remoteBranch: string; remoteSha: string | null; ahead: number; behind: number; protected: boolean; allowed: boolean; } export type TodoAction = "pick" | "reword" | "squash" | "fixup" | "drop"; export interface TodoEntry { sha: string; action: TodoAction; message?: string; } export type ResetMode = "soft" | "mixed" | "keep" | "hard"; export type RewriteAction = | "rebase" | "rebase-continue" | "rebase-skip" | "rebase-abort" | "rebase-interactive" | "reset" | "undo-rewrite" | "force-push" | "remote-branch-delete" | "remote-add" | "remote-rename" | "remote-remove" | "conflict-save"; export interface RewriteResult { action: string; ok: true; output: string; before?: string; after?: string | null; conflicts?: string[]; stopped?: boolean; needsForcePush?: boolean; resolved?: boolean; } export const gitRewrite = { conflict: (t: GitTarget, path: string) => request(gitUrl(t, "/conflict", new URLSearchParams({ path }))), reflog: (t: GitTarget, opts: { ref?: string; skip?: number; limit?: number } = {}) => { const q = new URLSearchParams(); if (opts.ref) q.set("ref", opts.ref); if (opts.skip) q.set("skip", String(opts.skip)); if (opts.limit) q.set("limit", String(opts.limit)); return request<{ available: boolean; ref?: string; entries: ReflogEntry[]; more: boolean }>( gitUrl(t, "/reflog", q) ); }, remotes: (t: GitTarget) => request<{ available: boolean; remotes: GitRemote[] }>(gitUrl(t, "/remotes")), planRebase: (t: GitTarget, onto: string) => request(gitUrl(t, "/rewrite-plan", new URLSearchParams({ kind: "rebase", onto }))), planInteractive: (t: GitTarget, base?: string) => request( gitUrl( t, "/rewrite-plan", new URLSearchParams(base ? { kind: "interactive", base } : { kind: "interactive" }) ) ), planReset: (t: GitTarget, target: string) => request(gitUrl(t, "/rewrite-plan", new URLSearchParams({ kind: "reset", target }))), planPush: (t: GitTarget) => request(gitUrl(t, "/rewrite-plan", new URLSearchParams({ kind: "push" }))), /** POST /git/; fires `agenthub:git-changed` once after it answers (ok or not). */ run: async (t: GitTarget, action: RewriteAction, body: Record = {}) => { try { return await request(gitUrl(t, `/${action}`), { method: "POST", body: JSON.stringify(body), }); } finally { emitGitChanged(typeof t === "number" ? t : t.lane, action); } }, }; /** The error message of a failed call, for an inline alert. */ export function errText(e: unknown): string { return e instanceof Error ? e.message : String(e); } /** The error code of a failed call (`ApiError.code` / `.status` shape). */ export function errCode(e: unknown): string | undefined { const c = (e as { code?: unknown })?.code; return typeof c === "string" ? c : undefined; }