/** * Git state probe for project locations (`nexus_project_git_state`, T12511). * * For every location of every project on THIS device, record branch, HEAD, * detached/shallow, dirty and untracked counts, upstream, ahead/behind, remote * URL and remote head, and when the remote was last fetched. Other devices * sharing the store read those rows to answer "which machines is this project * on, where, and in what state". * * ## Bounded * * - Locations are probed {@link GIT_STATE_DEFAULTS.concurrency} at a time. * - Every git call of one location shares ONE deadline ({@link GitStateProbeOptions.timeoutMs}). * Git runs in its own process group, and on expiry the whole group is * SIGKILLed — a credential helper or ssh child that inherited git's pipes * would otherwise keep the call alive after git itself died. * - `GIT_TERMINAL_PROMPT=0` and `GIT_OPTIONAL_LOCKS=0`: git never waits on a * prompt, and `status` never takes `index.lock` (so it neither blocks nor is * blocked by a concurrent `git` in the same checkout). * * ## No network unless asked * * `git fetch` runs only with `fetch: true`. Otherwise ahead/behind and the * remote head come from the local tracking ref — i.e. the LAST fetch — and * `remoteFetchedAt` is that fetch's instant (the newest `FETCH_HEAD` mtime of * the worktree and common git dirs). `remoteStale` is set when that instant is * unknown or older than {@link GitStateProbeOptions.staleAfterMs}, so a * `behind: 0` is never presented as current on the strength of an old fetch. * * ## Errors are rows * * A location that cannot be probed — missing directory, EACCES, not a git * repo, timeout, git failure — still yields a row, with `probeErrorCode` and * `probeError` set and every field that WAS learned kept. Nothing throws out * of {@link probeGitState}; nothing is filtered out of {@link probeGitStates}. * * @task T12511 * @epic T12496 */ import type { NexusProjectGitState, NexusProjectsStatusParams, NexusProjectsStatusResult } from '@cleocode/contracts'; import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite'; import { GitOutputBudget } from '../git/bounded-git.js'; /** Defaults and limits for the git state probe. */ export declare const GIT_STATE_DEFAULTS: { /** Locations probed at once. */ readonly concurrency: 8; /** Upper bound on `concurrency`. */ readonly maxConcurrency: 64; /** Per-location budget without a fetch, ms. */ readonly timeoutMs: 10000; /** Per-location budget with a fetch, ms. */ readonly fetchTimeoutMs: 30000; /** A fetch older than this is stale, ms (24 h). */ readonly staleAfterMs: number; /** Git output held in memory at once across the whole run, bytes. */ readonly runOutputBudgetBytes: number; }; /** One location to probe. */ export interface GitProbeTarget { /** Project id. */ projectId: string; /** Device the location is on. */ deviceId: string; /** Location path. */ path: string; /** Registered project name, when known. */ projectName?: string | null; } /** Options for {@link probeGitState} / {@link probeGitStates}. */ export interface GitStateProbeOptions { /** Run `git fetch` first. Default `false`. */ fetch?: boolean; /** Budget shared by every git call of ONE location, ms. */ timeoutMs?: number; /** A fetch older than this is `remoteStale`, ms. */ staleAfterMs?: number; /** Locations probed at once ({@link probeGitStates} only). */ concurrency?: number; /** Clock (tests). */ now?: () => Date; /** Git executable (tests substitute a hanging one). Default `git`. */ gitBin?: string; /** Cap on one git call's output, bytes. Default 8 MiB. */ maxOutputBytes?: number; /** Run-wide in-flight output budget shared by every location. */ budget?: GitOutputBudget; } /** Parsed `git status --porcelain=v2 --branch -z`. */ interface ParsedStatus { headSha: string | null; branch: string | null; detached: boolean; upstream: string | null; ahead: number | null; behind: number | null; dirtyCount: number; untrackedCount: number; } /** * Parse `git status --porcelain=v2 --branch -z`. * * @param raw - NUL-separated status output. * @returns Branch headers and entry counts. */ export declare function parsePorcelainV2Status(raw: string): ParsedStatus; /** * Remove credentials from a remote URL before it is stored or shown. * * - `scheme://user:pass@host/…` (http, https, ftp, git, …) → `scheme://host/…`: * for these schemes even a bare user is typically a token. * - `ssh://user:pass@host/…` → `ssh://user@host/…`: the ssh login (`git`) is not * a secret; a password is. * - scp-style `user:pass@host:path` → `user@host:path`. * - A query string or fragment is dropped (`?access_token=…`). * - Local paths and `file://` are otherwise unchanged. * * @param raw - Configured remote URL. * @returns The URL without secrets. * @example * ```ts * redactRemoteUrl('https://alice:ghp_x@github.com/o/r.git'); // 'https://github.com/o/r.git' * ``` */ export declare function redactRemoteUrl(raw: string): string; /** * Apply {@link redactRemoteUrl} to every URL embedded in free text (git's * stderr quotes the remote URL verbatim, credentials included). * * @param text - Message text. * @returns The text with URL credentials removed. */ export declare function redactUrlsInText(text: string): string; /** * Parse `git log --no-walk=unsorted --format='%H %cI' ` output into a * map of commit sha → ISO 8601 UTC committer instant. A date git printed with * an offset (`%cI`) is normalized to `Z`, so stored instants sort and compare * as text (§4 canonical TEXT). An unparsable date maps to `null` (the sha is * still listed). * * @param raw - `git log` stdout, one ` ` line per distinct commit. * @returns Committer instants keyed by sha, in output order. * @example * ```ts * parseCommitDates('abc 2026-09-29T08:07:21-07:00\n').get('abc'); // '2026-09-29T15:07:21.000Z' * ``` */ export declare function parseCommitDates(raw: string): Map; /** * Probe one location. Never throws: every failure is a row with * `probeErrorCode` set. * * @param target - The location. * @param options - Timeout, fetch, staleness, clock and git overrides. * @returns The location's git state. * @example * ```ts * const row = await probeGitState({ projectId: 'p', deviceId: 'd', path: '/w/p' }); * if (row.probeErrorCode) console.error(row.probeError); * ``` */ export declare function probeGitState(target: GitProbeTarget, options?: GitStateProbeOptions): Promise; /** * Whether a row's remote state is stale at `now`: it has a remote, and its * last fetch is unknown or older than `staleAfterMs`. A repository with no * remote has nothing to be stale against. * * @param row - Row with `remoteName` and `remoteFetchedAt`. * @param now - Reference instant. * @param staleAfterMs - Staleness window. * @returns `true` when the remote fields should not be trusted as current. */ export declare function isRemoteStale(row: Pick, now: Date, staleAfterMs: number): boolean; /** * Probe many locations with bounded concurrency. Every target yields exactly * one row, in input order. * * @param targets - Locations to probe. * @param options - Concurrency, timeout, fetch and staleness options. * @returns One row per target. * @example * ```ts * const rows = await probeGitStates(targets, { concurrency: 8, timeoutMs: 5000 }); * ``` */ export declare function probeGitStates(targets: readonly GitProbeTarget[], options?: GitStateProbeOptions): Promise; /** Registry handle subset the git state functions need. */ export type GitStateStoreHandle = Pick; /** * This device's locations worth probing: `live` and `missing` (a missing one * yields an `E_PATH_MISSING` row, which is the fact another device wants). * * @param db - Global registry handle. * @param deviceId - This device's id. * @returns Probe targets with project names. */ export declare function listLocalProbeTargets(db: GitStateStoreHandle, deviceId: string): GitProbeTarget[]; /** * Upsert probe rows into `nexus_project_git_state`, one transaction. With * `pruneDeviceId`, that device's rows not in `rows` are deleted in the same * transaction. * * @param db - Global registry handle. * @param rows - Probe results. * @param options - `pruneDeviceId`: the device whose full row set `rows` is. */ export declare function recordGitStates(db: GitStateStoreHandle, rows: readonly NexusProjectGitState[], options?: { pruneDeviceId?: string; }): void; /** * Read recorded git state rows, recomputing `remoteStale` at `now`. * * @param db - Global registry handle. * @param options - `excludeDeviceId` to skip one device (this one), clock and window. * @returns Stored rows, by project then device then path. */ export declare function listGitStates(db: GitStateStoreHandle, options?: { excludeDeviceId?: string; now?: Date; staleAfterMs?: number; }): NexusProjectGitState[]; /** * Probe and record every location on this device, and return the fresh rows * together with the last recorded rows of every other device. * * @param db - Global registry handle. * @param params - Fetch, concurrency, timeout and staleness. * @param overrides - Device id, clock and git executable (tests). * @returns The status result. * @example * ```ts * const res = await runProjectsGitStatus(await getNexusRegistryDb(getCleoHome()), {}); * ``` */ export declare function runProjectsGitStatus(db: GitStateStoreHandle, params?: NexusProjectsStatusParams, overrides?: { deviceId?: string; now?: () => Date; gitBin?: string; }): Promise; export {}; //# sourceMappingURL=git-state.d.ts.map