Repository maintenance helpers live in maintenance:
Repository::write_commit_graph— atomic monolithicobjects/info/commit-graphwith optional changed-path Bloom filters (CommitGraphOptions,CommitGraphWriteReport). Shallow clones,info/graftsand enabled replace refs change commit parents, so no graph is written there and any existing one is deleted (Git ignores graphs in that state butgit fsckstill rejects them);CommitGraphWriteReport::skippedholds theCommitGraphSkipReason. Writing needs the files backend; a repository with a custom primary store getsError::UnsupportedObjectStore.build_commit_graph(Repository::build_commit_graph) — the same graph built in memory through any object store, custom primaries included, returned as aCommitGraphBuild. TheBuiltCommitGraphbytes match whatwrite_commit_graphinstalls; a custom store keeps them itself and attaches them withOdbBuilder::commit_graph(see Object stores).Repository::write_midx— writemulti-pack-index(MidxOptions,MidxWriteReport). The file is format version 1 by default, readable by every Git with multi-pack-index support;MidxOptions::version, thenmidx.versionin config, select version 2. Other values fail withMidxError::UnknownWriteVersionbefore anything is written, including in dry-run mode. Incremental repack follows the samemidx.versionsetting when it writes a multi-pack-index bitmap.References::pack— pack loose refs intopacked-refsor compact reftable stacks (PackRefsOptions).repack_allandgcorchestrate repack and prune;repository_fsck::fsckvalidates the result.gc_autoisgit gc --auto:auto_gc_neededappliesgc.auto(loose objects estimated fromobjects/17, two directory reads in total) andgc.autoPackLimitand returns anAutoGcReason. Too many loose objects packs the reachable ones into one new, self-contained pack without touching existing packs (unreachable ones stay loose forgc.pruneExpire), delta-compressed withpack.window/pack.depthlike a full repack; too many packs runs a full consolidating gc. The result is anAutoGcOutcome(not needed, already running, declined by thepre-auto-gchook, or ran withGcStats). Call it after writing objects; the library never forks, so an application that honorsgc.autoDetach(ConfigSet::gc_auto_detach) runs it on its own thread or process.gcandgc_autohold Git'sgc.pidlock (GcLock) while they run, so maintenance never overlaps another grit or Git gc. A held lock isError::MaintenanceRunningfromgcandAutoGcOutcome::AlreadyRunningfromgc_auto.
Maintenance APIs split into repository fsck, object validation, and hook execution:
maintenance::repository_fsck— repository-wide integrity checks (loose/pack objects, connectivity fromreachability_roots, dangling objects, refs, MIDX, commit-graph). Progress viaProgressSink; severity follows Git defaults andfsck.<msg-id>config (issue_severity). EachFsckIssuereports 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 ascorruptObjectalong 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,FsckErrorwith Git-compatible message ids). Connectivity walks for receive-pack and bundles live inmaintenance::repository_fsck.maintenance::repack— all-into-one repack usingreachability_roots, optional.rev/.bitmapsidecars, pack pruning, and unreachable loose-object pruning viaRepackOptions/RepackReport.Repository::repackis 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 atpack.depth); only loose objects and deltas whose base is dropped go through a streamingpack.windowdelta search, and the pack streams to disk. SetRepackOptions::force_recompressto recompute every delta, likegit repack -f. Incremental repack (RepackOptions::incremental) keeps existing packs; when combined withwrite_bitmapand multiple packs, grit writes a multi-pack-index reachability bitmap instead of a single-pack sidecar.hooks— resolve and run hook scripts through the repositoryCommandRunner(run_hook,HookResult). A hook that runs and exits non-zero isOk(HookResult::Failed { status, output })with its real exit status; a hook that cannot be started at all isHookError::Spawncarrying the program, theio::Error, and whatever earlier hooks for the event (configuredhook.<name>.*entries run first) already wrote. The library never prints hook output. By default (HookStdio::Capture) both streams are piped and returned as aHookOutputin the result, and inHookError::FailedwhenHookResult::checkorcommit_worktreeturns a failingpre-commitorcommit-msginto an error, so a TUI or GUI can show it in its own pane. TheDisplayof the error stays one line. A terminal front end that wants Git's live output opens the repository withRepositoryOptions::with_hook_stdio(HookStdio::Inherit): the hook then writes straight to the process's stderr (its stdout too, except forpre-push) and the returned output is empty. Output an injectedCommandRunnerreturns 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 throughsh(/bin/shon Unix; on Windows the#!interpreter, resolved in the Git for Windows tree and then on the hook'sPATH). Apost-commithook that cannot start never fails the commit; it is reported asWarning::HookNotRun. The same holds for the hooks porcelain runs after an operation completes:switch_repositoryrunspost-checkout,integrate_branch_withrunspost-merge, an amendingcreate_commitrunspost-rewrite amend, andapply_rewriterunspost-checkoutandpost-rewrite rebase(pairs fromRewritePlan::rewritten_pairs). Each takes aPostOperationHooksoption (Skipto opt out) and reports what ran asHookNamevalues;run_post_checkout,run_post_mergeandrun_post_rewriterun one hook with Git's arguments. The hooks that run before an operation records anything can refuse it, withError::HookwrappingHookError::Failed:create_commitrunspre-commit,prepare-commit-msg <COMMIT_EDITMSG> <source>andcommit-msg;integrate_branch_withrunspre-merge-commit, thenprepare-commit-msg <MERGE_MSG> mergeandcommit-msgwithMERGE_HEADrecorded, and leaves the merge in progress when one refuses;start_rebaserunspre-rebaseasStartRebaseOptions::pre_rebasesays, 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_hookdoes the same for callers that plan and apply a rebase themselves,run_pre_rebaseruns the bare hook).HookPolicy::SkipVerify(CommitRequest::hooks,MergeOptions::verify) is--no-verify: it skipspre-commit,pre-merge-commitandcommit-msg, neverprepare-commit-msg. Hooks inherit every variable of the repositoryEnvironmentsnapshot.Repository::hookslists scripts viaRepositoryHooks::list(HookInfo,HookSource). Tests often installRecordingRunnerviaRepositoryOptions.patch_ids—Repository::patch_idandcompute_patch_idfor stable/unstable commit equivalence (merge commits returnNone). Text diffs use unified hunks with three context lines on each side, matchinggit show --format= --no-ext-diff -U3 <REV> | git patch-id --stablein stable mode. Nearby unchanged context is hashed; distant unchanged lines are excluded.compute_patch_id_for_pathsapplies the same rules to selected paths, whilecompute_patch_ids_from_texthashes the hunks supplied by its caller, including their chosen context.Repository::patch_ids_for_revstakes revision specs and expands ranges:A..Byields one(patch_id, commit)pair per commit inrev-listorder, andA...Byields both sides as one unlabelled sequence. To compare the two sides ofA...B(for example a branch before and after a force-push), setRevWalkListOptions::cherry_markand readRevListResult::cherry_matches: eachCherryMatchlists 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.