Skip to content

Releases: tkittich/git-remote-seafile

v0.9.7 - Hardened Upload Retries, Recycled PID Reclaim, and Unicode CLI Polish

Choose a tag to compare

@github-actions github-actions released this 16 Oct 04:56

What's Changed in v0.9.7

This release hardens upload reliability, fast-reclaims stale locks from recycled local PIDs, consolidates SQLite schema reading, and polishes Unicode path presentation in snapshot vaults. Nothing in how a snapshot or a push is stored changes.

Fixed

  • Upload retries in client.py refresh the upload link. On transient connection failures and 5xx errors, rewindable uploads obtain a fresh upload link before retrying, preventing immediate HTTP 400 Bad Request failures on Seafile fileservers that consume tokens on interrupted connections.
  • seafile_doctor.py errors printed nothing with default --days 0. The summary and error-group formatting logic was indented inside if args.days:, producing blank output when run with the default CLI setting.
  • Stale remote locks from recycled local PIDs are fast-reclaimed. A local lock ticket records process creation timestamps on Windows and Linux; _is_pid_alive verifies that an active PID matches the ticket's start time, reclaiming abandoned locks immediately even if the operating system reused the PID.
  • Doctor SQLite reads survive concurrent client syncing. Reading repo.db retries up to 3 times on torn WAL snapshots.
  • Snapshot tool outputs clean UTF-8 paths on Windows. Staging repositories and restores pin core.quotepath = false, preventing multi-byte UTF-8 filenames (e.g., Thai characters) from being octal-escaped in Git command outputs.

Changed

  • Consolidated repo.db SQLite schema reading (T-4). Factored read_repo_properties into sqlite_read.py with shared retry logic for torn live WAL databases, eliminating schema drift across safety.py and tools/seafile_doctor.py.
  • HTTP connection pool scaled. Configured HTTP client session with pool_connections=64 and pool_maxsize=64 to prevent pool exhaustion warnings during parallel payload transfers.
  • Remediation hints on HTTP 401/403. When resolving repository IDs fails due to authentication or permission errors, client outputs an actionable suggestion pointing to git-remote-seafile check-auth and desktop client login status.
  • Diff stat truncation. Passed --stat-count=35 in snapshot diff summaries to prevent large file listings from flooding console logs.
  • Standardized notification prefixes. Unified user-facing notifications across snapshot tools and CLI with title-case prefixes (Warning:, Note:, Notice:, Hint:).
  • Hardened lock test fixture hostname. Defaulted _ticket machine name in test fixtures to foreign-<hostname> so CI hosts named nodeA cannot accidentally flip foreign-ticket assertions.

The full list is in CHANGELOG.md.


Full Changelog: v0.9.6...v0.9.7

v0.9.6 - Long Paths No Longer Break a Push on Windows

Choose a tag to compare

@github-actions github-actions released this 15 Oct 17:16

What's Changed in v0.9.6

A push of a repository holding long non-ASCII paths failed on Windows with
git pack-objects failed: fatal: expected object ID, got garbage. Found on a real vault whose Thai
file names run to 400 bytes. Nothing about what is stored changes.

Fixed

  • A long path broke the push. pack-objects --stdin on git for Windows reads each <id> <path>
    line into a 302-byte buffer and takes the overflow for the next line. The path is only a hint for
    delta compression, so a line over 300 bytes is now sent as the bare object id; the pack holds the
    same objects.
  • A path containing U+0085, U+2028 or U+2029 was cut in two by the object listing
    (str.splitlines() splits on them). It is now split on \n only.

Not changed

  • What is stored, the pack layout and every setting.

The full list is in CHANGELOG.md.


Full Changelog: v0.9.5...v0.9.6

v0.9.5 - A Server Under a Path, a Torn Database Said Once, and a Test Runner That Does Not Hang

Choose a tag to compare

@github-actions github-actions released this 15 Oct 08:19

What's Changed in v0.9.5

A Seafile served under a path (https://corp.example.com/seafile/) now works. That was the one thing a
seafile:// URL could not express, and the one open item that blocked a whole class of users. The path
belongs to the server, so it is set with SEAFILE_SERVER, the credentials file, or the desktop client's
account; the no-code alternative (give the server a host name of its own at the proxy) is documented
beside it. Checked against a real Seafile server (CE 13.0.28, scratch library) through a local proxy that
serves it only under /seafile: push, clone and lock-status, 75 requests, none outside the prefix. The
suite is 1101 tests, also run on Linux (Debian under WSL).

Added

  • A server under a path via SEAFILE_SERVER, the credentials file's server, or the desktop
    client's account URL. API calls and relative transfer links go under it; the token stays scoped to
    host and port, never to the path. desktop-url prints the short form and names the setting for such a
    server. USER_GUIDE 7.1, with the proxy alternative.

Fixed

  • A synced-folder check that could not read the desktop client's repo.db said nothing. A copy
    torn by a concurrent write read as "no libraries", so the guard did not run. The read is retried and,
    if the file is there and still unreadable, one warning names it.
  • A restore that failed after the clone left a partial tree and the retry stopped with
    "destination is not empty". The failure now says where it was left and that it must be deleted first.

Changed (development)

  • The parallel test runner kills a target after --timeout (default 1800 s) and names it, stops
    every worker's process tree on Ctrl-C, closes a log whose spawn failed, and cannot turn a finished
    run into a traceback because Windows held a temporary directory.
  • Six suite gaps closed: credential branches, cmd_fetch fed what git sends, the "empty" pack
    outcome, a deterministic concurrency assertion, a gc test that checks the mirror it computed, and a
    stale cross-reference.

Not changed

  • A seafile:// URL still cannot carry a context path: the segment after the host is the library.

The full list is in CHANGELOG.md.


Full Changelog: v0.9.4...v0.9.5

v0.9.4 - Read It Back: `gc` Proves the New Pack by Content, and a Snapshot Checks Its Payload Objects After the Push

Choose a tag to compare

@github-actions github-actions released this 15 Oct 07:36

What's Changed in v0.9.4

The remaining items of the 0.9.3 review round, all of them about trusting what a server says. Checked
on the stub, on Linux (Debian under WSL), and against a real Seafile server (CE 13.0.28, scratch
library): the opt-in real-server tier, a payload snapshot with restore and verify, and a full gc
followed by a clone and fsck. The suite is 1079 tests.

Changed

  • gc and the tail merge read the new pack back and compare SHA-256 before deleting anything. The
    listing only proves a size, and a same-length substitution (a padded partial write, a damaged
    block) passed it and led to the originals being deleted. The check costs one more download of the
    new pack under the lock already held, about 0.015 s per MiB.
  • A snapshot run lists its payload objects once more after the push. A --payload-delete on
    another machine can remove an object between the pre-push check and the ref landing. The object is
    uploaded again when the source file is as it was; otherwise the run exits 1 naming it.
  • A ref deletion is read back. A server that answers 200 without deleting is an error now, not ok.

Tests

  • The unreadable-directory guard and the forced English git messages are covered on Windows too (the
    earlier test needed POSIX permissions and was skipped there).

Not changed, on purpose

  • Bounding the server's Date offset was rejected: a client clock that is wrong is exactly what the
    offset exists to absorb.

The full list is in CHANGELOG.md.


Full Changelog: v0.9.3...v0.9.4

v0.9.3 - The Third Review Round: `gc` Re-reads Refs Before Deleting, the Windows Hash Cache Sees In-Place Edits, and a Release Pre-flight

Choose a tag to compare

@github-actions github-actions released this 15 Oct 03:33

What's Changed in v0.9.3

Three independent reviews (GLM-5.3, Gemini, DeepSeek) went over 0.9.2. Every finding was reproduced
or refuted before anything changed; the fixes carry tests that fail without them. The suite is 1074
tests, also run on Linux (Debian under WSL), and the directory-listing question was measured against a
real Seafile server (CE 13.0.28, scratch library).

The two that mattered

  • Full gc deleted the old packs without re-reading the refs. A writer that ignored the lock and
    pushed while gc was repacking had its pack deleted and its ref left naming nothing. The refs are
    read again after the upload is verified; a change deletes nothing and says so.
  • On Windows the payload hash cache could vouch for a changed file. st_ctime is the creation
    time there and never moves, so a same-size in-place edit with the modification time put back was
    trusted and the old hash recorded. The key now holds NTFS's change time. The first run on Windows
    hashes every large payload file once more.

Fixed

  • A reaped-but-present expired lock ticket was rewritten fresh by renew(), putting a stalled holder
    back ahead of the peer that took over. It is LEASE_LOST when a live peer is behind it.
  • get_file_bytes enforced its cap only against a declared Content-Length; a chunked body was
    buffered whole. It is bounded as it is read.
  • A remote ref file whose content was an option (--stdin) reached git merge-base's argv.
  • Deleting a nested ref could remove a sibling created a moment earlier (the empty-parent prune).
  • --payload-gc --payload-delete re-reads the refs every 25 deletions or 10 s and stops on a change.
  • A file name that is not UTF-8 aborted an --exclude run; a name with a newline or a leading "
    made restore verification refuse; a vault lock that could not be written leaked its descriptor and
    left an empty lock file; --code-remote with --no-push was silently ignored.
  • Git's English messages are now forced (LC_MESSAGES=C) where the tool reads them.
  • A failed branch-delete rollback was reported as "(deleted)".
  • Plain unlock deleted a live local holder's ticket; it now refuses.

Decided

  • A lock-ignoring writer's lost last-writer-wins race is not rescued by a gc grace period; it
    stays documented. Every writer must use the helper.
  • The default conversion of an existing vault to payload mode is announced, and now repeated as the
    last line of the run on stdout and stderr.

Added and removed

  • tools/release_check.py answers before the tag exists what the release job answers after it.
  • The warning for the retired seafile.locksettle / seafile.gcmaxmib keys is gone.
  • Documents named tests that did not exist; a guard now fails on any test_... a document names that
    is not defined. DESIGN and USER_GUIDE now say what the code does about the ref read-back, the
    size-only pack proof, a token over plain HTTP, Trap 2's direction and LFS storage.

The full list is in CHANGELOG.md.


Full Changelog: v0.9.2...v0.9.3

v0.9.2 - Times Survive a Restore, `--force-with-lease` Works, and a Directory the Tool Cannot Read No Longer Vanishes From the Snapshot

Choose a tag to compare

@github-actions github-actions released this 14 Oct 18:07

What's Changed in v0.9.2

Two more rounds of independent review went over the snapshot tool: Opus twice on the code (the second
time on code written for this release), and GLM-5.3 and Gemini on the specification. Every finding that
reproduced is fixed with a test that fails without the fix. The suite is 1049 tests; the tool and helper
changes were also run on Linux (Debian under WSL) and against a real Seafile server (CE 13.0.28) in a
scratch library.

The one that mattered most

  • A directory the tool could not read was silently missing from the snapshot. git add prints a
    warning for a directory it cannot open and exits 0, so everything under it was absent and the run
    reported success. The run now stops and names the directory; --exclude leaves it out on purpose.

Added

  • Modification times of git-stored files are restored. A snapshot that pushes to a seafile:// remote
    records them in .seafile-payload/mtimes.json and --restore puts them back (payload files already
    carried theirs). --record-mtimes / --no-record-mtimes; an explicit choice is remembered; a run with
    nothing typed follows the last snapshot. Touching a file without changing it still makes no new snapshot.
  • git push --force-with-lease is supported: a push may rewrite history over exactly the value you
    saw, and is refused as stale info if the remote moved. Updates, deletions and --dry-run.
  • A fetch no longer advertises half a repository. A refs directory that its parent listed and that then
    answers 404 is retried once and then an error.
  • macOS network mounts are recognised by the clock probe.

Fixed in the snapshot tool

  • A restore overwrote and then removed a real git file named <payload file>.part, and certified the tree.
  • A manifest path through a link to .git (tool/hooks/pre-commit) passed the "inside the destination"
    check; a hostile manifest could fill the disk before its sizes were compared.
  • A share whose clock ran 2 to 5 s behind kept a stale hash (the noise floor was wider than the racy
    window); a share that could not be probed fell back to the machine's clock; a same-size edit that put the
    old mtime back was trusted. The cache key now holds the device and the inode change time, unprobed
    shares and other devices get a one-hour margin, and whole-second mtimes are trusted (twice the margin)
    instead of being re-read on every run of a FAT or HFS+ volume.
  • --no-embedded-repos after a capturing run wrote a gitlink naming the vault's own commit; a repository
    inside a repository crashed the run; one with no commit failed every run; a junction that is itself a
    repository was followed out of the tree.
  • --payload-gc decided from a listing that could have skipped a branch (so its objects looked unused),
    and from a decision minutes old. The listing is strict and the refs are read again before the first
    deletion.
  • A pid recycled by an unrelated process blocked every later snapshot on Windows.
  • The weekly report copied the whole vault into the temporary directory and ran on a vault's first run.
  • A manifest path using a code point HFS+ ignores was accepted; flags that would be ignored are refused.

Documentation

  • Corrected: the source is "never written to" (a probe file on a network share), objects are "never
    overwritten", a payload vault can snapshot offline (it cannot), --payload-verify covers every snapshot
    (the newest only), and 0.9.1's claim that the conflict-copy warning missed dotted refs (measured on a
    real server: it did not).
  • Stated: tags must be pushed to be seen by retention; the manifest is one line; a 0.8.8 machine restores a
    payload vault's git tree only, without a warning.

Upgrading from 0.9.1

  • A directory that cannot be read now stops the run: a scheduled job that had been skipping one will fail
    until it is made readable or excluded.
  • --payload-gc stops, deleting nothing, if a ref cannot be read or a push landed while it decided.
  • A vault gets .seafile-payload/mtimes.json on its next pushing run (about 60 bytes a file; about 1.5 MB
    compressed at 100,000 files, uploaded whole each time anything changes). --no-record-mtimes opts out.
  • No file format changed; 0.9.1 vaults and remotes are read as before.

Known gaps

  • The times record is one blob, rewritten whole when any file changes (sharding it is a format change).
  • A nested repository's history, and directory times, are not captured.
  • macOS (NFD names, HFS+, utime on symlinks) is covered by reasoning and CI, not by a Mac here.

The full list, with measurements, is in CHANGELOG.md.


Full Changelog: v0.9.1...v0.9.2

v0.9.1 - A Review Release: `gc` and `push` Can No Longer Lose a Ref Silently, Tokens Stay Where They Belong, and Nested Repositories Are Captured

Choose a tag to compare

@github-actions github-actions released this 14 Oct 09:30

What's Changed in v0.9.1

Three independent reviews (Opus, GLM-5.3, Gemini) went over the Git remote helper. Every finding that
reproduced is fixed here, each with a test that fails without the fix. The snapshot tool changes below
were not part of those reviews. The full suite is 987 tests; the helper changes were also run on Linux
(Debian under WSL) and against a real Seafile server (CE 13.0.28) in a scratch library.

The two that could lose data

  • gc deleted the commits of a branch it did not see. The compactor mirrored each remote ref as a
    loose file in a scratch repository, so refs whose names collide on the local filesystem (Feature and
    feature on Windows and macOS, NUL, a trailing dot) became one file; the branch that lost had its
    objects pruned and its old packs deleted, and gc exited 0. A ref whose read came back 404 was dropped
    the same way. The mirror is now one packed-refs file, git must read back exactly the refs written,
    and an unreadable ref stops gc with nothing deleted.
  • A push could write a ref that names objects the remote does not have. The pack left out what the
    refs read before the lock reached; a branch deleted (and gc run) in that window took its commits
    away while main was written pointing at them. The refs are read again under the lock, the pack
    directory is listed after the upload and checked at size, and the lease is verified before every ref.

Credentials

  • A returned transfer link could choose who received the Seafile token (http://other\@api-host/:
    the origin check and the connection parsed it differently). It is compared with the connection's parser,
    and an ambiguous authority is refused.
  • Transfer tokens no longer appear in connection-error text, and a URL password is masked in the four
    messages that printed it.

The smaller ones

  • A remote named gc, test or unlock ran that subcommand (git fetch gc ran a compaction).
  • A remote pack named as the uppercase twin of a local one overwrote it on Windows; a backslash in a
    seafile:// path bypassed the working-tree collision guard.
  • Windows scratch directories (grs-push-*) were never removed and were counted as removed.
  • A landed push could be reported remote rejected; a lock that could not be renewed warned on every
    16 KiB block; the conflict-copy warning missed refs with a dot; --dry-run said ok to deleting a
    ref that is not there; ref, HEAD and lock files were read with no size bound.

Snapshot tool

  • Nested git repositories are captured as their files (ignored files too, --exclude and payload
    mode apply); their .git is not. --no-embedded-repos keeps the old commit-id-only record.
  • The payload hash cache's racy rule uses the share's own clock on a network filesystem (a probe file
    measures it; a local tree is never written to). A same-size edit within one timestamp granule on a share
    whose clock ran behind used to keep its stale hash.
  • Long Windows paths: core.longpaths on the vault and on a restored clone, extended-length paths for
    restored payload files.
  • A report-only --payload-gc summary after a run that pushes, at most every --payload-report-days
    (default 7, 0 off). It never deletes.
  • Manifest growth at 100,000 files is measured: about 10 kB per snapshot at 0.1% changed, 110 kB at 1%.

Upgrading from 0.9.0

  • gc is stricter: it stops, deleting nothing, when a ref or a refs directory cannot be read. Re-run it.
  • A push makes two more requests (refs under the lock, a pack directory listing).
  • A vault whose source holds a nested git repository gets its files on the next snapshot; add
    --no-embedded-repos to keep the old record.
  • Pack names must be lowercase and a seafile:// path may not contain a backslash.
  • Set the library's history to a few days (USER_GUIDE 8.0.1).
  • No file format changed; 0.9.0 vaults and remotes are read as before.

Known gaps

  • --force-with-lease is not supported (documented; it was rejected as non-fast-forward without a reason).
  • macOS network mounts are not detected by the clock probe; --payload-rehash is the audit.
  • The nested repository's history, and git-tracked files' timestamps, are not restored.

The full list, with measurements, is in CHANGELOG.md.


Full Changelog: v0.9.0...v0.9.1

v0.9.0 - Payload Mode: a 131 GiB Tree Through Seafile Without Going Through Git, and the Snapshot Tool Installs with the Package

Choose a tag to compare

@github-actions github-actions released this 14 Oct 06:42

What's Changed in v0.9.0

The snapshot tool (git seafile-snapshot) can now carry a tree that is mostly large,
immutable data. Files above a threshold, or that do not compress, bypass git: each is
hashed and stored on the server under its SHA-256, and the snapshot is a manifest.
A 131 GiB tree (633 payload files, 124.6 GB) was snapshotted in 38 minutes against a
real Seafile server and restored byte-exact, verified file by file.

Upgrading from 0.8.8

  • The snapshot tool ships in the wheel: git-seafile-snapshot (so git seafile-snapshot)
    and git-remote-seafile snapshot. python tools/seafile_snapshot.py still works from a checkout.
  • Payload mode is on by default (auto: files of 64 MiB or more, or 4 MiB or more that do not compress) for snapshots that push to a seafile:// remote. A tree with no such file is snapshotted exactly as in 0.8.8. --no-payload turns it off and the vault remembers that.
  • An existing vault with large files in git changes on its next run: they leave the git tree and are uploaded as payload objects (one full pass; stored twice on the server, since the old blobs stay in the history). Older snapshots restore as before; newer ones need the payload, and 0.8.8 cannot restore them completely. Run once with --no-payload to keep a vault as it was.

Added

  • Payload mode (--payload-min-size [SIZE|auto]): large files stored by hash under
    <path>/payload/objects/, a manifest in the vault commit, parallel uploads, a
    per-machine hash cache, and a verified --restore. The setting is remembered by the vault.
  • --payload-gc (two-phase retention, --payload-keep N for expiry), --payload-verify
    (audit by download and hash), --payload-view (a browsable directory of real names
    by server-side copy), --payload-rehash.
  • --no-payload, a .seafile-payload/README.txt in every payload vault, and a size line for the
    payload store after an upload.
  • Safeguards (PAYLOAD.md D13): each snapshot commit records the tool's version and a run warns when it
    builds on a newer tool's snapshot; --payload-delete needs --vault (or --payload-no-vault) and stops
    with --payload-accept-loss when the remote's history was rewritten or --payload-keep would expire
    snapshots (a tag is a pin); a restore that would not fit the disk is refused before downloading; an
    upload over the quota or into a library this machine syncs is warned about first; --payload-verify
    leaves a marker that the gc report shows.
  • The specification, with two rounds of independent review, is PAYLOAD.md.

Changed

  • SeafileClient.delete_entry forgets the directories under a deleted path.
  • --help groups the payload options and states each default.

Fixed

  • --restore --ref <rev> failed against any remote whose HEAD is the vault branch.

The full list, with measurements, is in CHANGELOG.md.


Full Changelog: v0.8.8...v0.9.0

v0.8.8 - Measured Against a Real Server: Size Caps, Chunked Uploads, and One Fewer Moving Part

Choose a tag to compare

@github-actions github-actions released this 13 Oct 13:07

What's Changed in v0.8.8

This release started as a simplification pass and became a size release when the changes were finally
measured against a real Seafile server (CE 13.0.28). Three things the stub could not have told us:

  • A single request of 2 GiB is reset (1.75 GiB is accepted), so an uncapped 4 GiB tail merge failed at
    its upload and a single 20 GB raw-data pack could never have been pushed.
  • The same server assembles a 5 GiB file from Content-Range chunks and serves it back
    byte-identical.
  • Git for Windows with core.autocrlf=true silently truncates a 4 GiB+ blob at git add (5 GiB came out
    as 1 GiB) and stalls git clone of one. The helper cannot prevent the first; it now warns before the
    second.

Upgrading from 0.8.7 or earlier

  • seafile.autogc is on by default; git config seafile.autogc false restores tip-only behaviour.
  • Usage errors exit 2 on stderr (they were 1 on stdout).
  • seafile.locksettle and seafile.gcmaxmib are retired (reported once, then ignored);
    seafile.gcmaxpackmib is now the largest pack the helper writes.
  • Clients older than 0.8.4 must not share a remote with this version.

Added

  • Pack size cap and chunked upload. Every pack is capped at seafile.gcmaxpackmib (1 GiB); files of
    1 GiB or more upload in 128 MiB Content-Range chunks, checked against the listing afterwards.
  • git-remote-seafile doctor [url] - login, how the URL is interpreted, safety guards, a write probe.
  • gc --tail - run the automatic tail merge by hand.
  • A "Known Limitations and What They Mean for You" section in the User Guide.
  • A real-server test tier (opt-in) and a differential test of ref-name validation against git.

Changed

  • Automatic tail merge defaults on; budget is 2x the cap; packs at least half the cap are frozen.
  • One output and exit-code convention for every subcommand; cli.main is a dispatch table.
  • cmd_push is a sequence of named steps shared with --dry-run.
  • Concurrent pushers wait by queue progress, not wall time; a waiting ticket is not re-uploaded each poll.
  • Push-time pack-objects no longer delta-searches big blobs (50 s to 5 s on the measured payload).
  • The snapshot tool's source is cohesive parts built into one file; its watchdog is thread-free.
  • Snapshot tool: the raw-payload profile starts at a sample ratio of 0.5 (was 0.9), so mixed data stages at 125-200 MiB/s instead of ~45 (131 GiB: ~50 minutes before, ~20 now); a free-space guard and a temp-pack sweep protect large runs.

Fixed

  • Encrypted libraries are refused clearly; ref-name validation matches git check-ref-format.
  • Conflict copies are named when they appear; HEAD read failures cannot half-advertise refs;
    deleting HEAD's branch says how to repair it; uploads wait for the server's reply; stderr is UTF-8.
  • Snapshot tool: git failures are not "diverged", settings are remembered only after success,
    --restore finds the remote's only branch.

The full list, with measurements, is in CHANGELOG.md.


Full Changelog: v0.8.7...v0.8.8

v0.8.7 - The Fail-Loud Pass: a Bounded Subprocess, Three Client Lies, and the Two Halves of D6

Choose a tag to compare

@github-actions github-actions released this 12 Oct 02:19

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 git invocation is bounded (H-24). The tree had timeouts on its HTTP calls and none at all
    on its twenty-five subprocess sites. 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 —
    a gc whose git repack never 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_objects is 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 returned False
    for every non-200, while list_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 a False now. The pruning loop already caught the raise and stopped; mkdir_p now
    aborts with the client's own message instead of proceeding to a creation it could not confirm.

  • .gitignore gained .coverage.*. Parallel mode — which pyproject.toml turns on for the whole
    suite, so that a worker cannot overwrite the driver's data — writes one data file per process, and the
    plain .coverage rule matched none of them.

Fixed

  • A truncated download is a failure, not a success (A12.10). download_file_to read
    Content-Length into the progress callback and never compared it against what it had written, so a
    short body returned True. 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 no Date header 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 named REVIEW.*.md, archive/…, (N-16) and (Qwen …); every one of them lives in the
    gitignored archive/, 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 typecheck CI job (D6 residual). mypy 2.4.0, pinned, run bare so that the scope and the
    strictness come from [tool.mypy] in pyproject.toml rather than from the workflow — a local mypy
    and CI cannot disagree about what is checked. The package and tools/ 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 are MagicMocks 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_ignores is on, so an ignore that has stopped being necessary fails the job
    instead of absorbing the next real error on its line.

  • A coverage CI job (D6 residual). It runs the whole suite under coverage and 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: parallel keeps each process's data in its own file instead of letting the last one
    overwrite it, and patch = ["subprocess"] installs a startup hook and puts the setting in the
    environment — the hook lives in site-packages and the variable is inherited through git, 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_under chosen 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