# Maintenance

> Object fsck validation and repository hooks.

Repository maintenance helpers live in [`maintenance`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/index.html):

- [`Repository::write_commit_graph`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.Repository.html) — atomic monolithic `objects/info/commit-graph` with optional changed-path Bloom filters ([`CommitGraphOptions`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/struct.CommitGraphOptions.html), [`CommitGraphWriteReport`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/struct.CommitGraphWriteReport.html)). 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`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/struct.CommitGraphWriteReport.html#structfield.skipped) holds the [`CommitGraphSkipReason`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/enum.CommitGraphSkipReason.html). Writing needs the files backend; a repository with a custom primary store gets `Error::UnsupportedObjectStore`.
- [`build_commit_graph`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/fn.build_commit_graph.html) (`Repository::build_commit_graph`) — the same graph built in memory through any object store, custom primaries included, returned as a [`CommitGraphBuild`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/enum.CommitGraphBuild.html). The [`BuiltCommitGraph`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/struct.BuiltCommitGraph.html) bytes match what `write_commit_graph` installs; a custom store keeps them itself and attaches them with `OdbBuilder::commit_graph` (see [Object stores](https://grit-scm.com/docs/library/maintenance/object-stores.md/index.md#commit-graphs-for-a-custom-store)).
- [`Repository::write_midx`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.Repository.html) — write `multi-pack-index` ([`MidxOptions`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/struct.MidxOptions.html), [`MidxWriteReport`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/struct.MidxWriteReport.html)). 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`](https://docs.rs/grit-lib/latest/grit_lib/error/enum.MidxError.html) 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`](https://docs.rs/grit-lib/latest/grit_lib/refs/handle/struct.References.html) — pack loose refs into `packed-refs` or compact reftable stacks ([`PackRefsOptions`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/struct.PackRefsOptions.html)).
- [`repack_all`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/fn.repack_all.html) and [`gc`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/fn.gc.html) orchestrate repack and prune; [`repository_fsck::fsck`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/fn.fsck.html) validates the result.
- [`gc_auto`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/fn.gc_auto.html) is `git gc --auto`: [`auto_gc_needed`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/fn.auto_gc_needed.html) applies `gc.auto` (loose objects estimated from `objects/17`, two directory reads in total) and `gc.autoPackLimit` and returns an [`AutoGcReason`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/enum.AutoGcReason.html). 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`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/enum.AutoGcOutcome.html) (not needed, already running, declined by the `pre-auto-gc` hook, or ran with [`GcStats`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/struct.GcStats.html)). Call it after writing objects; the library never forks, so an application that honors `gc.autoDetach` ([`ConfigSet::gc_auto_detach`](https://docs.rs/grit-lib/latest/grit_lib/config/struct.ConfigSet.html#method.gc_auto_detach)) runs it on its own thread or process.
- [`gc`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/fn.gc.html) and [`gc_auto`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/fn.gc_auto.html) hold Git's `gc.pid` lock ([`GcLock`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/struct.GcLock.html)) while they run, so maintenance never overlaps another grit or Git gc. A held lock is [`Error::MaintenanceRunning`](https://docs.rs/grit-lib/latest/grit_lib/error/enum.Error.html) from `gc` and [`AutoGcOutcome::AlreadyRunning`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/enum.AutoGcOutcome.html) from `gc_auto`.

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

- [`maintenance::repository_fsck`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/index.html) — repository-wide integrity checks (loose/pack objects, connectivity from [`reachability_roots`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/fn.reachability_roots.html), dangling objects, refs, MIDX, commit-graph). Progress via [`ProgressSink`](https://docs.rs/grit-lib/latest/grit_lib/progress/trait.ProgressSink.html); severity follows Git defaults and `fsck.<msg-id>` config ([`issue_severity`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/fn.issue_severity.html)). Each [`FsckIssue`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/enum.FsckIssue.html) reports the object it concerns ([`FsckIssue::oid`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/enum.FsckIssue.html#method.oid)), the file or ref it was found in ([`FsckIssue::location`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/enum.FsckIssue.html#method.location)), and a one-line description ([`FsckIssue::detail`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/enum.FsckIssue.html#method.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`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/fsck/index.html) — validate loose object bytes before hashing ([`fsck_object`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/fsck/fn.fsck_object.html), [`FsckError`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/fsck/struct.FsckError.html) with Git-compatible message ids). Connectivity walks for receive-pack and bundles live in [`maintenance::repository_fsck`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/index.html).
- [`maintenance::repack`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/fn.repack.html) — all-into-one repack using [`reachability_roots`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/fn.reachability_roots.html), optional `.rev`/`.bitmap` sidecars, pack pruning, and unreachable loose-object pruning via [`RepackOptions`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/struct.RepackOptions.html) / [`RepackReport`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/struct.RepackReport.html). [`Repository::repack`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.Repository.html) 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`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/struct.RepackOptions.html) 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`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/index.html) — resolve and run hook scripts through the repository [`CommandRunner`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/trait.CommandRunner.html) ([`run_hook`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/fn.run_hook.html), [`HookResult`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/enum.HookResult.html)). 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`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/enum.HookError.html) 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`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/enum.HookStdio.html)) both streams are piped and returned as a [`HookOutput`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/struct.HookOutput.html) in the result, and in [`HookError::Failed`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/enum.HookError.html) when [`HookResult::check`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/enum.HookResult.html#method.check) or [`commit_worktree`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/commit/fn.commit_worktree.html) 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`](https://docs.rs/grit-lib/latest/grit_lib/config/environment/struct.RepositoryOptions.html#method.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`](https://docs.rs/grit-lib/latest/grit_lib/diagnostics/enum.Warning.html). The same holds for the hooks porcelain runs after an operation completes: [`switch_repository`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/switch/fn.switch_repository.html) runs `post-checkout`, [`integrate_branch_with`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/fn.integrate_branch_with.html) runs `post-merge`, an amending [`create_commit`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/commit/fn.create_commit.html) runs `post-rewrite amend`, and [`apply_rewrite`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.apply_rewrite.html) runs `post-checkout` and `post-rewrite rebase` (pairs from [`RewritePlan::rewritten_pairs`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.RewritePlan.html#method.rewritten_pairs)). Each takes a [`PostOperationHooks`](https://docs.rs/grit-lib/latest/grit_lib/enum.PostOperationHooks.html) option (`Skip` to opt out) and reports what ran as [`HookName`](https://docs.rs/grit-lib/latest/grit_lib/enum.HookName.html) values; [`run_post_checkout`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/fn.run_post_checkout.html), [`run_post_merge`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/fn.run_post_merge.html) and [`run_post_rewrite`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/fn.run_post_rewrite.html) run one hook with Git's arguments. The hooks that run before an operation records anything can refuse it, with [`Error::Hook`](https://docs.rs/grit-lib/latest/grit_lib/error/enum.Error.html) wrapping [`HookError::Failed`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/enum.HookError.html): [`create_commit`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/commit/fn.create_commit.html) runs `pre-commit`, `prepare-commit-msg <COMMIT_EDITMSG> <source>` and `commit-msg`; [`integrate_branch_with`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/fn.integrate_branch_with.html) 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`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.start_rebase.html) runs `pre-rebase` as [`StartRebaseOptions::pre_rebase`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.StartRebaseOptions.html#structfield.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`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.plan_rebase_with_hook.html) does the same for callers that plan and apply a rebase themselves, [`run_pre_rebase`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/hooks/fn.run_pre_rebase.html) runs the bare hook). [`HookPolicy::SkipVerify`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/commit/enum.HookPolicy.html) ([`CommitRequest::hooks`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/commit/struct.CommitRequest.html#structfield.hooks), [`MergeOptions::verify`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/struct.MergeOptions.html#structfield.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`](https://docs.rs/grit-lib/latest/grit_lib/config/environment/struct.Environment.html) snapshot. [`Repository::hooks`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.Repository.html) lists scripts via [`RepositoryHooks::list`](https://docs.rs/grit-lib/latest/grit_lib/repo_handles/struct.RepositoryHooks.html) ([`HookInfo`](https://docs.rs/grit-lib/latest/grit_lib/repo_handles/struct.HookInfo.html), [`HookSource`](https://docs.rs/grit-lib/latest/grit_lib/repo_handles/enum.HookSource.html)). Tests often install [`RecordingRunner`](https://docs.rs/grit-lib/latest/grit_lib/command_runner/struct.RecordingRunner.html) via [`RepositoryOptions`](https://docs.rs/grit-lib/latest/grit_lib/config/environment/struct.RepositoryOptions.html).
- [`patch_ids`](https://docs.rs/grit-lib/latest/grit_lib/diff/patch_ids/index.html) — [`Repository::patch_id`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.Repository.html) and [`compute_patch_id`](https://docs.rs/grit-lib/latest/grit_lib/diff/patch_ids/fn.compute_patch_id.html) 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`](https://docs.rs/grit-lib/latest/grit_lib/diff/patch_ids/fn.compute_patch_id_for_paths.html) applies the same rules to selected paths, while [`compute_patch_ids_from_text`](https://docs.rs/grit-lib/latest/grit_lib/diff/patch_ids/fn.compute_patch_ids_from_text.html) hashes the hunks supplied by its caller, including their chosen context.
  [`Repository::patch_ids_for_revs`](https://docs.rs/grit-lib/latest/grit_lib/repo/struct.Repository.html) 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`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/queries/struct.RevWalkListOptions.html) and read [`RevListResult::cherry_matches`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/rev_list/struct.RevListResult.html): each [`CherryMatch`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/rev_list/struct.CherryMatch.html) 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`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/struct.FsckReport.html#method.summary)
with the configuration, fsck options, and an `include_info` boolean to classify a
report for display. The returned
[`FsckSummary`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/struct.FsckSummary.html) keeps
all findings in discovery order as borrowed
[`FsckDiagnostic`](https://docs.rs/grit-lib/latest/grit_lib/maintenance/repository_fsck/struct.FsckDiagnostic.html)
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.

## Example

```rust
//! Validate objects with fsck rules and run a repository hook.
//!
//! Source for the library guide "Maintenance" page (included in the docs site).

use grit_lib::command_runner::hooks::{run_hook, HookResult};
use grit_lib::config::environment::RepositoryOptions;
use grit_lib::maintenance::fsck::{fsck_object, FsckObjectOptions};
use grit_lib::objects::{HashAlgo, ObjectKind};
use grit_lib::repo::Repository;
use grit_lib::state::resolve_head;
use std::fs;
use std::path::{Path, PathBuf};

fn open_repo(root: &Path) -> Result<Repository, grit_lib::error::Error> {
    let git_dir = if root.join(".git").is_dir() {
        root.join(".git")
    } else {
        root.to_path_buf()
    };
    let work_tree = if root.join(".git").is_dir() {
        Some(root)
    } else {
        None
    };
    let mut opts = RepositoryOptions::from_process();
    if let Some(wt) = work_tree {
        opts = opts.with_work_tree(wt);
    }
    Repository::open(&git_dir, &opts)
}

fn main() -> Result<(), grit_lib::error::Error> {
    let root = std::env::args()
        .nth(1)
        .map(PathBuf::from)
        .or_else(|| std::env::current_dir().ok())
        .ok_or_else(|| grit_lib::error::Error::Message("missing repository path".into()))?;
    let repo = open_repo(&root)?;

    let head = resolve_head(repo.git_dir())?;
    let commit = head
        .oid()
        .cloned()
        .ok_or_else(|| grit_lib::error::Error::Message("HEAD has no commit".into()))?;

    let obj = repo.objects().read(&commit)?;
    fsck_object(
        ObjectKind::Commit,
        &obj.data,
        FsckObjectOptions::new(HashAlgo::Sha1),
    )
    .map_err(|e| grit_lib::error::Error::Message(e.report_line()))?;
    println!("fsck_object_ok=1");

    let hook_path = repo.git_dir().join("hooks").join("pre-commit");
    if let Some(parent) = hook_path.parent() {
        fs::create_dir_all(parent).map_err(grit_lib::error::Error::Io)?;
    }
    fs::write(&hook_path, b"#!/bin/sh\nexit 0\n").map_err(grit_lib::error::Error::Io)?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        fs::set_permissions(&hook_path, fs::Permissions::from_mode(0o755))
            .map_err(grit_lib::error::Error::Io)?;
    }

    let hook_result = run_hook(&repo, "pre-commit", &[], None)?;
    let ran = matches!(hook_result, HookResult::Success { .. });
    println!("pre_commit_ran={ran}");

    Ok(())
}
```

This example fsck-validates `HEAD`’s commit object, installs a `pre-commit` hook that exits 0, and runs it through `run_hook` (propagating a spawn failure with `?`). The test expects `git fsck --strict` to stay clean afterward.
