Skip to content

Catalogue and download private data from SSH, HTTPS, WebDAV and S3 servers - #62

Merged
denniswu28 merged 55 commits into
codex/function-atomic-replay-20260928from
feat/private-data-backend
Oct 21, 2026
Merged

denniswu28 merged 55 commits into
codex/function-atomic-replay-20260928from
feat/private-data-backend

Conversation

@denniswu28

@denniswu28 denniswu28 commented Oct 19, 2026 •

Copy link
Copy Markdown

Hallmark can now catalogue data that lives on private servers and download it on demand, with every file verified and recorded for commit.

Closes #51. Closes #54.

What this adds

  • Servers: SSH and SFTP (OpenSSH 8.0 or later), HTTP(S) with basic or bearer authentication, WebDAV, and S3 (optional pip install hallmark[s3]). Credentials come from typed profiles in a per-user auth.yml, never from the repository or a URL.
  • Sources: one catalogue can combine selections from several folders and servers, recorded under sources: in config.yml. Adding a source again updates it:
    • new files are added;
    • files that changed upstream are listed;
    • files gone from the server are removed or kept after asking (--prune / --keep-missing).
  • Clashes: paths another source already catalogs are labelled as same content, different content or not comparable. hm add then offers to skip them, keep both under a folder, or abort (--skip-duplicates in scripts). Paths that differ only in letter case get a warning.
  • Filename formats (Sam's work from the base branch, merged in):
    • formats belong to the whole catalogue and sort every source's files, each format into its own TSV;
    • files no format matches become file: entries;
    • hm detect suggests formats;
    • when a new format arrives, hm add asks whether to re-sort this source's files, every source's files (all), or only unsorted ones.
  • Checksums: taken from published manifests, WebDAV properties, S3, or the server itself (--server-checksum). The checksum_source column records where each came from.
  • Downloads: each download runs plan → approval → .hm/objects → worktree → staged SHA-1.
    • Files are fetched into a content-addressed, read-only object store, with size and digest verified.
    • They are hard-linked into the worktree, or copied across filesystems.
    • Each file's SHA-1 is staged for commit.
    • An interrupted download leaves nothing partial, and finished files are still recorded.
    • Outdated files are replaced only after approval, and old versions stay in .hm/objects, so checking out an older commit restores them without a transfer.
    • Edits made in place to a downloaded file are detected.
  • Clone: published catalogue snapshots or Git repositories; data is downloaded only after approval.

Changes to existing behaviour

  • The column naming a file's source is catalog_source, so {source} stays free as a format field (for example M87).
  • Formats no longer select files; --filter does.
  • A scan that fails or matches nothing exits with an error and keeps the previous catalogue.
  • Downloaded files are read-only links into the object store; copy a file to edit it. Local files are still writable copies, as before.

Testing

  • Locally: 1497 tests pass on Python 3.13, including the loopback SSH tests, and 1496 on Python 3.9 with Click 8.1. Sphinx builds with -W.
  • CI: all 19 jobs pass, including Python 3.9 to 3.14 on Ubuntu, 3.9 and 3.14 on macOS, Rocky Linux 8 and 9 OpenSSH, S3 against moto, the lowest supported dependencies, and pre-release dependencies. Combined coverage is 99%, and Codecov reports 99.35%; the S3 job's coverage now counts toward it.
  • demo_private_data.ipynb and demo_add.ipynb run as tests.
  • Manual end-to-end runs of hm against local HTTP servers covered:
    • the duplicate prompt;
    • replacing a file that changed upstream, then restoring the old version from .hm/objects on a branch switch;
    • hm detect --add followed by download and status.

Also fixed

Edge-case tests found three older bugs, fixed here with regression tests:

  • --prefix ./ (or .) failed with "Remote path cannot be empty".
  • restore --staged . did nothing on a branch with no commits.
  • A local repository in a folder whose name contains braces, such as run_{id}, globbed a sibling folder's files.

Not in this PR

  • hm gc for objects no commit references.
  • Mirrors with automatic fallback.
  • Restoring a single file: entry with restore --staged (restore .hm/config.yml instead).
  • The notebook-style Python API (hm.clone("eht/2026:science-release-1")), planned as a separate branch.

🤖 Generated with Claude Code

denniswu28 and others added 30 commits October 11, 2026 22:28
Remote add and set-config --remote-url now refuse URLs carrying a password,
an HTTP(S) username or token, a query string or a fragment before any
network access, so no secret reaches config.yml. Clone refuses a source URL
with a password or an HTTP(S) username before creating anything, since Git
would save it in .hm/.git/config. The errors do not repeat the URL. SSH
usernames remain allowed. The private data guide says where credentials
belong instead.

Ported from e4e8376 on feat/private-data-v2; clone keeps codex's split into
clone_catalog and _prepare_catalog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finish discovery before staging and report empty selections through the API and CLI. Keep the regression tests and user guidance with this behavior.

Prerequisites: Existing remote discovery and catalogue staging APIs.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(86 passed in 5.53s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: 8bfc7da51e5db96dc924b516071b51d2eb727623
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Select compatible client options and validate keepalive, compression and session settings together with their transport tests, CI jobs and documentation.

Prerequisites: Existing SSH transport and version-one auth profiles.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(131 passed in 17.90s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: 78c65daee229930eaba8cfb5dde6fefdd5edd965
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Persist published or server checksum provenance, migrate the shared catalogue predicate and update every importer in the same revision.

Prerequisites: RemoteEntry metadata and repository state.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(122 passed in 5.39s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: 7f077ea3b1ec1edf01a09ba62caddae3a09c67ea
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Prepare source validation, path routing, legacy state migration and clone validation before exposing multiple-source additions.

Prerequisites: Shared remote catalogue detection and RemoteSpec.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(103 passed in 12.46s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: 1b8dea8285a7d3db07cf7ea0960dac27033d81f9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Expose source selections only with complete add, configuration, plan and download behavior. Keep collision handling, pruning, CLI callers and lifecycle tests together.

Prerequisites: Named source model, migration helpers and clone validation.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(309 passed in 32.07s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: ce23eb85ba06f04fb16568e130ec96f88c4a80ef
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Provide streaming multi-digest verification and complete no-overwrite publication with the shared hard-link fallback constants needed by the cache.

Prerequisites: Existing streaming checksum and temporary-file utilities.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(24 passed in 2.14s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: d25f0db3da0770e4a998a18d0cdfb7d98a061d53
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Batch literal NUL-separated version paths and avoid rewriting unchanged records. Preserve normal catalogue save behavior while preparing bulk downloaded hash staging.

Prerequisites: Version record serialization and source-aware catalogue state.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(95 passed in 16.14s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: 4121651849014a5c8827707155fce742a295efaf
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Introduce temporary cache files, read-only objects, hard-link placement, verified copy fallback and stale temporary cleanup. Repositories share the cache across their data worktrees.

Prerequisites: Complete no-overwrite publication and object verification helpers.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(71 passed in 14.73s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: 82fb4aa994ff305aa4ff013ffb94b03b8ff6bb71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reject truncated or oversized transfers even without a published digest. Carry known SHA-1 metadata through complete plan and transfer functions and update their callers and regression tests.

Prerequisites: Multi-digest hashing and routed DownloadItem values.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(176 passed in 7.19s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: 1f6dc369acae2eb56e4d047ea0975a130eb21bbc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Expose the cache workflow with verification, placement, state saving, interruption recovery and retry behavior complete. Include cached plan counts, dry-run output, CLI reporting and the user guide at the first usable revision.

Prerequisites: Cache lifecycle operations, verified downloads, source routing and batched state persistence.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(320 passed in 29.79s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: 8154f0da23b57860f5ea5a39b80c9e306ac281ae
Replay-Source: 9407bbb69d80b8a1f24ea3fa0980fccc00fe8ca0
Replay-Source: 057769766822b2c2b446991c8e9dd48fe530e9d3
Replay-Source: f26c74c8af2e225440afb9a858d9b24bbb9526a5
Replay-Source: 6ce26797e782ac1091efb923457a6a7b024ba6a2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the complete secret reference and resolution layer with its independent tests before auth profiles consume it. Values remain redacted and helper errors do not echo secret output.

Prerequisites: Existing transport configuration and capability errors.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(9 passed in 0.16s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: dee99043bd5e0fb5340599f090141353c5b2bf02
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add version-two SSH, HTTP and S3 profiles, host and endpoint bindings, ownership validation and operation-context resolution. Preserve version-one SSH behavior and complete profile tests.

Prerequisites: Secret references and existing SSH settings.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(156 passed in 7.13s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: dee99043bd5e0fb5340599f090141353c5b2bf02
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve credentials before requests, send them only to permitted hosts and explain authentication failures. Exercise discovery, downloads and cross-server redirects using loopback HTTP fixtures.

Prerequisites: Typed HTTP profiles and backend auth resolution.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(154 passed in 10.21s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: b22a47ae6c57b87e2db92acef882f1d2801b5675
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Introduce PROPFIND listing, metadata limits, checksum provenance, URL shorthand and backend selection as one usable backend. Reuse HTTP authentication and test complete loopback catalogue/download flows.

Prerequisites: Authenticated HTTP requests, checksum provenance and backend registry.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(143 passed in 13.79s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: 78635463da79b28a302570546a89b36ed7d45f99
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add S3 URL validation, paginated listings, whole-object digests, profile credentials and streaming transfers together with optional packaging dependencies, moto tests and CI.

Prerequisites: Typed S3 profiles, multi-source routing and complete cache downloads.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(92 passed in 3.15s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: c3ee60484ce64077198180ac86f116cbc86de76c
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Carry server-checksum selection through source persistence, discovery, SSH batching, API and CLI in one change. Preserve capability errors and test real disposable SSH and SFTP-only accounts.

Prerequisites: Source metadata, checksum provenance and compatible SSH transport.

Validation: whole-suite collection, CLI help, Ruff and focused tests
(121 passed in 16.91s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Replay-Source: d02603c2bcd89bb32040650e28af068e6927b5be
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on notebook

Introduce the final Python demo with a disposable local HTTP dataset, catalogue selections, cached downloads and staged SHA-1s. Include the notebook runner and README at the same revision; keep account-dependent examples illustrative.

Prerequisites: Multiple sources, complete cache workflow, authentication and backend APIs.

Validation: whole-suite collection, CLI help, CI-scoped Ruff and focused tests
(4 passed in 36.05s).
Checks ran in an isolated tracked-tree snapshot; no live data services.

Broad Ruff reproduces the frozen source notebook E501 (89 > 88).
Preserve that source line for exact tree fidelity.

Replay-Source: 4b2dd1503a4dbd013308a292ed2fbaada8fd81d7
Replay-Source: d536fea66391d5df222118d2c449f45504652b11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Downloads and committed local files now share .hm/objects, as git keeps all
content in one object database, instead of a separate .hm/cache that could
drift from it. Every object is read-only; checkout restores writable copies of
local files, while downloads are still hard-linked read-only.

The download path for callers without a store is removed. It had drifted from
the store path: it discarded each file's SHA-1, reported different keys and
left .part files behind. Its merging of repeated plan items now applies to
every download.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DownloadPlan.cached_items becomes stored_items and the result key from_cache
becomes from_store. Plan summaries and CLI messages name .hm/objects instead
of a cache that no longer exists. These fields were added on this branch and
never released.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A download is hard-linked to its object, so making it writable and editing it
in place, or writing to it as root, also changed the object. Planning then
reported the file as already downloaded, and later downloads linked the
changed bytes into new paths as a success.

Each object now has a record of its size and modification time in
objects/stat/, as git's index and git-annex's inode cache keep for tracked
files. A link, or an object about to be placed, is trusted while it matches
the record. Otherwise the object is hashed: a touched object is recorded
again, and a changed one is discarded and downloaded afresh. The edited file
is reported as a conflict, and hm status lists it as modified without
hashing anything. Objects stored before records existed are trusted once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows reached the catalogue through several writers that each formatted values
differently. Remote adds wrote parsed fields as JSON numbers that later saves
turned into strings, so adding a source rewrote every other source's records.
Re-adds compared "123.0" and "nan" against "123" and "", which dropped known
SHA-1s. hm add PATTERN kept "005" while hm add PATH and set-config --fmt
stored "5". data.tsv kept insertion order and the platform's line endings,
while restore wrote sorted LF text.

save_state now writes every row through normalize_rows: text values, missing
values as "", whole-number floats without ".0", sorted by path, LF endings.
That is the form load_state returns, so saving a loaded catalogue changes
nothing. Field values are read as the text of the name everywhere, local and
remote, as hm add PATTERN always did: a remote "{day:03d}" field now stores
"002" rather than "2".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every save wrote the catalogue twice: as data.tsv and as one versions/ JSON
file per row. The two could disagree. Loading read versions/ from the index,
download selection read data.tsv from disk, and unstaging or a rename
detected by git could leave them different. Each row also cost a file write
on every save and a blob read on every load, and versions/ names collided on
case-insensitive filesystems.

data.tsv, sorted by path with one line per row, is now the only record, so
git still shows each row's changes on its own line. The first save in an
existing repository stops tracking versions/ and versions.yml, which older
commits keep and load_state still reads. Unstaging rows swaps them in the
catalogue from HEAD and, when nothing else changed, restores HEAD's exact
data.tsv; the copy on disk stays as it was. Downloads select rows from the
loaded catalogue rather than the file on disk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…umns

is_remote_catalog took any "source" column as proof of a remote catalogue.
A local pattern with a {source} field, the config template's own example,
was therefore treated as remote: hm commit stored none of its files, and
checkout restored none. An older remote catalogue, recognized by a path
column without sha1, became indistinguishable from local files once a
download added that column; hm commit then failed with KeyError: 'sha1' and
hm add URL was refused.

A catalogue is remote when config.yml lists its sources, or while it keeps
the older shape. Downloads record an older catalogue's source before adding
SHA-1s, as hm add URL does; one naming several remotes keeps its shape and
the download says why no SHA-1s were recorded. Filename patterns may no
longer name fields after the catalogue's own columns, which downloads read
as sizes and checksums; remote patterns used to drop such fields silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalogue paths rejected .hm and .git only as the first folder, so a
published or cloned catalogue row such as x/.git/hooks/post-checkout passed
validation and a download would plant a git repository inside the data
folder. Listings and source prefixes each kept their own copy of the check,
one of them case-sensitive. names_repository_metadata now refuses either name
at any depth and in any case, and every check uses it.

Secrets in URLs were refused when typed into hm add and set-config, but not in
configurations loaded or received through a clone, and a clone source could
store a ?token= query in .hm/.git/config. RemoteSpec.from_url now refuses
HTTP credentials, queries and fragments itself, and clone applies the same
rule to every URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A download stages SHA-1s when it ends, but it saved the catalogue it had
loaded when it began. An hm add saved by another command during a long
download was erased from the index and disk. A remote add likewise saved the
copy it took before a scan that can last minutes.

Both now save under an advisory lock on .hm. Download staging reads the
catalogue again and fills in only SHA-1 cells, so concurrent changes are
kept; any failure there is reported in staging_error, since the files are
already in place. An add whose catalogue changed during its scan refuses to
save and asks to be run again. Checkout resolves its target once and loads
that revision's state, instead of loading it twice and classifying a commit
id by the current state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Digests from a plain checksums.txt were catalogued with the algorithm
"unknown" and marked published, but downloads read "unknown" as no checksum,
so those files were never verified. Each supported algorithm has its own
digest length, so discovery now records the algorithm a digest's length
names, and downloads infer it for older rows. The algorithm list itself now
comes from one place.

A catalogue's single unnamed remote was "origin" when routing files but not
when selecting a remote, so hm download --remote origin failed and plans
reported no remote name. remote_name_of now names remotes for every caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A remote catalogue refused checkout with "Commit hallmark state changes"
even when nothing was staged: after hm restore --staged, .hm keeps the
unstaged change on disk, as git keeps the working copy, and committing could
not help. The refusal now names staged changes and how to commit or unstage
them, or names the changed .hm files and the git command that discards them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Downloads read responses through Requests' iter_content, which undoes any
Content-Encoding. A server that labels .gz files with Content-Encoding: gzip,
as Apache's AddEncoding does, delivered them decompressed, so their size and
checksums failed. Requests also advertises gzip, inviting servers to
compress text files on the fly.

Payload requests now ask for the identity encoding and store the response
body exactly as sent. Directory listings and other metadata are still
decoded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rwise

Index pages, checksum files and published catalogues were decoded three ways.
HTTP used the encoding Requests guesses, ISO-8859-1 for any text without a
declared charset, and replaced undecodable bytes; S3 also replaced them; SSH
required UTF-8. The same snapshot cloned over HTTPS got garbled non-ASCII
paths, and manifest names silently stopped matching their files.

decode_metadata now applies one rule on every backend: the charset the
server declares, otherwise UTF-8, and an error rather than replacement
characters. Reading an empty S3 metadata object no longer fails with
InvalidRange, and the loopback host list is defined once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
denniswu28 and others added 6 commits October 13, 2026 00:22
Every remote text, from config.yml to a directory listing, shared one fixed
16 MiB limit. A catalogue table grows by about 250 bytes per file, so a
published snapshot of around 70,000 files could not be cloned, and a WebDAV
folder of some 30,000 files could not be listed, while git clones, SFTP and
S3 listings had no such limit.

Documents that list files (listing pages, checksum manifests and catalogue
tables) now get a 1 GiB bound; config.yml and meta.yml keep 16 MiB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An S3 listing names an object's SHA checksum algorithm but not the digest, so
the backend sent a HEAD request for every listed object, one after another
and before filters applied: cataloguing one run of a 100,000-object prefix
cost 100,000 sequential requests, on every re-add.

Backends can now complete selected entries after filtering
(DataBackend.complete_entries). S3 sends those HEAD requests in parallel,
within its connection pool, and skips objects that already have an equally
strong published checksum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each command translated its own selection of exceptions. A typo in a
hand-edited config.yml, a full disk or a locked git index therefore showed
a Python traceback for some commands, including hm info. A command group
now reports every error a user can cause or fix in one line, for every
command and while opening the repository; anything else is a bug and keeps
its traceback.

Repo.download transfers only with approved=True, but Repo.clone downloaded
without asking unless given an approve callback. Clone now needs the same
approval, an approve callback or approved=True, and otherwise keeps the
catalogue without files, as when a download is declined. The CLI already
asks for approval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
history._load_revision_yaml and changes.filter_files_in_directory had no
callers, and history._parse_data_tsv was used only by its own tests, which
now check how Dothm.load_state reads data.tsv instead. "Used by" notes
named functions that no longer exist, and _bound_hosts took a name it never
used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Oct 19, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 99.52951% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
mod/hallmark/repo/dothm.py 93.4% 7 Missing and 2 partials ⚠️
mod/hallmark/transport/auth.py 98.6% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

denniswu28 and others added 13 commits October 19, 2026 23:04
Click 8.2 and later print "No such option '--force'." where 8.1 printed
"No such option: --force", so the exact match failed on every job
except Python 3.9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ilure

Rocky images lock root, and sshd with UsePAM no refuses a locked account
even for key logins, so every SSH test in those jobs failed to connect.
A '*' password field unlocks key logins without allowing passwords.
The client discards its stderr, so a failed SSH test now prints the
test server's log. Rocky 9 ships OpenSSH 9.9, not 8.7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wait() can return several finished transfers at once. When one of them
raised KeyboardInterrupt, the loop stopped before recording the others,
so files already in the worktree were left unstaged (seen on macOS CI).
Record the whole batch before raising, and after stopping the workers,
record the transfers that finished while they shut down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A source added again during a download can change a row without a
checksum: only its size or mtime moves. Staging compared checksums
alone, so it recorded the SHA-1 of the old bytes against the new row.
Compare the planned size and mtime too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n .hm/objects

When a file changed on the server and its source was added again, its
row lost the staged SHA-1. If the server published no checksum, the
next download took any link into the object store as current, so it kept
the stale file and staged its old SHA-1 against the new row. If it did
publish one, the download refused and asked the user to delete the file.

An existing file whose bytes are an unchanged stored object is now
outdated rather than a conflict: the plan lists it, and once approved
the new version is linked over it atomically. The old object stays, so
checking out an earlier commit places it again without a transfer. A row
with no digest cannot vouch for a link, so the file is fetched to
compare; identical content is only staged. This also re-fetches such
files after a failed save, a case nothing on disk can tell apart.

Files the user created or edited stay conflicts, nothing is replaced
unless the approved plan listed it, a file edited during the transfer is
kept, and a file that two paths differing only in letter case share is
never replaced for either of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adding a source again already updated changed rows and dropped their
staged SHA-1, but said nothing about it, so a user could not tell that
downloaded files were now outdated. hm add now lists downloaded files
whose size, modification time or checksum the server reports changed,
and points to hm download to replace them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two sources sometimes catalog the same path. It may be one file on two
mirrors, or two different files that share a name, and the refusal did
not say which. Each clashing path is now labelled same content,
different content, or cannot compare, from the checksums and recorded
SHA-1 the rows share, their sizes, or the fact that overlapping patterns
in one folder name the same server file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A path two sources share may be one file on two mirrors, which needs
one row, or two different files with one name, which need both. hm add
refused either way and only suggested --prefix. At a terminal it now
lists the paths with how their content compares and asks whether to
skip them, keeping the catalogued rows, keep both under a folder, or
abort. Elsewhere --skip-duplicates skips them. In Python, Repo.add takes
on_duplicate: "skip" or a callable that receives the clashes.

The choice is not saved. Skipped paths never become the new source's
rows, so adding it again never reports them removed; it meets the same
clashes and asks again. File and folder clashes are still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
M87.fits and m87.fits are two catalogue rows but one file on macOS and
Windows, so only one of them can be downloaded there. hm add now warns
about such paths, whether they come from one listing or from different
sources. It does not refuse them, since they are fine on Linux, and the
download check already keeps either file from replacing the other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cover a replacement whose temporary name is taken, digests saved without
their algorithm, long clash listings, and worktree checks for bare
repositories and paths outside the worktree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With root unlocked, the Rocky jobs reached two last failures. Server-side
hashing runs xargs, which the rockylinux:8 image lacks, so findutils is
installed. Root reads a file whatever its permissions, so the test that
a server's unreadable file fails to download cannot run there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in origin/codex/function-atomic-replay-20260928: repeated --fmt
with one TSV per format, file: entries for files no format matches,
re-sorting when a format joins, the multi-format matcher and hm detect.

Sam's storage is adopted. Formats belong to the whole catalogue, in
config.yml's data list, and sort the files of every source; a source is
now only a selection (remote, folder, prefix, filters). Everything that
read data.tsv alone now reads the catalogue's rows in every TSV and its
file: entries: re-adding a source replaces them, clashes and changed
files are found across them, downloads select and route them, SHA-1s
are staged into them, and status and restore --staged cover them. The
state loads and saves every TSV its configuration names.

The column naming a file's source is now catalog_source, because
hm detect names a field "source" for objects such as M87, and formats
may use it. file: entries name their source only when there are several.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rt every source

Re-adding a source silently removed catalogued files the server no
longer lists, which a user may not expect. hm add now lists them and
asks whether to remove them or keep their rows; --prune and
--keep-missing answer in advance, and Python passes prune=True or
False. Without a choice, Repo.add refuses, as it does for resort.

When a new format joins, the resort question also offers "all" when
other sources have sorted files: their files are sorted again by their
recorded paths, without listing their servers. A resort callable gets
the number of other sources' sorted files as a second argument only
when there are any, so one-argument callables keep working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@denniswu28 denniswu28 changed the title Feat/private data backend Catalogue and download private data from SSH, HTTPS, WebDAV and S3 servers Oct 21, 2026
denniswu28 and others added 6 commits October 20, 2026 22:52
_validate_folder meant "." to name the root, but it validated the path
first, and the path check rejects "." as empty, so --prefix ./ failed
with "Remote path cannot be empty". "/" is still refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
restore --staged . cleared the entries of one index object and wrote
another: each access to Dothm.index reads the index afresh, so nothing
was unstaged before the first commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
glob_search filled in the format together with the base path, so braces
in a folder name such as run_{id} became wildcards and hm add PATTERN
picked up a sibling folder's files. Only the format is filled in now,
and the base path is escaped for glob.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Error paths and less common branches: interrupted and racing downloads,
SFTP replies and failures, SSH time limits, auth profile validation,
HTTP and WebDAV responses, S3 listings and errors, CLI reports and
prompts, path and checksum helpers, and format detection merges. SFTP
and SSH cases run against local pipes and child processes, not sshd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The S3 tests run only in the job that installs boto3 and moto, and that
job uploaded no coverage, so the S3 backend counted as untested. It now
uploads its data, the coverage job waits for it, and it also runs the
new S3 edge-case tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@denniswu28
denniswu28 merged commit 4af4f20 into codex/function-atomic-replay-20260928 Oct 21, 2026
19 checks passed
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.

2 participants