Skip to content

connected: add incremental connectivity check - #2211

Open
spkrka wants to merge 4 commits into
gitgitgadget:masterfrom
spkrka:tree-diff-connectivity-v1-clean
Open

connected: add incremental connectivity check#2211
spkrka wants to merge 4 commits into
gitgitgadget:masterfrom
spkrka:tree-diff-connectivity-v1-clean

Conversation

@spkrka

@spkrka spkrka commented Aug 28, 2026

Copy link
Copy Markdown

This series implements the tree verification optimization described in
my recent RFC [1], gated behind transfer.connectivityCheck=incremental.

The RFC received no replies; the patches here are hopefully a more
concrete way to evaluate the approach.

The current connectivity check uses a rev-list subprocess to perform
the object traversal. On repositories with large active trees this
can become expensive even for small fetches, because the tree/blob
closure at the connectivity boundary may be much larger than the
incoming change.

This series addresses the object-walk cost (step 2 from the RFC) by
introducing an incremental tree-diff approach. Incoming trees are
verified against their parents, recursively descending only into
entries whose OIDs have changed. Parent trees still need to be
scanned as comparison bases. The boundary search (step 1) continues
to use rev-list; optimizing that is a natural follow-up.

The approach uses the Merkle tree property: when a tree entry has the
same OID in both a new commit's tree and a trusted parent's tree, the
entire subtree is already verified. Only differing entries require
recursive verification. The verified set persists across commits, so
subtrees that have already been established as trusted can also be
reused across changes, reverts, moves, and merges.

Benchmarks on a large monorepo (~3M commits, 221K trees and 503K
blobs reachable from the tip). Scenario: 1 new commit, 1 file
changed, measured with hyperfine:

With ~10K local refs:

                     mean +/- stddev
rev-list:          1849 ms +/- 60 ms
incremental:        143 ms +/- 19 ms   (12.9x faster)
  boundary search:  114 ms
  tree verification:  5 ms   (1 new tree walked, 1 blob checked)

With 1 local ref:

                     mean +/- stddev
rev-list:          1791 ms +/- 72 ms
incremental:         17 ms +/-  1 ms   (107x faster)
  boundary search:    8 ms
  tree verification:  5 ms   (1 new tree walked, 1 blob checked)

In this benchmark the rev-list approach visits all 724K tree and blob
objects while classifying the connectivity boundary. The incremental
verifier walks one new tree and checks one blob; the corresponding
parent tree is scanned as the comparison base. Tree verification took
5 ms in both measurements.

With many local refs, the boundary search dominates the incremental
timings. Eliminating that subprocess and performing boundary discovery
in-process is the target for a follow-up.

On linux.git (~1.5M commits, 6.2K trees and 95K blobs reachable
from the tip, ~940 refs):

                     mean +/- stddev
rev-list:           226 ms +/- 21 ms
incremental:         97 ms +/- 11 ms   (2.3x faster)
  boundary search:   80 ms
  tree verification:  1 ms   (1 new tree walked, 1 blob checked)

On git.git (~82K commits, 224 trees and 4851 blobs reachable
from the tip, ~7K refs):

                     mean +/- stddev
rev-list:           371 ms +/- 17 ms
incremental:        368 ms +/- 21 ms   (no measurable difference)
  boundary search:  393 ms
  tree verification:  1 ms   (1 new tree walked, 1 blob checked)

The benefit scales with tree closure size. linux.git's 101K
tree+blob objects produce a clear 2.3x win; git.git's 5K objects
are too small for the difference to be measurable.

The implementation falls back to the rev-list path for deepening
fetches and repositories with active replacement objects.

One question around replacements: the existing rev-list connectivity
path follows replacement refs, while git prune explicitly disables
replacement refs before performing its reachability traversal. The
connectivity check ensures that refs do not point into incomplete
object graphs, while pruning ultimately operates on the underlying
object graph. It may therefore be worth discussing which replacement
semantics are intended here. I have left the existing behavior
unchanged in this series.

The series is structured as four commits:

  1. connected: extract get_self_contained_pack() helper
    Pure refactor: lifts the index-pack self-contained pack
    optimization into a shared helper, used by both paths.

  2. connected: add incremental connectivity check
    Core implementation: config plumbing, three-phase algorithm
    (collect tips, find boundary, verify new commits via
    tree-diff), and tests.

  3. connected: handle shallow fetches in incremental check
    Adds shallow boundary support: commits listed in the
    temporary shallow file are treated as roots with no parents,
    receiving full closure verification.

  4. connected: handle partial clones in incremental check
    Adds promisor-remote support: missing objects that are
    promisor objects are accepted, matching the existing
    --exclude-promisor-objects semantics.

[1] https://lore.kernel.org/git/CAL71e4Nf=-zCrfN7ghEVGq11irajJhtdxYZgKe0Ycux0qs1ZvQ@mail.gmail.com/

Move the inline self-contained pack detection into a helper
function.  This makes check_connected() easier to follow and
makes the detection logic available as a standalone helper.

No functional change.

Signed-off-by: Kristofer Karlsson <krka@spotify.com>
@spkrka
spkrka marked this pull request as ready for review August 28, 2026 10:15
@spkrka
spkrka force-pushed the tree-diff-connectivity-v1-clean branch from 1f543de to fd76802 Compare August 28, 2026 11:41
spkrka added 3 commits August 29, 2026 16:56
The connectivity check walks objects reachable from incoming tips
using rev-list.  mark_edges_uninteresting() prunes at commit
boundaries, but the traversal still visits the full tree closure
of each boundary commit to classify every entry.  For repositories
with large trees, this is expensive.

Add an alternative that verifies incoming commits by diffing their
trees against parent trees, recursively descending only into entries
whose OIDs have changed.  Use the Merkle tree property -- an unchanged
tree entry proves the entire subtree is intact -- to skip subtrees
that already exist in the repository.  Cross-directory moves may be
reverified rather than recognized by the parent-tree comparison, but
correctness is preserved.

The algorithm works in three phases:

 1. Collect and peel tips: consume the tip iterator, peel tags, and
    verify non-commit objects immediately.  Tips found in a
    self-contained pack (verified by index-pack) are skipped.

 2. Find boundary: feed commit tips to rev-list --stdin --not --all
    to identify new commits not yet reachable from local refs.

 3. Verify trees: walk new commits in topological order.  For each
    commit, diff its tree against parent trees.  Unchanged entries
    are skipped (Merkle property).  The verified set persists across
    commits so subtrees seen in earlier commits (change-then-revert,
    subtree moves, merges) are not re-walked.

Gate the new algorithm behind transfer.connectivityCheck=incremental.
Fall back to rev-list for shallow fetches, partial clones, replacement
objects, and deepening fetches.

Signed-off-by: Kristofer Karlsson <krka@spotify.com>
Teach the incremental connectivity check to handle shallow fetches.
Shallow commits are treated as traversal roots with no parents,
matching the boundary semantics of rev-list.

Add parse_shallow_file_gently() to parse the temporary shallow file
without dying on errors, and thread the resulting oidset through
verify_new_commits and verify_commit_tree.  Pass --shallow-file to
the boundary-finding rev-list so it respects the shallow grafts.

Remove the shallow_file guard from incremental_check_applicable()
so incremental mode is now used for shallow fetches when configured.

Signed-off-by: Kristofer Karlsson <krka@spotify.com>
Teach the incremental connectivity check to handle partial clones
where some objects are promised by a promisor remote but not present
locally.

When a tree or blob cannot be read, check whether it is a promisor
object before reporting an error.  Promisor objects are trusted and
added to the verified set without fetching them.  During tag peeling,
a missing object that is a promisor object causes the tip to be
silently skipped rather than treated as an error.

Pass --exclude-promisor-objects to the boundary-finding rev-list so
promisor commits do not pollute the set of commits to verify.

Remove the repo_has_promisor_remote() guard from
incremental_check_applicable() so incremental mode is now used for
partial clones when configured.

Signed-off-by: Kristofer Karlsson <krka@spotify.com>
@spkrka
spkrka force-pushed the tree-diff-connectivity-v1-clean branch from fd76802 to 68dbb9c Compare August 29, 2026 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant