|
| 1 | +Connectivity checking |
| 2 | +===================== |
| 3 | + |
| 4 | +After receiving new objects via fetch, push (receive-pack), clone, |
| 5 | +or bundle, Git verifies that the new reference tips do not leave |
| 6 | +the repository in a state where reachable objects are missing. |
| 7 | +This verification is called the connectivity check. |
| 8 | + |
| 9 | +Connectivity invariant |
| 10 | +---------------------- |
| 11 | + |
| 12 | +A repository is connected when every object reachable from its |
| 13 | +references is available locally (with exceptions noted below). |
| 14 | +The reverse does not hold: objects that exist in the object |
| 15 | +database but are not reachable from any reference may be |
| 16 | +incomplete or lack a connected closure. |
| 17 | + |
| 18 | +The connectivity check maintains this invariant when references |
| 19 | +are updated. It trusts that all objects reachable from existing |
| 20 | +reference tips are already fully connected and verifies that the |
| 21 | +new reference tips do not introduce references to unavailable |
| 22 | +objects. Verification is permitted to stop when it reaches |
| 23 | +objects already reachable from existing references, since their |
| 24 | +closure is already connected. These trusted references include |
| 25 | +local references and references from alternate object stores. |
| 26 | + |
| 27 | +Without this check, a truncated or corrupted transfer could leave |
| 28 | +a repository in a state where later history walks encounter |
| 29 | +missing objects. |
| 30 | + |
| 31 | +Exceptions |
| 32 | +~~~~~~~~~~ |
| 33 | + |
| 34 | +Gitlink entries (submodule references) are excluded from |
| 35 | +connectivity checking. Their target objects belong to a separate |
| 36 | +repository. |
| 37 | + |
| 38 | +In partial clones, objects promised by a promisor remote are |
| 39 | +accepted as connected without requiring local existence. The |
| 40 | +check excludes promisor objects from traversal so that it does |
| 41 | +not trigger on-demand fetches for them. |
| 42 | + |
| 43 | +Full connectivity check |
| 44 | +----------------------- |
| 45 | + |
| 46 | +The full connectivity check runs a `rev-list` subprocess, feeding |
| 47 | +the new reference tips via stdin. A normal invocation is roughly: |
| 48 | + |
| 49 | + git rev-list --objects --stdin --not --all --quiet |
| 50 | + --alternate-refs [--exclude-promisor-objects] |
| 51 | + |
| 52 | +In a partial clone, objects received from a promisor remote do |
| 53 | +not need to be checked: anything they reference is promised by |
| 54 | +that remote and can be fetched on demand. So if every new tip is |
| 55 | +found in a promisor packfile, `rev-list` is not run at all. |
| 56 | +Otherwise it runs with `--exclude-promisor-objects`, so that the |
| 57 | +traversal stops at promisor objects instead of fetching them. |
| 58 | + |
| 59 | +Consider the following graph after a fetch, where all reference |
| 60 | +tips point directly to commits. For simplicity, only local |
| 61 | +references appear on the already-connected side; alternate refs |
| 62 | +play the same role. N3 is a merge commit: |
| 63 | + |
| 64 | + /-------------L2 |
| 65 | + / |
| 66 | + C1---B1---C2---B2-----L1 |
| 67 | + \ \ |
| 68 | + N1 N3---T2 |
| 69 | + \ / |
| 70 | + N2-----------T1 |
| 71 | + |
| 72 | + L1, L2: local refs |
| 73 | + T1, T2: incoming tips (new refs) |
| 74 | + N1, N2, N3: incoming commits (N3 is a merge) |
| 75 | + B1, B2: boundary commits (already connected) |
| 76 | + C1, C2: already connected (but not boundary) |
| 77 | + |
| 78 | +The incoming set is the commits reachable from the incoming |
| 79 | +tips but not from the already-connected side. Boundary commits |
| 80 | +are the already-connected commits at the edge of that set. Here |
| 81 | +B1 is an ancestor of B2, which happens when incoming branches |
| 82 | +fork at different depths in the existing history. |
| 83 | + |
| 84 | +The `--not --all` arguments establish the trust boundary: the |
| 85 | +incoming set is the commits reachable from the new tips but not |
| 86 | +from any existing reference. |
| 87 | + |
| 88 | +The traversal then walks the trees of the boundary commits and |
| 89 | +marks those objects uninteresting. Trees of incoming commits |
| 90 | +are then walked, stopping at objects already marked as |
| 91 | +uninteresting. The cost of marking boundary trees uninteresting |
| 92 | +dominates when the boundary commits have large trees, even when |
| 93 | +the incoming changes are small. |
| 94 | + |
| 95 | +Deepening fetches |
| 96 | +~~~~~~~~~~~~~~~~~ |
| 97 | + |
| 98 | +For deepening fetches (where the shallow boundary moves), the |
| 99 | +full check omits `--not --all`. There is no existing-reference |
| 100 | +boundary at which the walk can stop. Instead, traversal follows |
| 101 | +the effective shallow boundary supplied for the deepened |
| 102 | +repository. The new content may be below the old shallow |
| 103 | +boundary even when the tips themselves have not changed. |
| 104 | + |
| 105 | +Non-commit tips |
| 106 | +~~~~~~~~~~~~~~~ |
| 107 | + |
| 108 | +When a new reference points to a non-commit object, such as a |
| 109 | +tag, tree, or blob, that object is not part of the commit walk. |
| 110 | +These non-commit tips are verified during the tree and blob |
| 111 | +traversal that follows the commit walk within the same rev-list |
| 112 | +invocation. |
| 113 | + |
| 114 | +Incremental connectivity check |
| 115 | +------------------------------ |
| 116 | +
|
| 117 | +The incremental mode, selected by |
| 118 | +`transfer.connectivityCheck=incremental`, avoids traversing the |
| 119 | +full tree walk of the boundary commits. Instead, it verifies |
| 120 | +each incoming commit's tree against the already-trusted trees of |
| 121 | +its parent commits. |
| 122 | +
|
| 123 | +The same `--not --all` boundary as the full check identifies the |
| 124 | +incoming commits. The incremental mode reuses this boundary but |
| 125 | +replaces what happens afterwards: rather than walking boundary |
| 126 | +trees to mark objects uninteresting, it compares each incoming |
| 127 | +commit's tree against its parents' trees, processing commits in |
| 128 | +topological order (ancestors first). This all happens inside |
| 129 | +the same `rev-list` invocation, and the promisor fast path above |
| 130 | +still runs first. |
| 131 | +
|
| 132 | +Trust model |
| 133 | +~~~~~~~~~~~ |
| 134 | +
|
| 135 | +A tree is trusted when its transitive object closure is known to |
| 136 | +be connected. |
| 137 | +
|
| 138 | +Every parent of an incoming commit is either on the |
| 139 | +already-connected side of the `--not --all` boundary (a boundary |
| 140 | +parent whose tree is trusted by definition) or is an earlier |
| 141 | +incoming commit that was already verified due to the topological |
| 142 | +processing order. In both cases the parent's tree is trusted, so |
| 143 | +all parent trees can be unconditionally used as comparison bases. |
| 144 | +
|
| 145 | +Once an incoming commit's tree has been verified, it is trusted |
| 146 | +and can serve as a comparison base for later descendants. This |
| 147 | +gives an inductive correctness argument: the base case is the |
| 148 | +trust boundary established by `--not --all`, and the inductive |
| 149 | +step is that verifying a commit extends the trusted set. |
| 150 | +
|
| 151 | +Tree states |
| 152 | +~~~~~~~~~~~ |
| 153 | +
|
| 154 | +The verifier tracks tree OIDs in three states: |
| 155 | +
|
| 156 | +untrusted:: |
| 157 | + The tree has not yet been established as connected. This |
| 158 | + is the implicit state of an OID not present in the state |
| 159 | + map. |
| 160 | +
|
| 161 | +trusted:: |
| 162 | + The tree is known to have a connected transitive closure, |
| 163 | + but its direct entries have not yet been published into the |
| 164 | + verifier's trusted object sets. |
| 165 | +
|
| 166 | +expanded:: |
| 167 | + The tree is trusted and its direct non-gitlink entries |
| 168 | + have also been published into the trusted object sets. |
| 169 | +
|
| 170 | +State transitions are monotonic: a tree may move from untrusted |
| 171 | +to trusted to expanded, but never backwards. An untrusted tree |
| 172 | +that is successfully verified goes directly to expanded. |
| 173 | +
|
| 174 | +Blobs require only trusted/untrusted state: a blob becomes |
| 175 | +trusted when it is found in a trusted tree or when its existence |
| 176 | +and type have been verified directly. |
| 177 | +
|
| 178 | +The trusted/expanded distinction is an optimization. An expanded |
| 179 | +parent does not need to be reread merely to publish entries that |
| 180 | +are already trusted, though it may still be read when same-path |
| 181 | +subtree bases are needed for recursive comparison. |
| 182 | +
|
| 183 | +Algorithm |
| 184 | +~~~~~~~~~ |
| 185 | +
|
| 186 | +At a high level: |
| 187 | +
|
| 188 | + verify(commits): |
| 189 | + sort topologically (ancestors first) |
| 190 | + for each commit: |
| 191 | + mark parent root trees as trusted |
| 192 | + verify_tree(commit.tree, parent root trees) |
| 193 | +
|
| 194 | + verify_tree(tree, base_trees): |
| 195 | + if tree already trusted: return |
| 196 | + read tree, collect entries not already trusted |
| 197 | + for each available base tree: |
| 198 | + publish its entries as trusted |
| 199 | + record same-path subtrees as bases |
| 200 | + for each collected entry: |
| 201 | + if now trusted: skip |
| 202 | + if blob: verify blob connectivity and type |
| 203 | + if tree: verify_tree(entry, its recorded bases) |
| 204 | + mark tree as expanded |
| 205 | +
|
| 206 | +The important ordering within `verify_tree` is that entries from |
| 207 | +the new tree are collected before the base trees are scanned, but |
| 208 | +are processed only afterwards. Trust learned from any base can |
| 209 | +therefore eliminate work before recursive verification begins. |
| 210 | +
|
| 211 | +Same-path parent subtrees are passed down as comparison bases |
| 212 | +during recursive descent. If no comparison base is available, |
| 213 | +the new subtree is verified from scratch. |
| 214 | +
|
| 215 | +Example |
| 216 | +~~~~~~~ |
| 217 | +
|
| 218 | +Consider a commit that changes one file under `lib/` and moves an |
| 219 | +unchanged subtree from `src/` to `dev/`: |
| 220 | +
|
| 221 | + Parent tree New tree |
| 222 | + +-- src/ (aaa) +-- dev/ (aaa) |
| 223 | + +-- lib/ (bbb) +-- lib/ (ccc) |
| 224 | + +-- foo.c (ddd) +-- foo.c (ddd) |
| 225 | + +-- bar.c (eee) +-- bar.c (fff) |
| 226 | +
|
| 227 | +Scanning the parent makes `aaa` trusted even though it moved from |
| 228 | +`src/` to `dev/`, so that subtree is skipped. The changed `ccc` |
| 229 | +subtree is compared against its same-path parent `bbb`; scanning |
| 230 | +`bbb` makes `ddd` and `eee` trusted, leaving only the new `fff` |
| 231 | +blob to be checked. |
| 232 | +
|
| 233 | +This illustrates two properties: |
| 234 | +
|
| 235 | +* Trust is OID-based rather than path-based. An unchanged subtree |
| 236 | + is recognized after a move. |
| 237 | +
|
| 238 | +* Same-path parent subtrees provide recursive comparison bases. |
| 239 | + These bases improve pruning efficiency but are not required for |
| 240 | + correctness; a new subtree can always be verified from scratch. |
| 241 | +
|
| 242 | +Multiple parents |
| 243 | +~~~~~~~~~~~~~~~~ |
| 244 | +
|
| 245 | +For a merge commit, root trees from all parents are comparison |
| 246 | +bases. When several parents contain a same-path subtree, each |
| 247 | +matching subtree is collected as a recursive comparison base. |
| 248 | +
|
| 249 | +Entries published from any trusted parent become globally trusted, |
| 250 | +so a matching tree or blob entry present in any parent can be |
| 251 | +skipped while verifying the merge tree. |
| 252 | +
|
| 253 | +Missing comparison bases |
| 254 | +~~~~~~~~~~~~~~~~~~~~~~~~ |
| 255 | +
|
| 256 | +A promised base tree may not be locally available for comparison. |
| 257 | +In that case the verifier skips that base and verifies the new |
| 258 | +subtree without it. The missing comparison can reduce pruning but |
| 259 | +does not affect correctness. |
0 commit comments