grit
CLILibrary
HEAD → docs/library/maintenance
Library guide / MaintenanceMarkdown

Maintenance

Object fsck validation and repository hooks.

Repository maintenance helpers live in maintenance:

  • Repository::write_commit_graph — atomic monolithic objects/info/commit-graph with optional changed-path Bloom filters (CommitGraphOptions, CommitGraphWriteReport). Shallow clones, info/grafts and enabled replace refs change commit parents, so no graph is written there and any existing one is deleted (Git ignores graphs in that state but git fsck still rejects them); CommitGraphWriteReport::skipped holds the CommitGraphSkipReason. Writing needs the files backend; a repository with a custom primary store gets Error::UnsupportedObjectStore.
  • build_commit_graph (Repository::build_commit_graph) — the same graph built in memory through any object store, custom primaries included, returned as a CommitGraphBuild. The BuiltCommitGraph bytes match what write_commit_graph installs; a custom store keeps them itself and attaches them with OdbBuilder::commit_graph (see Object stores).
  • Repository::write_midx — write multi-pack-index (MidxOptions, MidxWriteReport). The file is format version 1 by default, readable by every Git with multi-pack-index support; MidxOptions::version, then midx.version in config, select version 2. Other values fail with MidxError::UnknownWriteVersion before anything is written, including in dry-run mode. Incremental repack follows the same midx.version setting when it writes a multi-pack-index bitmap.
  • References::pack — pack loose refs into packed-refs or compact reftable stacks (PackRefsOptions).
  • repack_all and gc orchestrate repack and prune; repository_fsck::fsck validates the result.
  • gc_auto is git gc --auto: auto_gc_needed applies gc.auto (loose objects estimated from objects/17, two directory reads in total) and gc.autoPackLimit and returns an AutoGcReason. Too many loose objects packs the reachable ones into one new, self-contained pack without touching existing packs (unreachable ones stay loose for gc.pruneExpire), delta-compressed with pack.window / pack.depth like a full repack; too many packs runs a full consolidating gc. The result is an AutoGcOutcome (not needed, already running, declined by the pre-auto-gc hook, or ran with GcStats). Call it after writing objects; the library never forks, so an application that honors gc.autoDetach (ConfigSet::gc_auto_detach) runs it on its own thread or process.
  • gc and gc_auto hold Git's gc.pid lock (GcLock) while they run, so maintenance never overlaps another grit or Git gc. A held lock is Error::MaintenanceRunning from gc and AutoGcOutcome::AlreadyRunning from gc_auto.

Maintenance APIs split into repository fsck, object validation, and hook execution:

  • maintenance::repository_fsck — repository-wide integrity checks (loose/pack objects, connectivity from reachability_roots, dangling objects, refs, MIDX, commit-graph). Progress via ProgressSink; severity follows Git defaults and fsck.<msg-id> config (issue_severity). Each FsckIssue reports the object it concerns (FsckIssue::oid), the file or ref it was found in (FsckIssue::location), and a one-line description (FsckIssue::detail), so callers can render findings without matching every variant. Packs are memory-mapped and verified in one depth-first pass over their delta trees on worker threads: each object is inflated once, hashed against its index entry and checked, then its bytes are dropped. Peak memory therefore follows the delta chains being walked plus a few dozen bytes per object (commit and tag links, object kinds), not the uncompressed size of the pack. A delta whose base is corrupt, missing or fails its hash check is reported as corruptObject along with every delta built on it. The connectivity walk re-reads trees from the object store, as Git does, instead of caching every tree entry.
  • maintenance::fsck — validate loose object bytes before hashing (fsck_object, FsckError with Git-compatible message ids). Connectivity walks for receive-pack and bundles live in maintenance::repository_fsck.
  • maintenance::repack — all-into-one repack using reachability_roots, optional .rev/.bitmap sidecars, pack pruning, and unreachable loose-object pruning via RepackOptions / RepackReport. Repository::repack is a thin alias. Objects already in a pack keep their stored form (whole objects byte for byte, deltas with their base reference rewritten, chains capped at pack.depth); only loose objects and deltas whose base is dropped go through a streaming pack.window delta search, and the pack streams to disk. Set RepackOptions::force_recompress to recompute every delta, like git repack -f. Incremental repack (RepackOptions::incremental) keeps existing packs; when combined with write_bitmap and multiple packs, grit writes a multi-pack-index reachability bitmap instead of a single-pack sidecar.
  • hooks — resolve and run hook scripts through the repository CommandRunner (run_hook, HookResult). A hook that runs and exits non-zero is Ok(HookResult::Failed { status, output }) with its real exit status; a hook that cannot be started at all is HookError::Spawn carrying the program, the io::Error, and whatever earlier hooks for the event (configured hook.<name>.* entries run first) already wrote. The library never prints hook output. By default (HookStdio::Capture) both streams are piped and returned as a HookOutput in the result, and in HookError::Failed when HookResult::check or commit_worktree turns a failing pre-commit or commit-msg into an error, so a TUI or GUI can show it in its own pane. The Display of the error stays one line. A terminal front end that wants Git's live output opens the repository with RepositoryOptions::with_hook_stdio(HookStdio::Inherit): the hook then writes straight to the process's stderr (its stdout too, except for pre-push) and the returned output is empty. Output an injected CommandRunner returns is only ever handed back, never re-emitted. Server-side hooks run by receive-pack are always captured and relayed to the client on the sideband. Scripts the OS refuses to execute run through sh (/bin/sh on Unix; on Windows the #! interpreter, resolved in the Git for Windows tree and then on the hook's PATH). A post-commit hook that cannot start never fails the commit; it is reported as Warning::HookNotRun. The same holds for the hooks porcelain runs after an operation completes: switch_repository runs post-checkout, integrate_branch_with runs post-merge, an amending create_commit runs post-rewrite amend, and apply_rewrite runs post-checkout and post-rewrite rebase (pairs from RewritePlan::rewritten_pairs). Each takes a PostOperationHooks option (Skip to opt out) and reports what ran as HookName values; run_post_checkout, run_post_merge and run_post_rewrite run one hook with Git's arguments. The hooks that run before an operation records anything can refuse it, with Error::Hook wrapping HookError::Failed: create_commit runs pre-commit, prepare-commit-msg <COMMIT_EDITMSG> <source> and commit-msg; integrate_branch_with runs pre-merge-commit, then prepare-commit-msg <MERGE_MSG> merge and commit-msg with MERGE_HEAD recorded, and leaves the merge in progress when one refuses; start_rebase runs pre-rebase as StartRebaseOptions::pre_rebase says, once the branch is known not to be up to date and before any commit is replayed, so a refusal comes before a replay conflict and writes nothing (plan_rebase_with_hook does the same for callers that plan and apply a rebase themselves, run_pre_rebase runs the bare hook). HookPolicy::SkipVerify (CommitRequest::hooks, MergeOptions::verify) is --no-verify: it skips pre-commit, pre-merge-commit and commit-msg, never prepare-commit-msg. Hooks inherit every variable of the repository Environment snapshot. Repository::hooks lists scripts via RepositoryHooks::list (HookInfo, HookSource). Tests often install RecordingRunner via RepositoryOptions.
  • patch_ids — Repository::patch_id and compute_patch_id for stable/unstable commit equivalence (merge commits return None). Text diffs use unified hunks with three context lines on each side, matching git show --format= --no-ext-diff -U3 <REV> | git patch-id --stable in stable mode. Nearby unchanged context is hashed; distant unchanged lines are excluded. compute_patch_id_for_paths applies the same rules to selected paths, while compute_patch_ids_from_text hashes the hunks supplied by its caller, including their chosen context. Repository::patch_ids_for_revs takes revision specs and expands ranges: A..B yields one (patch_id, commit) pair per commit in rev-list order, and A...B yields both sides as one unlabelled sequence. To compare the two sides of A...B (for example a branch before and after a force-push), set RevWalkListOptions::cherry_mark and read RevListResult::cherry_matches: each CherryMatch lists the left and right commits sharing one patch-id, and for a one-to-one pair says whether the commit message changed.

Partial-clone maintenance works offline. Repack keeps existing .promisor packs and their sidecars intact, and writes local objects into a separate unmarked pack. GC, reflog expiry, pruning, incremental repack, and dry runs honor promises without hydrating missing history. Missing non-promised objects still fail validation. Bitmaps are omitted when the local pack cannot contain the complete closure; RepackStats::pack_path and pack_bytes refer to the newly written local pack even when other packs remain.

The grit CLI maps fsck and hook failures to its own exit codes and messages; the library returns typed results only.

Fsck diagnostic summaries

Call FsckReport::summary with the configuration, fsck options, and an include_info boolean to classify a report for display. The returned FsckSummary keeps all findings in discovery order as borrowed FsckDiagnostic values, each with its resolved severity and listing visibility. No objects are read or changed by classification.

summary.ok is false if any finding has error severity. summary.has_warnings records warnings independently of errors; visible informational findings do not count as warnings. include_info changes visibility only. By default, most informational findings are hidden, while missingTaggerEntry remains visible. The CLI uses this policy for human listings and summary messages; JSON and Markdown retain every finding, including hidden informational findings.

Benchmarking

The October 2026 diagnostic-summary extraction was checked against main 7cc7979535ce and system Git 2.55.0 on a Linux factory VM with four AMD EPYC 7763 vCPUs, using Rust 1.99.0 and the dist profile. Builds and tests were idle during measurement. Times below are median milliseconds.

Repository and operation Git Grit before Grit after
20,000 files, 50,000 commits: fsck --no-dangling 267.38 221.36 221.76
1,001 warning trees and an informational tag finding: fsck --no-dangling 23.75 15.72 15.37

The large-repository row uses 30 interleaved rounds with randomized tool order, after three warmups per tool. Maximum process RSS was 60,960 KiB before and 60,724 KiB after, versus 28,444 KiB for Git. The diagnostic-heavy row uses hyperfine with five warmups and at least 30 runs per tool; it exercises severity and visibility classification rather than an empty report.

The standard grit-bench fsck --warmup 3 --min-runs 10 scenario also ran on both builds: 207.99 to 214.31 ms for grit, with Git at 265.00 and 261.92 ms respectively. The subsequent interleaved comparison put grit's medians within 0.2%, with the paired sample variability including no change. Summary classification performs no additional object reads and resolves severity once per finding.