|
| 1 | +# 2026-09-19 — Refuse partial clones too? No. The ambiguity was one call, not one environment. |
| 2 | + |
| 3 | +Round: `rustjava-partial-clone-refusal-decision` |
| 4 | +Adopted proposal: `2026-09-18-merge-drops-no-silent-git-failure#p0` |
| 5 | +— *"The check now refuses to run in a shallow clone, but a clone fetched without file contents can |
| 6 | +still make it look like nothing is there."* |
| 7 | + |
| 8 | +## Decision |
| 9 | + |
| 10 | +**Do not refuse partial clones.** Fix the one call that could not tell "the path is not in this tree" |
| 11 | +from "the blob is not here": `symbols()` now asks `git ls-tree` before reading a failed `git show` as |
| 12 | +absence. The decision and its numbers are recorded in `preflight()`'s docstring so the next round |
| 13 | +does not re-ask. |
| 14 | + |
| 15 | +## The proposal was half right, and the half matters |
| 16 | + |
| 17 | +Everything below was measured on a real `--filter=blob:none` clone of this repository, not reasoned |
| 18 | +about. Range `e53b2142^..e53b2142` throughout — the known-accident merge, 6 dropped definitions. |
| 19 | + |
| 20 | +| clone | promisor | result | time | |
| 21 | +|---|---|---|---| |
| 22 | +| full | — | `6 definition(s) dropped` · **rc 1** | 2.5 s | |
| 23 | +| blobless | **reachable** | `6 definition(s) dropped` · **rc 1** — *identical* | 15.7 s | |
| 24 | +| blobless (fresh) | **unreachable** | `0 definition(s) dropped` · ★**rc 0 — green** | 8.3 s | |
| 25 | + |
| 26 | +So: |
| 27 | + |
| 28 | +- **"A partial clone makes it look like nothing is there" is false on its own.** With the promisor |
| 29 | + reachable git fetches the blobs transparently and the answer is byte-identical. Refusing partial |
| 30 | + clones would refuse a setup that works — and unlike a shallow clone, a partial one can go get what |
| 31 | + it is missing. |
| 32 | +- **The silent green is real, but the trigger is narrower**: the promisor has to be *unreachable*. In |
| 33 | + that state the check printed `✓ … (3 file(s) examined)` and exited 0 where a full clone reports six |
| 34 | + dropped definitions. That is exactly the failure class this script exists for. |
| 35 | + |
| 36 | +*Method note*: the first offline attempt reported the **correct** answer, because the earlier online |
| 37 | +run had already cached those blobs. The measurement above is from a **fresh** clone with the remote |
| 38 | +broken before any read. A contaminated clone answers the wrong question. |
| 39 | + |
| 40 | +## Is the risk realised here? No — measured, and it does not change the decision |
| 41 | + |
| 42 | +`git ls-files .github | grep filter:` → nothing; **no workflow uses a partial clone**, and |
| 43 | +`merge_drops` explicitly checks out with `fetch-depth: 0`. So today the false green needs a developer |
| 44 | +laptop with a blobless clone and no network. That is *why* refusing would be the wrong trade — it |
| 45 | +would cost a working configuration to close a case nobody is in — but it is not a reason to leave the |
| 46 | +ambiguity, because the cost of closing it turned out to be one git call. |
| 47 | + |
| 48 | +## Why `ls-tree`, and not the two alternatives |
| 49 | + |
| 50 | +- **Not refusal** (`remote.origin.partialclonefilter`): the detection works — measured `blob:none`, |
| 51 | + while `rev-parse --is-shallow-repository` returns false, which is exactly why the existing |
| 52 | + preflight misses this case — but it blocks the reachable-promisor case that gives the right answer. |
| 53 | +- **Not matching git's error text**: the two failures *are* distinguishable by message — |
| 54 | + `fatal: path 'X' does not exist in 'REV'` versus |
| 55 | + `fatal: could not fetch <sha> from promisor remote` — but both exit 128, and the round that added |
| 56 | + `preflight` already weighed and deferred message-matching as fragile across git versions. Nothing |
| 57 | + here changes that. |
| 58 | +- **`ls-tree` answers from the tree object**, which a partial clone holds even when it lacks blobs. |
| 59 | + Measured identical behaviour in both clones: path present → one entry, rc 0; path absent → empty |
| 60 | + output, rc 0. No message matching, no environment refused. |
| 61 | + |
| 62 | +## Axis (bidirectional, on the product script) |
| 63 | + |
| 64 | +| form | blobless + unreachable promisor | full clone | |
| 65 | +|---|---|---| |
| 66 | +| before | `0 dropped` · **rc 0** (silent green) | `6 dropped` · rc 1 | |
| 67 | +| after | ★**rc 2** `cannot measure: …:jvm-bytecode/src/class_definition.rs is in that tree but its content could not be read…` | `6 dropped` · rc 1 | |
| 68 | + |
| 69 | +And the case that must **not** break: blobless with a reachable promisor, after the fix → **rc 1, 6 |
| 70 | +dropped**. Over-blocking 0. |
| 71 | + |
| 72 | +**Cost**: none measurable. Same range, three runs each — before 7.78 / 7.99 / 7.05 s, after 6.39 / |
| 73 | +6.72 / 8.06 s (overlapping ranges). DoD's default range `origin/main..HEAD`: 0.46 s → 0.33 s. The |
| 74 | +extra call only runs when `show` has already failed. |
| 75 | + |
| 76 | +## Found while measuring, not fixed here |
| 77 | + |
| 78 | +The checker's **output order is not stable between runs** — findings are emitted from a set. Measured |
| 79 | +on `origin/main`'s own version with `PYTHONHASHSEED=random`: five runs gave two different orderings |
| 80 | +(4 + 1). The rc and the set of findings are identical; only line order moves. This is pre-existing |
| 81 | +and unrelated to this round — it is noted because it briefly looked like a regression in the diff of |
| 82 | +before/after output, and the next person comparing two runs deserves to know. Filed as a proposal. |
0 commit comments