Skip to content

Release 0.4.0: catch-up pull, root confinement, typed outcomes - #15

Merged
rmorgans merged 7 commits into
mainfrom
release/0.4.0
Oct 1, 2026
Merged

rmorgans merged 7 commits into
mainfrom
release/0.4.0

Conversation

@rmorgans

@rmorgans rmorgans commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Folder-mode changes asked for by its first consumer, asset-store's dataset backup. CHANGELOG has the details.

  • Catch-up pull: Overwrite::IfUnchanged (pull --if-unchanged) replaces only files that still match the .dvc's base. It also removes base files that are unchanged locally and that the new version dropped. Every file is re-hashed just before it is replaced or removed. Any other difference is a PullConflict, and there is never a fallback to Force.
  • Confinement: PushOptions::root / PullOptions::root refuse a symlink or reparse point anywhere from the root down to the output, at the output itself, and at its .dvc (Refusal::SymlinkedComponent). They also refuse a path that is not plain names (Refusal::OutsideRoot).
  • Forks: PushReport::outcome: Pushed::{AlreadyLatest, Published, Forked}, and the report is #[must_use]. The CLI exits non-zero after a fork.
  • Typed errors: CredentialsMissing, RemoteUnusable, TooManyHeads, NoHead. Messages no longer name CLI flags; the CLI adds a hint: line instead.

0.4.0 rather than 0.3.1 because of the breaking changes: Pushed replaces history_record/forked_with, Overwrite, PushReport and PullReport are now #[non_exhaustive], and both option structs gain a root field.

Tests cover catch-up, a refused local change (including a file the new version removed), a file written mid-pull, symlinks at every level on unix and a junction on Windows, and each new typed error. Locally: the full suite with --include-ignored (pinned DVC 3.67.1, rclone), clippy, the feature matrix, deny, and llvm-cov at 86.9% of lines.

- Overwrite::IfUnchanged (pull --if-unchanged) replaces or removes only
  files still as the .dvc's base records them, re-hashing each just
  before; any other differing file is PullConflict. Never falls back to
  Force.
- PushOptions::root / PullOptions::root: paths relative to a root, plain
  names only, no symlink or reparse point from the root to the output,
  at the output or at its .dvc (Refusal::OutsideRoot,
  SymlinkedComponent).
- PushReport::outcome: Pushed::{AlreadyLatest, Published, Forked}
  replaces history_record/forked_with; PushReport is #[must_use]. The
  CLI exits non-zero after a fork.
- folder::Error::{CredentialsMissing, RemoteUnusable, TooManyHeads,
  NoHead} replace untyped errors. Messages name no CLI flag; the CLI adds
  a hint line.
IfUnchanged replaced or removed a base file once its manifest was on the
remote, but objects can be gone behind a manifest. Each file it would
discard must now have its object on the remote at its local size, else
Refusal::BaseNotOnRemote, before anything is written.
From the Fable review of #15:
- A base file whose name matches a target's but for case or Unicode
  normalization is the target itself on macOS and Windows; removing it
  removed the pulled file. Refused on every OS as CaseCollision.
- A base file deleted locally that the version still has was written
  again, undoing the deletion; it is now PullConflict. Pulling the very
  version the output is based on is a restore and behaves as Refuse.
- CredentialsMissing names no env vars (Static reaches it too); the CLI
  adds that hint.
- Document that Windows confinement refuses every reparse point,
  placeholders and compressed files included.
@rmorgans
rmorgans merged commit 051a234 into main Oct 1, 2026
2 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.

1 participant