Add CardDAV cache and read path (ADR 0064 pieces 3 and 4), rebased #776

Open
kayg wants to merge 1 commit from issue/carddav-read-r2 into dev
Owner

Round 2 shared epoch fix implemented, FINAL review pending

Head 113af4f8. Status table, exact gates and limits. Held-reader tokens are validated against committed epochs; mid-read replacement fails closed. Updated #777 is re-merged into #794 at 21e678b2, pushed after green gates. Never merged into dev.

Earlier implementation records below are historical.

Round 1 fixes implemented, review pending

Head f3d6361d: bounded wire-history retention and RFC 6578 token expiry. Plan and bounds; status table, exact gates and open risks. All scoped VM gates passed. Never merged into dev.

The original implementation record below is historical; its unbounded-retention/rebuild gaps are superseded by this round.

Replacement for #734 (same change, rebased onto current dev as one commit; Forgejo marked #734 unmergeable). Originals are untouched. Refs #657, ADR 0064, D0 in contacts-0064-plan.

Contents: crates/carddav (cache in users/<uuid>/carddav.db, journal projection, read-only DAV adapter, unmounted), contacts-actions projection, contacts wire index. See #734 for the full description.

Merge order: this PR first, then the perf follow-up.

Gates (cua@calternal-dev-02-std, aarch64, Nix dev shell, via slot, -j4)

  • cargo fmt --all -- --check: clean
  • cargo clippy -j4 -p calternal-carddav -p calternal-contacts -p calternal-contacts-actions --all-targets -- -D warnings: clean
  • cargo test -j4 -p calternal-carddav -p calternal-contacts -p calternal-contacts-actions: all pass (carddav 7 cache + 2 journal + 10 read + 1 recovery = 20; contacts suites incl. wire_index, hostile, break_*; contacts-actions 130)
  • Run on head 49ccd9a3. No conflicts and no test breakage from the rebase.
  • Not re-run here: perf examples on a perf VM (see gaps).

Review against ADR 0064 (independent; not the author)

Matches: bridge never writes files (read-only methods, writes get 405); vCard 3.0 precomputed with strong ETags, reads never run the converter; stable user-id URLs and /.well-known/carddav redirect; paged sync with fixed snapshot, continuation tokens and 507 marker (RFC 6578 3.6); deletes reported as 404; expired/foreign tokens give valid-sync-token; rebuild is atomic and rotates the epoch; routes unmounted and an authenticator is required; depth infinity refused.

Gaps and risks (none block landing; D4 and later should pick them up):

  1. No macOS 27 replay. Piece 4 gate asks for replayed Apple traffic; only RFC fixtures exist. D5 recorder covers this.
  2. Perf gates are still hand-run examples, not a CI gate on the perf VM (ADR "Speed is a gate", 0005). Numbers in #739 were measured on the older base and not re-measured after this rebase.
  3. revisions is append-only and never pruned. Each edit adds a full card row; disk grows with edit count and only a rebuild compacts it. Needs a compaction rule (rebuild also rotates the epoch, so devices resync).
  4. changed_ids maps a fixed list of journal kinds. A new journal kind that touches a contact (for example Trash or tier moves) would be missed until a rebuild. Needs a test that enumerates note-affecting kinds, or a fallback rebuild on unknown kinds. Companion cards (FO1/D2) are not covered yet.
  5. PROPFIND depth 1 loads all summaries (O(n), not O(p)) and multiget has no href count cap beyond the 1 MiB body. Fine at 1,000 cards; ADR 0067 reply cap of 1,000 resources is not enforced.
  6. Cold rebuild stages the whole book in memory (O(n)); stated in #739, acceptable for 20k-card import but unbounded for photo books.
  7. Blocking SQLite work runs under an async mutex in the request path; one slow rebuild stalls that user's DAV reads (not other users).
  8. Missing in the read surface by design (later pieces): Lists/group cards, photos, writes, aliases for phone-chosen hrefs (hrefs are <id>.vcf only; D6 must add the alias table), If-Match/412.
  9. Cache tests use fake time-free fixtures only; no kill-9 test of a process dying mid-rebuild (backup install is atomic by design, covered by the rollback test, not by a real crash).
## Round 2 shared epoch fix implemented, FINAL review pending Head **113af4f8**. [Status table, exact gates and limits](https://git.kayg.org/kayg/calternal/pulls/776#issuecomment-71333). Held-reader tokens are validated against committed epochs; mid-read replacement fails closed. Updated #777 is re-merged into #794 at **21e678b2**, pushed after green gates. Never merged into dev. Earlier implementation records below are historical. ## Round 1 fixes implemented, review pending Head **f3d6361d**: bounded wire-history retention and RFC 6578 token expiry. [Plan and bounds](https://git.kayg.org/kayg/calternal/pulls/776#issuecomment-71171); [status table, exact gates and open risks](https://git.kayg.org/kayg/calternal/pulls/776#issuecomment-71194). All scoped VM gates passed. Never merged into dev. The original implementation record below is historical; its unbounded-retention/rebuild gaps are superseded by this round. Replacement for #734 (same change, rebased onto current dev as one commit; Forgejo marked #734 unmergeable). Originals are untouched. Refs #657, ADR 0064, D0 in contacts-0064-plan. Contents: `crates/carddav` (cache in `users/<uuid>/carddav.db`, journal projection, read-only DAV adapter, unmounted), `contacts-actions` projection, `contacts` wire index. See #734 for the full description. Merge order: this PR first, then the perf follow-up. ## Gates (cua@calternal-dev-02-std, aarch64, Nix dev shell, via slot, -j4) - `cargo fmt --all -- --check`: clean - `cargo clippy -j4 -p calternal-carddav -p calternal-contacts -p calternal-contacts-actions --all-targets -- -D warnings`: clean - `cargo test -j4 -p calternal-carddav -p calternal-contacts -p calternal-contacts-actions`: all pass (carddav 7 cache + 2 journal + 10 read + 1 recovery = 20; contacts suites incl. wire_index, hostile, break_*; contacts-actions 130) - Run on head 49ccd9a3. No conflicts and no test breakage from the rebase. - Not re-run here: perf examples on a perf VM (see gaps). ## Review against ADR 0064 (independent; not the author) Matches: bridge never writes files (read-only methods, writes get 405); vCard 3.0 precomputed with strong ETags, reads never run the converter; stable user-id URLs and `/.well-known/carddav` redirect; paged sync with fixed snapshot, continuation tokens and 507 marker (RFC 6578 3.6); deletes reported as 404; expired/foreign tokens give `valid-sync-token`; rebuild is atomic and rotates the epoch; routes unmounted and an authenticator is required; depth infinity refused. Gaps and risks (none block landing; D4 and later should pick them up): 1. No macOS 27 replay. Piece 4 gate asks for replayed Apple traffic; only RFC fixtures exist. D5 recorder covers this. 2. Perf gates are still hand-run examples, not a CI gate on the perf VM (ADR "Speed is a gate", 0005). Numbers in #739 were measured on the older base and not re-measured after this rebase. 3. `revisions` is append-only and never pruned. Each edit adds a full card row; disk grows with edit count and only a rebuild compacts it. Needs a compaction rule (rebuild also rotates the epoch, so devices resync). 4. `changed_ids` maps a fixed list of journal kinds. A new journal kind that touches a contact (for example Trash or tier moves) would be missed until a rebuild. Needs a test that enumerates note-affecting kinds, or a fallback rebuild on unknown kinds. Companion cards (FO1/D2) are not covered yet. 5. PROPFIND depth 1 loads all summaries (O(n), not O(p)) and multiget has no href count cap beyond the 1 MiB body. Fine at 1,000 cards; ADR 0067 reply cap of 1,000 resources is not enforced. 6. Cold rebuild stages the whole book in memory (O(n)); stated in #739, acceptable for 20k-card import but unbounded for photo books. 7. Blocking SQLite work runs under an async mutex in the request path; one slow rebuild stalls that user's DAV reads (not other users). 8. Missing in the read surface by design (later pieces): Lists/group cards, photos, writes, aliases for phone-chosen hrefs (hrefs are `<id>.vcf` only; D6 must add the alias table), `If-Match`/412. 9. Cache tests use fake time-free fixtures only; no kill-9 test of a process dying mid-`rebuild` (backup install is atomic by design, covered by the rollback test, not by a real crash).
Add journal-backed CardDAV cache and read bridge
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
49ccd9a338
Author
Owner

Independent review, round 1

Reviewed head 49ccd9a338edaf0356f9619579316b4a6446a129, ADRs 0063/0064, the PR bodies and the earlier #734/#739 discussions. Scope: cache/read correctness and data safety; admission, transport caps and auth are assigned to #794.

Findings

  • P2: revision payload retention is unbounded. crates/carddav/src/cache.rs:172 inserts a complete wire card and property index on every byte-changing edit. Only rebuild deletes history (:191); source::refresh does not schedule a rebuild for an otherwise healthy, same-generation journal. A book with photos therefore retains another full photo for each ordinary text edit, including after the contact is deleted forever. For scale, 1,000 cards with 224 KiB photos and 52 edits add about 11 GiB of photo copies alone. This disposable per-user SSD cache has neither retention nor a byte budget, so normal long-running usage can exhaust shared state storage. Fix: define bounded cache-history retention and token expiry. Either retain the revisions required by a bounded snapshot/delta window and reject older tokens, or atomically compact and rotate the epoch at a size threshold. Do not simply delete revisions while still accepting their tokens. Add an edit-churn test that checks bounded storage, paged snapshots, tombstones and rejection of expired tokens.

Checks and limits

  • changed_ids (crates/contacts-actions/src/projection.rs:17) silently treats unknown kinds as irrelevant. This is a forward-compatibility risk, but I did not establish a currently missing Trash/restore kind: ordinary Trash emits notes.deleted, restore emits notes.created, and both are covered. Add an exhaustive producer/consumer contract or a conservative rescan for unknown contact/note mutations before extending the event vocabulary.
  • Wire ETags hash the actual stored bytes, no-op projections preserve them, and rebuild rotates collection tokens. The source captures its rebuild cursor before scanning and retries failed conversions without committing that cursor. Production cache paths and source calls use the stable user ID; I found no concrete cross-user cache leak.
  • Rebuild puts rows, cursor and epoch in one SQLite transaction. Existing rollback tests support failure atomicity; an actual process-kill/reopen test mid-rebuild is still missing. Do not describe rollback injection as a kill test.
  • Static review only. No build/test/install ran on the control host. Read-only whitespace check: git diff --check 49ccd9a3^ 49ccd9a3 passed. Existing reported VM gates were read, not independently rerun.

Verdict: fix first. No P0/P1 finding in this PR; bound the disposable revision history before BM4.

## Independent review, round 1 Reviewed head `49ccd9a338edaf0356f9619579316b4a6446a129`, ADRs 0063/0064, the PR bodies and the earlier #734/#739 discussions. Scope: cache/read correctness and data safety; admission, transport caps and auth are assigned to #794. ### Findings - **P2: revision payload retention is unbounded.** `crates/carddav/src/cache.rs:172` inserts a complete wire card and property index on every byte-changing edit. Only `rebuild` deletes history (`:191`); `source::refresh` does not schedule a rebuild for an otherwise healthy, same-generation journal. A book with photos therefore retains another full photo for each ordinary text edit, including after the contact is deleted forever. For scale, 1,000 cards with 224 KiB photos and 52 edits add about 11 GiB of photo copies alone. This disposable per-user SSD cache has neither retention nor a byte budget, so normal long-running usage can exhaust shared state storage. **Fix:** define bounded cache-history retention and token expiry. Either retain the revisions required by a bounded snapshot/delta window and reject older tokens, or atomically compact and rotate the epoch at a size threshold. Do not simply delete revisions while still accepting their tokens. Add an edit-churn test that checks bounded storage, paged snapshots, tombstones and rejection of expired tokens. ### Checks and limits - `changed_ids` (`crates/contacts-actions/src/projection.rs:17`) silently treats unknown kinds as irrelevant. This is a forward-compatibility risk, but I did **not** establish a currently missing Trash/restore kind: ordinary Trash emits `notes.deleted`, restore emits `notes.created`, and both are covered. Add an exhaustive producer/consumer contract or a conservative rescan for unknown contact/note mutations before extending the event vocabulary. - Wire ETags hash the actual stored bytes, no-op projections preserve them, and rebuild rotates collection tokens. The source captures its rebuild cursor before scanning and retries failed conversions without committing that cursor. Production cache paths and source calls use the stable user ID; I found no concrete cross-user cache leak. - Rebuild puts rows, cursor and epoch in one SQLite transaction. Existing rollback tests support failure atomicity; an actual process-kill/reopen test mid-rebuild is still missing. Do not describe rollback injection as a kill test. - Static review only. No build/test/install ran on the control host. Read-only whitespace check: `git diff --check 49ccd9a3^ 49ccd9a3` passed. Existing reported VM gates were read, not independently rerun. **Verdict: fix first.** No P0/P1 finding in this PR; bound the disposable revision history before BM4.
Author
Owner

round 1 implementation plan (no dev merge):

  • #776: default 1,024 wire-changing revisions of delta/snapshot history, configurable through Cache::open_with_retention. advance a durable floor in the same transaction as cursor and pruning. keep one predecessor per href at the floor plus newer revisions, so accepted paged snapshots remain coherent. expire older snapshot high-water marks and delta positions with RFC 6578 valid-sync-token; an empty token starts a full resync. no-op projection does not consume the window. disk pages are reused.
  • #777: disk-backed staging on the same state tier, bounded SQLite page caches, incremental card batches and paged source IDs. install the finished image atomically through SQLite backup, rotate epoch only after success. O(batch bytes + SQLite cache) additional memory, O(book bytes) staging disk; retention O(live cards + window) payloads, prune indexed by href/seq.
failure effect detection recovery gate
expired delta/snapshot no partial or false continuity durable floor 403 valid-sync-token, empty-token full resync churn, paged edits/deletes, reopen and HTTP tests
conversion/staging error old cache/cursor/epoch stay intact source/SQLite error discard staging, retry failure at every batch
process killed mid-build/install old or complete new generation reopen SQLite rollback/retry real killed child at batch boundaries and backup contention
large/concurrent photo books bounded process memory isolated RSS limit staging stays on disk 50k synthetic cards and concurrent photo rebuilds

then push #776 and #777 in order, merge updated #777 into #794, rerun its specified gates and push only if green. #797 remains a downstream consumer; its independent write fixes are outside this task. cheap P2s will be fixed where established; remaining delivery gaps will be listed. review round 1 of maximum 3.

round 1 implementation plan (no dev merge): - #776: default 1,024 wire-changing revisions of delta/snapshot history, configurable through Cache::open_with_retention. advance a durable floor in the same transaction as cursor and pruning. keep one predecessor per href at the floor plus newer revisions, so accepted paged snapshots remain coherent. expire older snapshot high-water marks and delta positions with RFC 6578 valid-sync-token; an empty token starts a full resync. no-op projection does not consume the window. disk pages are reused. - #777: disk-backed staging on the same state tier, bounded SQLite page caches, incremental card batches and paged source IDs. install the finished image atomically through SQLite backup, rotate epoch only after success. O(batch bytes + SQLite cache) additional memory, O(book bytes) staging disk; retention O(live cards + window) payloads, prune indexed by href/seq. | failure | effect | detection | recovery | gate | |---|---|---|---|---| | expired delta/snapshot | no partial or false continuity | durable floor | 403 valid-sync-token, empty-token full resync | churn, paged edits/deletes, reopen and HTTP tests | | conversion/staging error | old cache/cursor/epoch stay intact | source/SQLite error | discard staging, retry | failure at every batch | | process killed mid-build/install | old or complete new generation | reopen SQLite | rollback/retry | real killed child at batch boundaries and backup contention | | large/concurrent photo books | bounded process memory | isolated RSS limit | staging stays on disk | 50k synthetic cards and concurrent photo rebuilds | then push #776 and #777 in order, merge updated #777 into #794, rerun its specified gates and push only if green. #797 remains a downstream consumer; its independent write fixes are outside this task. cheap P2s will be fixed where established; remaining delivery gaps will be listed. review round 1 of maximum 3.
Author
Owner

round 1 implementation status at f3d6361d. pushed, never merged into dev. review remains pending.

finding / gate status evidence
unbounded revisions fixed configurable Retention::max_revisions, default 1,024 wire-changing revisions; durable floor and pruning commit with journal cursor
expired delta / initial snapshot fixed rejected with RFC 6578 valid-sync-token; HTTP regression checks 403 then successful empty-token full resync
accepted paged snapshot, tombstones pass boundary snapshot retains predecessor bytes across updates/deletes; aged deletion payloads and tombstones removed
no-op / zero window / reopen pass no-op preserves head; deleting last contact cannot reset head; expired tokens remain expired after reopen or policy growth
photo edit disk growth pass 100 edits of a 224 KiB synthetic inline photo; checkpointed database stays within 512 KiB of warm size
package tests pass carddav 21 tests; contacts and all 130 contacts-actions tests passed
clippy / fmt pass exact commands below

VM: cua@calternal-dev-02-std, /home/cua/Developer/Worktrees/carddav-retention-r1, Nix shell through ~/bin/slot. logs ~/carddav-retention-r1-green.log and ~/carddav-retention-r1-space.log, both exit 0. the new churn test failed on the original head (exit 101, old delta still accepted), then passed with the fix.

cargo test -j4 -p calternal-carddav -p calternal-contacts -p calternal-contacts-actions --locked --no-fail-fast
cargo clippy -j4 -p calternal-carddav -p calternal-contacts -p calternal-contacts-actions --all-targets --locked -- -D warnings
cargo test -j4 -p calternal-carddav --locked
cargo clippy -j4 -p calternal-carddav --all-targets --locked -- -D warnings
cargo fmt --all -- --check

Decision: count-based disposable wire-cache retention, configured by Cache::open_with_retention; no change to authoritative contact history. one baseline per href plus the recent window supports snapshots. growing the policy cannot resurrect discarded history. freed SQLite pages are reused, so this prevents further edit-driven growth rather than shrinking an already oversized historical file. the existing O(n) rebuild and source inventory are addressed in stacked #777. native Apple replay, perf-VM latency gates, groups/photos delivery and an exhaustive future journal-vocabulary contract remain separate work; unknown kinds currently have no demonstrated missing producer. max 3 independent review rounds; this is the round 1 implementation, not a new review.

round 1 implementation status at **f3d6361d**. pushed, never merged into dev. review remains pending. | finding / gate | status | evidence | |---|---|---| | unbounded revisions | fixed | configurable `Retention::max_revisions`, default 1,024 wire-changing revisions; durable floor and pruning commit with journal cursor | | expired delta / initial snapshot | fixed | rejected with RFC 6578 `valid-sync-token`; HTTP regression checks 403 then successful empty-token full resync | | accepted paged snapshot, tombstones | pass | boundary snapshot retains predecessor bytes across updates/deletes; aged deletion payloads and tombstones removed | | no-op / zero window / reopen | pass | no-op preserves head; deleting last contact cannot reset head; expired tokens remain expired after reopen or policy growth | | photo edit disk growth | pass | 100 edits of a 224 KiB synthetic inline photo; checkpointed database stays within 512 KiB of warm size | | package tests | pass | carddav 21 tests; contacts and all 130 contacts-actions tests passed | | clippy / fmt | pass | exact commands below | VM: `cua@calternal-dev-02-std`, `/home/cua/Developer/Worktrees/carddav-retention-r1`, Nix shell through `~/bin/slot`. logs `~/carddav-retention-r1-green.log` and `~/carddav-retention-r1-space.log`, both exit 0. the new churn test failed on the original head (exit 101, old delta still accepted), then passed with the fix. ```sh cargo test -j4 -p calternal-carddav -p calternal-contacts -p calternal-contacts-actions --locked --no-fail-fast cargo clippy -j4 -p calternal-carddav -p calternal-contacts -p calternal-contacts-actions --all-targets --locked -- -D warnings cargo test -j4 -p calternal-carddav --locked cargo clippy -j4 -p calternal-carddav --all-targets --locked -- -D warnings cargo fmt --all -- --check ``` Decision: count-based disposable wire-cache retention, configured by `Cache::open_with_retention`; no change to authoritative contact history. one baseline per href plus the recent window supports snapshots. growing the policy cannot resurrect discarded history. freed SQLite pages are reused, so this prevents further edit-driven growth rather than shrinking an already oversized historical file. the existing O(n) rebuild and source inventory are addressed in stacked #777. native Apple replay, perf-VM latency gates, groups/photos delivery and an exhaustive future journal-vocabulary contract remain separate work; unknown kinds currently have no demonstrated missing producer. max 3 independent review rounds; this is the round 1 implementation, not a new review.
Author
Owner

Independent review, round 2

Reviewed f3d6361d5c, round-1 finding #issuecomment-71157 and implementation #issuecomment-71194; also traced the inherited code through #777 and the #794 merge 80ec1fa3.

round-1 finding status does its test prove it?
P2: unbounded full-card revision retention verified fixed Yes, for edit-driven growth: configurable count window, durable monotonic floor, one predecessor per href, and atomic pruning/cursor commit. Churn checks expiration and bounded row count; the 224 KiB photo test checks checkpointed disk reuse. This bounds history relative to live book size, not all cache bytes.
expiry must preserve usable snapshots and deletes verified Boundary snapshot test preserves predecessor bytes and a recent tombstone; later churn removes the retired tombstone/payload. Reopen/policy growth cannot resurrect discarded tokens. HTTP test asserts 403 valid-sync-token followed by successful empty-token resync. These tests do not cover a reader held across another connection's rebuild: R2-1 below.

new finding: R2-1, P2: an already-open cache reader accepts a replaced epoch

Locations: sync.rs:19, cache.rs:247. epoch is cached when a handle opens; rebuild changes the database epoch and only the rebuilding handle's field. Another handle sees new rows/head with its old epoch.

Reproduction: seed a.vcf, open a second Cache and obtain its token, then rebuild through the writer with only b.vcf. The writer correctly rejects the old token. The held reader accepts it without reporting the disappearance of a.vcf; its subsequent tokens still use the obsolete epoch. In the #777/#794 replacement, sequence numbering restarts at 1, so this same one-card replacement returns an empty, complete delta under the old token: a silent missed replacement.

Fix: validate and issue tokens against the durable epoch, head, floor and rows from one SQLite read snapshot, including token/ctag; or enforce a single-handle invariant at the storage boundary. Add a reader-held-across-successful-rebuild regression that requires invalid-token/full-resync. Same-handle rebuild tests cannot prove this property.

Severity is P2, not P1: the production adapter shares one locked Book per user, so this demonstrates the public multi-handle cache path, not a reproduced failure in the mounted single-Book server. The cache's independent-reader behavior is already exercised in the stacked WAL-read regression. This is a newly identified pre-existing gap, not evidence that the retention fix regressed.

other checks

  • Just-inside-window tokens and paged deletes: independent probe passes on the merged tree.
  • No clock-based expiry exists. SQLite AUTOINCREMENT at i64::MAX fails the next write closed; independent probe confirms unchanged cursor/token and no partial row.
  • #794 admission/limits: no merge regression found. Diff from its pre-merge head 26edc647 leaves crates/dav, adapter, properties, transport/network tests unchanged. Sync's existing aggregate payload/index byte guard remains in addition to retention.
  • Existing rebuild transaction and rollback checks do not prove process-kill/power-loss behavior during installation; #777's batch-kill evidence is scoped separately.

verification and limits

Read-only diffs/whitespace checks passed for all three updates. Hash comparison confirms 421/421 Rust/Cargo files in the existing dev-02 retention worktree match f3d6361d, and 445/445 in the tested dev-01 merged worktree match 80ec1fa3. The independent runtime evidence in this round is from the merged tree; the #776-only run remained queued behind occupied dev-02 slots and was cancelled before starting. The base finding above is also visible directly in f3d6361d's unchanged cached-epoch logic. Existing #776 gate reports were inspected, not represented as fresh passes.

On dev-01, in nix develop -c bash -c through ~/bin/slot:

cargo test -j4 -p calternal-carddav --test reviewer_round2 --test cache --test rebuild_crash --test rebuild_memory --locked --no-fail-fast -- --nocapture

All 13 committed cache, 2 crash-test functions and the memory parent test pass. Independent window/deletion and sequence-exhaustion probes pass; the held-reader probe fails as expected for R2-1. An initial file-size-limit probe hit SQLITE_IOERR rather than FULL; a separate stage-only ENOSPC fault-injection rerun passes cleanup/retry. Logs: ~/review-carddav-r2b.log and ~/review-carddav-r2-enospc-b.log. Untracked review probes live only on VMs; no tracked source edits.

verdict: fix first, for R2-1. New findings: P0 0, P1 0, P2 1, P3 0. No code pushed, merge or production access.

## Independent review, round 2 Reviewed **f3d6361d5cfa636affa43e3032e312369b1322b0**, round-1 finding #issuecomment-71157 and implementation #issuecomment-71194; also traced the inherited code through #777 and the #794 merge **80ec1fa3**. | round-1 finding | status | does its test prove it? | |---|---|---| | P2: unbounded full-card revision retention | **verified fixed** | Yes, for edit-driven growth: configurable count window, durable monotonic floor, one predecessor per href, and atomic pruning/cursor commit. Churn checks expiration and bounded row count; the 224 KiB photo test checks checkpointed disk reuse. This bounds history relative to live book size, not all cache bytes. | | expiry must preserve usable snapshots and deletes | **verified** | Boundary snapshot test preserves predecessor bytes and a recent tombstone; later churn removes the retired tombstone/payload. Reopen/policy growth cannot resurrect discarded tokens. HTTP test asserts 403 `valid-sync-token` followed by successful empty-token resync. These tests do **not** cover a reader held across another connection's rebuild: R2-1 below. | ### new finding: R2-1, P2: an already-open cache reader accepts a replaced epoch Locations: [sync.rs:19](https://git.kayg.org/kayg/calternal/src/commit/f3d6361d/crates/carddav/src/cache/sync.rs#L19), [cache.rs:247](https://git.kayg.org/kayg/calternal/src/commit/f3d6361d/crates/carddav/src/cache.rs#L247). `epoch` is cached when a handle opens; rebuild changes the database epoch and only the rebuilding handle's field. Another handle sees new rows/head with its old epoch. Reproduction: seed `a.vcf`, open a second `Cache` and obtain its token, then rebuild through the writer with only `b.vcf`. The writer correctly rejects the old token. The held reader accepts it without reporting the disappearance of `a.vcf`; its subsequent tokens still use the obsolete epoch. In the #777/#794 replacement, sequence numbering restarts at 1, so this same one-card replacement returns an empty, complete delta under the old token: a silent missed replacement. Fix: validate and issue tokens against the durable epoch, head, floor and rows from one SQLite read snapshot, including `token`/`ctag`; or enforce a single-handle invariant at the storage boundary. Add a reader-held-across-successful-rebuild regression that requires invalid-token/full-resync. Same-handle rebuild tests cannot prove this property. Severity is **P2**, not P1: the production adapter shares one locked Book per user, so this demonstrates the public multi-handle cache path, not a reproduced failure in the mounted single-Book server. The cache's independent-reader behavior is already exercised in the stacked WAL-read regression. This is a newly identified pre-existing gap, not evidence that the retention fix regressed. ### other checks - Just-inside-window tokens and paged deletes: independent probe passes on the merged tree. - No clock-based expiry exists. SQLite AUTOINCREMENT at `i64::MAX` fails the next write closed; independent probe confirms unchanged cursor/token and no partial row. - #794 admission/limits: no merge regression found. Diff from its pre-merge head **26edc647** leaves `crates/dav`, adapter, properties, transport/network tests unchanged. Sync's existing aggregate payload/index byte guard remains in addition to retention. - Existing rebuild transaction and rollback checks do not prove process-kill/power-loss behavior during installation; #777's batch-kill evidence is scoped separately. ### verification and limits Read-only diffs/whitespace checks passed for all three updates. Hash comparison confirms **421/421 Rust/Cargo files** in the existing dev-02 retention worktree match f3d6361d, and **445/445** in the tested dev-01 merged worktree match 80ec1fa3. The independent runtime evidence in this round is from the merged tree; the #776-only run remained queued behind occupied dev-02 slots and was cancelled before starting. The base finding above is also visible directly in f3d6361d's unchanged cached-epoch logic. Existing #776 gate reports were inspected, not represented as fresh passes. On dev-01, in `nix develop -c bash -c` through `~/bin/slot`: ```sh cargo test -j4 -p calternal-carddav --test reviewer_round2 --test cache --test rebuild_crash --test rebuild_memory --locked --no-fail-fast -- --nocapture ``` All 13 committed cache, 2 crash-test functions and the memory parent test pass. Independent window/deletion and sequence-exhaustion probes pass; the held-reader probe **fails as expected for R2-1**. An initial file-size-limit probe hit SQLITE_IOERR rather than FULL; a separate stage-only ENOSPC fault-injection rerun passes cleanup/retry. Logs: `~/review-carddav-r2b.log` and `~/review-carddav-r2-enospc-b.log`. Untracked review probes live only on VMs; no tracked source edits. **verdict: fix first**, for R2-1. New findings: **P0 0, P1 0, P2 1, P3 0**. No code pushed, merge or production access.
Author
Owner

R2-1 implementation plan (shared #776/#777): remove the per-handle cached epoch. Sync reads epoch/head/floor/rows in one SQLite read transaction, then releases that snapshot and checks the current committed epoch before returning. Token/ctag use the same guard. Old tokens return RFC 6578 invalid-token; a rebuild observed during a read fails closed.

Bounds: constant metadata/epoch work per request, existing bounded sync page and retention costs unchanged; one read transaction, no book-sized copy. No new lock across rebuild staging.

failure effect detection recovery test
held handle after rebuild old token refused durable epoch mismatch empty-token full resync independent reader, delta and initial continuation
rebuild during snapshot discard old reply committed epoch differs after snapshot release retry/full resync deterministic interleaved rebuild
sqlite read error request fails propagated error retry existing rollback/rebuild suites

Gates: reproduce held-reader failure first; cache/read tests on #776, inherited tests plus rebuild suites on #777; re-merge #777 into #794 and run requested dav/carddav/auth/server, mount, contract, clippy -D warnings and fmt before pushing #794. No dev merge or additional review round.

R2-1 implementation plan (shared #776/#777): remove the per-handle cached epoch. Sync reads epoch/head/floor/rows in one SQLite read transaction, then releases that snapshot and checks the current committed epoch before returning. Token/ctag use the same guard. Old tokens return RFC 6578 invalid-token; a rebuild observed during a read fails closed. Bounds: constant metadata/epoch work per request, existing bounded sync page and retention costs unchanged; one read transaction, no book-sized copy. No new lock across rebuild staging. | failure | effect | detection | recovery | test | |---|---|---|---|---| | held handle after rebuild | old token refused | durable epoch mismatch | empty-token full resync | independent reader, delta and initial continuation | | rebuild during snapshot | discard old reply | committed epoch differs after snapshot release | retry/full resync | deterministic interleaved rebuild | | sqlite read error | request fails | propagated error | retry | existing rollback/rebuild suites | Gates: reproduce held-reader failure first; cache/read tests on #776, inherited tests plus rebuild suites on #777; re-merge #777 into #794 and run requested dav/carddav/auth/server, mount, contract, clippy -D warnings and fmt before pushing #794. No dev merge or additional review round.
Author
Owner

R2-1 implementation status: ready for FINAL review

Shared finding from #776#issuecomment-71275 / #777#issuecomment-71278 implemented in the base and propagated in stack order. Heads: #776 113af4f819, #777 670a883c93. Re-merged #777 into #794: 21e678b258, pushed only after every requested gate passed. Never merged into dev. No additional independent review round started.

Removed the per-handle cached epoch. Each sync pins epoch/head/floor/rows in one SQLite read transaction. After releasing that snapshot, it queries the current committed epoch again and discards the result with InvalidToken if it changed. token and ctag use the same guard. Every emitted sync token carries the epoch read for that snapshot. Existing HTTP mapping returns RFC 6578 403 valid-sync-token, followed by empty-token full resync. A rebuild after the final epoch check is ordered after that read; the returned old-epoch token is then rejected on the next request.

finding / gate status evidence
held reader after successful rebuild pass Independent writer replaces a/c with b/d at the same revision count; held reader rejects both delta and initial continuation tokens as InvalidToken, issues current token/ctag, and fully resyncs replacement rows. Equal revision counts exercise #777's former silent empty delta.
rebuild during a sync read pass Deterministic WAL interleaving commits another handle's rebuild after epoch pinning but before sync-row reads. Both initial and delta cases observe the old snapshot internally, then discard it as InvalidToken after the current committed epoch check.
HTTP invalidation/full resync pass One-card a-to-b replacement with independently held reader and matching source cursor: 403 valid-sync-token, then 207 with b.vcf and fresh token.
#776 tests / clippy / fmt pass 24 CardDAV tests, 0 failures; all targets, -D warnings; workspace fmt.
#777 tests / clippy / fmt pass 31 top-level CardDAV tests, 0 failures; includes staging crash/reopen, retention, WAL reads and rebuild rollback. 50k-card peak RSS 10,380 KiB; concurrent photo books 32,272 KiB, below existing 192 MiB assertions.
#794 dav/carddav/auth/server tests pass All package tests pass, existing ignores retained; includes socket admission/deadlines/slow reading and the epoch regressions.
#794 dav_is_mounted / contract pass Real scoped-auth/contact projection mount and server contract tests both pass.
#794 clippy / fmt pass Requested packages/all targets with -D warnings; workspace fmt clean.
tested source identity pass SHA-256 agrees for all 422 / 426 / 446 Rust/Cargo files in #776 / #777 / #794 respectively; VM-only Nix files excluded and never committed.

VM: claude@calternal-dev-01-compute02. Worktrees: ~/Developer/Worktrees/carddav-epoch-r2, carddav-epoch-stack-r2, dav794-epoch-r2. All builds/tests/fmt used Nix, with heavy work through one ~/bin/slot, -j4. Reused target: /home/claude/Developer/Worktrees/dav794-retention-r1/target; log records recompilation from each checked worktree. Authoritative log ~/carddav-epoch-final2.log contains BASE_GREEN, STACK_GREEN, TRANSPORT_GREEN; ~/carddav-epoch-final2.exit = 0.

Exact commands in the Nix shell, inside the slot. On each of #776 and #777:

cargo test -j4 -p calternal-carddav --locked
cargo clippy -j4 -p calternal-carddav --all-targets --locked -- -D warnings
cargo fmt --all -- --check

On the refreshed #794:

cargo test -j4 -p calternal-dav -p calternal-carddav -p calternal-auth -p calternal-server --locked --no-fail-fast
cargo test -j4 -p calternal --bin calternal dav_is_mounted --locked
cargo test -j4 -p calternal --bin calternal contract --locked
cargo clippy -j4 -p calternal-dav -p calternal-carddav -p calternal-auth -p calternal-server -p calternal --all-targets --locked -- -D warnings
cargo fmt --all -- --check

Failing-before evidence: ~/carddav-epoch-red.log, baseline 407eabf0 (original base plus regression), exits 101 because the held reader accepts the obsolete epoch. Two subsequent test-fixture mistakes were corrected: an accidentally changed existing retention expectation, and the HTTP fake source cursor triggering an unrelated second rebuild. The final source above passes all gates; earlier runs are not claimed green.

Decisions/limits: constant extra epoch queries and a read transaction; existing bounded page/retention/staging behavior preserved. No new book-sized allocation or rebuild lock. Under repeated concurrent rebuilds a request may fail and require full resync instead of returning stale success. Native Apple replay, perf-VM latency budgets and deterministic kill/power loss inside backup installation remain the previously documented limits; none is claimed here. FINAL review remains pending.

## R2-1 implementation status: ready for FINAL review Shared finding from #776#issuecomment-71275 / #777#issuecomment-71278 implemented in the base and propagated in stack order. Heads: #776 **113af4f81970cba5d7c588de856d6b3d6bfd80e9**, #777 **670a883c9359c64cc73ec7ee847adccf0e5c9bfd**. Re-merged #777 into #794: **21e678b258f8fe7bb765897fc227d95e2b11abaf**, pushed only after every requested gate passed. Never merged into dev. No additional independent review round started. Removed the per-handle cached epoch. Each sync pins epoch/head/floor/rows in one SQLite read transaction. After releasing that snapshot, it queries the current committed epoch again and discards the result with `InvalidToken` if it changed. `token` and `ctag` use the same guard. Every emitted sync token carries the epoch read for that snapshot. Existing HTTP mapping returns RFC 6578 **403 `valid-sync-token`**, followed by empty-token full resync. A rebuild after the final epoch check is ordered after that read; the returned old-epoch token is then rejected on the next request. | finding / gate | status | evidence | |---|---|---| | held reader after successful rebuild | pass | Independent writer replaces a/c with b/d at the same revision count; held reader rejects both delta and initial continuation tokens as `InvalidToken`, issues current token/ctag, and fully resyncs replacement rows. Equal revision counts exercise #777's former silent empty delta. | | rebuild during a sync read | pass | Deterministic WAL interleaving commits another handle's rebuild after epoch pinning but before sync-row reads. Both initial and delta cases observe the old snapshot internally, then discard it as `InvalidToken` after the current committed epoch check. | | HTTP invalidation/full resync | pass | One-card a-to-b replacement with independently held reader and matching source cursor: 403 `valid-sync-token`, then 207 with b.vcf and fresh token. | | #776 tests / clippy / fmt | pass | 24 CardDAV tests, 0 failures; all targets, `-D warnings`; workspace fmt. | | #777 tests / clippy / fmt | pass | 31 top-level CardDAV tests, 0 failures; includes staging crash/reopen, retention, WAL reads and rebuild rollback. 50k-card peak RSS 10,380 KiB; concurrent photo books 32,272 KiB, below existing 192 MiB assertions. | | #794 dav/carddav/auth/server tests | pass | All package tests pass, existing ignores retained; includes socket admission/deadlines/slow reading and the epoch regressions. | | #794 `dav_is_mounted` / `contract` | pass | Real scoped-auth/contact projection mount and server contract tests both pass. | | #794 clippy / fmt | pass | Requested packages/all targets with `-D warnings`; workspace fmt clean. | | tested source identity | pass | SHA-256 agrees for all 422 / 426 / 446 Rust/Cargo files in #776 / #777 / #794 respectively; VM-only Nix files excluded and never committed. | VM: **claude@calternal-dev-01-compute02**. Worktrees: `~/Developer/Worktrees/carddav-epoch-r2`, `carddav-epoch-stack-r2`, `dav794-epoch-r2`. All builds/tests/fmt used Nix, with heavy work through one `~/bin/slot`, `-j4`. Reused target: `/home/claude/Developer/Worktrees/dav794-retention-r1/target`; log records recompilation from each checked worktree. Authoritative log **`~/carddav-epoch-final2.log`** contains `BASE_GREEN`, `STACK_GREEN`, `TRANSPORT_GREEN`; **`~/carddav-epoch-final2.exit` = 0**. Exact commands in the Nix shell, inside the slot. On each of #776 and #777: ```sh cargo test -j4 -p calternal-carddav --locked cargo clippy -j4 -p calternal-carddav --all-targets --locked -- -D warnings cargo fmt --all -- --check ``` On the refreshed #794: ```sh cargo test -j4 -p calternal-dav -p calternal-carddav -p calternal-auth -p calternal-server --locked --no-fail-fast cargo test -j4 -p calternal --bin calternal dav_is_mounted --locked cargo test -j4 -p calternal --bin calternal contract --locked cargo clippy -j4 -p calternal-dav -p calternal-carddav -p calternal-auth -p calternal-server -p calternal --all-targets --locked -- -D warnings cargo fmt --all -- --check ``` Failing-before evidence: `~/carddav-epoch-red.log`, baseline **407eabf0** (original base plus regression), exits 101 because the held reader accepts the obsolete epoch. Two subsequent test-fixture mistakes were corrected: an accidentally changed existing retention expectation, and the HTTP fake source cursor triggering an unrelated second rebuild. The final source above passes all gates; earlier runs are not claimed green. Decisions/limits: constant extra epoch queries and a read transaction; existing bounded page/retention/staging behavior preserved. No new book-sized allocation or rebuild lock. Under repeated concurrent rebuilds a request may fail and require full resync instead of returning stale success. Native Apple replay, perf-VM latency budgets and deterministic kill/power loss inside backup installation remain the previously documented limits; none is claimed here. FINAL review remains pending.
Author
Owner

Independent review, round 3 (final)

Reviewed #776 at 113af4f819, the shared round-2 finding (#776#issuecomment-71275 / #777#issuecomment-71278), implementation status (#776#issuecomment-71333 / #777#issuecomment-71338), and propagation into #794 21e678b2.

verdict: ready. New findings: P0 0, P1 0, P2 0, P3 0. The shared R2-1 finding is closed. No new P0/P1 appeared, so kayg's final-round “stop and simplify” trigger is not met.

check status evidence
R2-1: held reader after rebuild verified fixed Epoch is read from SQLite rather than cached per handle. Held delta and initial-continuation tokens are rejected after a same-sized replacement; current token/ctag agree with the writer.
Old token to full resync verified fixed HTTP regression requires 403 valid-sync-token, followed by an empty-token 207 containing replacement b.vcf. No successful empty delta under the obsolete token.
Rebuild during read verified fixed Deterministic WAL interleaving covers initial and delta reads. Old rows are visible inside the pinned snapshot, then discarded as InvalidToken after the transaction ends and the current committed epoch differs.
New transaction boundary no new defect found Epoch/head/floor/rows share one read transaction, including the empty-delta fast path. Error exits drop/roll back the transaction. Token and ctag use the same guard. A rebuild after the final check is ordered after that read; the next request rejects its old-epoch token.
#794 integration no merge regression found 80ec1fa3..21e678b2 changes only cache implementation and regression tests. DAV transport, adapter/properties, auth, admission, response reservations/byte guards, deadlines and socket tests are unchanged.

New findings

None at P0–P3 within this review's scope.

Verification and limits

Fresh independent checks on dev-01, in Nix through ~/bin/slot, reused target /home/claude/Developer/Worktrees/dav794-retention-r1/target:

cargo test -j4 -p calternal-carddav --lib --test cache --test read --locked -- --nocapture

#776: 21 passed, #777: 25 passed, no failures. Both runs include the held-reader cache/HTTP regressions and the deterministic mid-read rebuild test. Logs: ~/review-carddav-r3-final/gates.log (through TREE_GREEN=carddav-epoch-r2) and ~/review-carddav-r3-final/remainder.log (through TREE_GREEN=carddav-epoch-stack-r2). After those passes, #794's attempted rerun remained queued for a slot and was cancelled before Cargo started; no fresh merged-tree or transport runtime pass is claimed.

Independent SHA-256 comparison matches 422/422, 426/426, 446/446 Rust/Cargo files against the specified #776/#777/#794 heads. The #794 VM worktree has an older Git HEAD but its compared source matches 21e678b2. Inspected prior source-matched ~/carddav-epoch-final2.log and exit file (0): all three epoch regressions pass on each tree, with BASE_GREEN, STACK_GREEN, TRANSPORT_GREEN; clippy/fmt and broader transport/auth/socket gates are prior implementation evidence, not fresh runs in this review. Read-only git diff --check passes for f3d6361d..113af4f8, 3c8ddf53..670a883c and 80ec1fa3..21e678b2.

Native Apple replay, perf-VM latency gates and interruption inside backup installation remain the documented limits. No new allocation proportional to book size, write lock or transport-policy change was introduced by this fix. No code changed/pushed, merge or production access.

## Independent review, round 3 (final) Reviewed #776 at **113af4f81970cba5d7c588de856d6b3d6bfd80e9**, the shared round-2 finding (#776#issuecomment-71275 / #777#issuecomment-71278), implementation status (#776#issuecomment-71333 / #777#issuecomment-71338), and propagation into #794 **21e678b2**. **verdict: ready. New findings: P0 0, P1 0, P2 0, P3 0.** The shared R2-1 finding is closed. No new P0/P1 appeared, so kayg's final-round “stop and simplify” trigger is not met. | check | status | evidence | |---|---|---| | R2-1: held reader after rebuild | verified fixed | Epoch is read from SQLite rather than cached per handle. Held delta and initial-continuation tokens are rejected after a same-sized replacement; current token/ctag agree with the writer. | | Old token to full resync | verified fixed | HTTP regression requires 403 `valid-sync-token`, followed by an empty-token 207 containing replacement b.vcf. No successful empty delta under the obsolete token. | | Rebuild during read | verified fixed | Deterministic WAL interleaving covers initial and delta reads. Old rows are visible inside the pinned snapshot, then discarded as `InvalidToken` after the transaction ends and the current committed epoch differs. | | New transaction boundary | no new defect found | Epoch/head/floor/rows share one read transaction, including the empty-delta fast path. Error exits drop/roll back the transaction. Token and ctag use the same guard. A rebuild after the final check is ordered after that read; the next request rejects its old-epoch token. | | #794 integration | no merge regression found | 80ec1fa3..21e678b2 changes only cache implementation and regression tests. DAV transport, adapter/properties, auth, admission, response reservations/byte guards, deadlines and socket tests are unchanged. | ### New findings None at P0–P3 within this review's scope. ### Verification and limits Fresh independent checks on dev-01, in Nix through `~/bin/slot`, reused target `/home/claude/Developer/Worktrees/dav794-retention-r1/target`: ```sh cargo test -j4 -p calternal-carddav --lib --test cache --test read --locked -- --nocapture ``` #776: **21 passed**, #777: **25 passed**, no failures. Both runs include the held-reader cache/HTTP regressions and the deterministic mid-read rebuild test. Logs: `~/review-carddav-r3-final/gates.log` (through `TREE_GREEN=carddav-epoch-r2`) and `~/review-carddav-r3-final/remainder.log` (through `TREE_GREEN=carddav-epoch-stack-r2`). After those passes, #794's attempted rerun remained queued for a slot and was cancelled before Cargo started; no fresh merged-tree or transport runtime pass is claimed. Independent SHA-256 comparison matches **422/422**, **426/426**, **446/446** Rust/Cargo files against the specified #776/#777/#794 heads. The #794 VM worktree has an older Git HEAD but its compared source matches 21e678b2. Inspected prior source-matched `~/carddav-epoch-final2.log` and exit file (0): all three epoch regressions pass on each tree, with `BASE_GREEN`, `STACK_GREEN`, `TRANSPORT_GREEN`; clippy/fmt and broader transport/auth/socket gates are prior implementation evidence, not fresh runs in this review. Read-only `git diff --check` passes for f3d6361d..113af4f8, 3c8ddf53..670a883c and 80ec1fa3..21e678b2. Native Apple replay, perf-VM latency gates and interruption inside backup installation remain the documented limits. No new allocation proportional to book size, write lock or transport-policy change was introduced by this fix. No code changed/pushed, merge or production access.
Author
Owner

housekeeping #1009: rebased+green, left open for review.

rebased CardDAV read/cache implementation; used final feature snapshots to reconcile chronological new-crate conflicts. fixed the journal fixture to use the current Caller credential.

head: 17726432f0a4497657dd7981189196a6bf5a4739, based on dev cd7f84432febfc63b788fbb9b9f9950452982d74; pushed with the original exact force-with-lease.

Mac VM gates passed through Nix, shared Mac lock and slot: frontend frozen install, check, lint, build, bundle gate and unit tests; cargo fmt -p calternal-carddav -p calternal-contacts-actions -p calternal-contacts -- --check; cargo check -j4 --locked -p calternal-carddav -p calternal-contacts-actions -p calternal-contacts --all-targets; the same package selection with cargo clippy --all-targets -- -D warnings and cargo test. frontend ran before the Rust-only fixture repair; the Rust run records the pushed head. commands and logs.

original-head patch inventory, pr-776.md.

housekeeping #1009: rebased+green, left open for review. rebased CardDAV read/cache implementation; used final feature snapshots to reconcile chronological new-crate conflicts. fixed the journal fixture to use the current Caller credential. head: `17726432f0a4497657dd7981189196a6bf5a4739`, based on dev `cd7f84432febfc63b788fbb9b9f9950452982d74`; pushed with the original exact force-with-lease. Mac VM gates passed through Nix, shared Mac lock and slot: frontend frozen install, check, lint, build, bundle gate and unit tests; `cargo fmt -p calternal-carddav -p calternal-contacts-actions -p calternal-contacts -- --check`; `cargo check -j4 --locked -p calternal-carddav -p calternal-contacts-actions -p calternal-contacts --all-targets`; the same package selection with `cargo clippy --all-targets -- -D warnings` and `cargo test`. frontend ran before the Rust-only fixture repair; the Rust run records the pushed head. [commands and logs](https://git.kayg.org/attachments/54bfe4ba-a0ef-45dc-9755-bf88ab5ce785). [original-head patch inventory](https://git.kayg.org/attachments/fe3c09dc-dea3-47a0-92e2-b44a777083e5), `pr-776.md`.
Some checks failed
CI / server (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / web (pull_request) Has been cancelled
Merge conflict checking is in progress. Try again in few moments.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue/carddav-read-r2:issue/carddav-read-r2
git switch issue/carddav-read-r2

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch dev
git merge --no-ff issue/carddav-read-r2
git switch issue/carddav-read-r2
git rebase dev
git switch dev
git merge --ff-only issue/carddav-read-r2
git switch issue/carddav-read-r2
git rebase dev
git switch dev
git merge --no-ff issue/carddav-read-r2
git switch dev
git merge --squash issue/carddav-read-r2
git switch dev
git merge --ff-only issue/carddav-read-r2
git switch dev
git merge issue/carddav-read-r2
git push origin dev
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal!776
No description provided.