Repository navigation
v0.8.7 - The Fail-Loud Pass: a Bounded Subprocess, Three Client Lies, and the Two Halves of D6
What's Changed in v0.8.7
A patch release: every change in it is a defect rather than a capability. The theme is the one the
fail-closed principle has always stated and the code did not always follow — a check that cannot
answer must not be read as an answer, and a command that never finished must not be read as one that
did.
Three of the four groups below are that sentence in different clothes. Nothing bounded a subprocess, so
a wedged git repack held the remote lock past its own expiry while another client lawfully took over.
A download compared nothing against its Content-Length, so a truncated packfile was reported as a
success. A dir_exists() answered "does not exist" for every non-200, including the ones that mean "I
could not tell". And the shipped source cited twenty-three documents that are not in the repository, so
no reader could follow a single one of them.
The fourth is the two halves of D6 that the v0.8.6 pass deliberately left undone — and the reason it
left them is the reason they were worth doing properly: a type check that is red on arrival and a
coverage report nothing produces are both worse than nothing, because they look like enforcement.
Changed
-
Every
gitinvocation is bounded (H-24). The tree had timeouts on its HTTP calls and none at all
on its twenty-fivesubprocesssites.git_util.run_git,gc's runner and the snapshot tool's
runner now bound every call and fail loudly, naming the command and the limit.The bound is a constant rather than a configuration key, on purpose: a repository must not be able to
raise the ceiling that protects its own concurrency guarantee. The failure it prevents is specific —
agcwhosegit repacknever returns holds the remote lease open past expiry, at which point
another client lawfully takes over the lock this one still believes it holds.filter_existing_objectsis the one deliberate exception: a timeout there warns on stderr and answers
"assume no objects are present", because the cost of a false negative is a re-upload rather than a
lost object. -
dir_exists()no longer answers "does not exist" for a transient error (H-7). It returnedFalse
for every non-200, whilelist_dir()beside it raised for anything that was not 200 or 404 — the
two disagreed about the same state, and the ref-pruning path acted on the optimistic reading. Only an
explicit 404 is aFalsenow. The pruning loop already caught the raise and stopped;mkdir_pnow
aborts with the client's own message instead of proceeding to a creation it could not confirm. -
.gitignoregained.coverage.*. Parallel mode — whichpyproject.tomlturns on for the whole
suite, so that a worker cannot overwrite the driver's data — writes one data file per process, and the
plain.coveragerule matched none of them.
Fixed
-
A truncated download is a failure, not a success (A12.10).
download_file_toread
Content-Lengthinto the progress callback and never compared it against what it had written, so a
short body returnedTrue. A packfile that is quietly incomplete is a corrupt object store, found
later and far from the cause; a mismatch now raises. -
A server time that was never learned is announced (H-8).
get_server_time()fell back to the
local clock whenever noDateheader had been seen, which rested the lock's ordering on a clock the
protocol does not trust. It still falls back — a server that sends no date must not make the lock
unusable — but it now says so once, on stderr. -
The shipped source stops citing documents a reader cannot open (C8). Twenty-three comments across
seven files namedREVIEW.*.md,archive/…,(N-16)and(Qwen …); every one of them lives in the
gitignoredarchive/, so none could be followed from a checkout. The reasoning is inlined, and an
identifier survives only where it names a fact about the review rather than standing in for the
explanation.
Added
-
A
typecheckCI job (D6 residual). mypy 2.4.0, pinned, run bare so that the scope and the
strictness come from[tool.mypy]inpyproject.tomlrather than from the workflow — a localmypy
and CI cannot disagree about what is checked. The package andtools/are clean.tests/is deliberately out of scope: it is a namespace package, so mypy resolves each module under
two names and stops before checking anything, and its test doubles areMagicMocks whose attributes
no annotation can describe. A check that has to exclude a third of what it sees is not one people
trust.warn_unused_ignoresis on, so an ignore that has stopped being necessary fails the job
instead of absorbing the next real error on its line. -
A
coverageCI job (D6 residual). It runs the whole suite undercoverageand publishes the
per-file table to the job summary, because a workflow log needs admin rights to download and a job
summary does not.[tool.coverage.*]holds the configuration, and two settings there are load-bearing rather than
tidiness:parallelkeeps each process's data in its own file instead of letting the last one
overwrite it, andpatch = ["subprocess"]installs a startup hook and puts the setting in the
environment — the hook lives in site-packages and the variable is inherited throughgit, so the
helper is measured as well as the workers. Without both, the report would describe the driver process
alone: a figure that looks like coverage and is not.There is deliberately no threshold. A
fail_underchosen to make today's build green is one that
gets raised for the wrong reason or lowered for a worse one; what the job is for is that a drop is
visible in a review. At this release it reads 91% of the package (2944 statements, 264 missed) —
a dated observation, not a target. The report is also a measurement rather than a promise: it counts
the paths a stub exercises, and
DESIGN.md §4.3.1 says plainly
that none of this has been validated against a real Seafile instance.
Tests & Tooling
Twelve new tests in four classes cover the changes above: the three client.py defects, and the
subprocess timeout in git_util, in gc and in the snapshot tool. Suite: 681 → 693 tests / 134 → 138
targets. ROADMAP.md is re-targeted: the six rows a patch release could carry are closed and recorded
in §2, and every row that is left still names v0.9.0, v1.0.0 or won't do.
Full Changelog: v0.8.6...v0.8.7