Skip to content

Skip unchanged pulls only when source hashes prove equality

Date: 2026-08-12

forest pull used to list a unit and download every remote file even when a previous verified pull left an identical clean local copy. Large unchanged S3 units therefore paid the full transfer cost on every invocation, while push already skipped unchanged source content via ADR 0017.

Before transferring a pull target, forest now requests the backend's strongest available SHA-256, SHA-1, or MD5 digest for each source file. It derives a stable fingerprint from the sorted path, size, algorithm, and digest tuples and compares it with the current local files. A pull skips only when all of the following are true:

  • the newest sync entry has the same remote path and fully covers the file set;
  • every target exists locally and still matches that entry's recorded SHA-256;
  • the live remote and local fingerprints match; and
  • --force was not requested.

The dirty-local guard runs before this decision. A dirty target still refuses, an unchanged --dry-run prints the same SKIP, and --force preserves its existing overwrite behavior by transferring. File-mode units use an exact remote stat so selecting several files does not repeatedly hash the whole stage. Directory and subdirectory units hash-list only their unit prefix.

The hash-bearing listing and decision run inside the checkout lock from ADR 0020, serializing them with other forest transfers. Ordinary discovery, forest ls, status, and push lease listings remain hash-free.

Consequences

  • Same-size changes and file additions, removals, renames, and remote-path remaps transfer instead of falsely skipping.
  • A backend or file without a usable content hash transfers conservatively; size or modification time alone never authorizes a skip.
  • No sync-state migration or new flag is needed. The live fingerprint is compared with the clean local evidence already stored in each sync entry.
  • Unchanged pulls read source hashes and local content but download zero files and do not rewrite sync state.