a73x

Deleting a patch is local-only, so a peer republishes it on the next sync

open   by a73x

`patch delete` removes the local ref and nothing else. Any clone that still holds the patch republishes it on its next sync, and the deleting clone re-adopts it on the one after.

Demonstrated accidentally, in production. A two-clone round trip left a test patch `4c718907` on the live server. I deleted it locally, the stale refs were deleted from the server by hand, and the tracker looked clean. It is now visible on https://git.a73x.sh — `4c718907 open Two-clone round trip test by bob` — because the second clone still had it and synced.

```
bob's clone:  2 refs   (never deleted there)
my clone:     2 refs   (re-adopted)
the server:   2 refs   (re-pushed by bob)
```

**This is correct behaviour for an append-only DAG** — deletion is not a concept the model has, and a peer holding data cannot be told to forget it by someone else's local action. The problem is that the command is called `delete` and reports `Deleted patch <id>`, which reads as an assertion about the patch rather than about one clone's refs.

The trap has teeth beyond tidiness: the obvious way to remove something published by mistake — a wrong file, a credential in a patch body — is `patch delete`, and it will appear to work and then quietly undo itself. Whoever does that will believe the removal succeeded.

Wanted, in rough order of value:

- **Say what it does.** `Deleted patch <id> from this repository. Other clones that still have it will republish it on their next sync.` The behaviour may be unfixable; the false impression is not.
- Consider whether `--json` should say the same, since a script cannot read a caveat it was never given.
- Decide whether a distributed removal is wanted at all. A tombstone event that peers honour is expressible — `BodyEdit`/delete already supersede text this way (`b8a55806`) — but it is a genuine design question, not a bug fix, and "the DAG is an audit trail" was the deliberate answer for comments.
- At minimum, document it in the README beside the storage model, where the append-only property is already explained.

Found by looking at the deployed site (5c62b43b). No amount of local measurement would have shown it — my clone reported clean at the time.

Comments

a73x   2026-08-14 07:56

**A second resurrection path, separate from the peer.** Cleaning this up took two deletions, not one.

After the patch was gone from my clone, from the server, and from bob's clone (deleted entirely), it was still held in **local sync staging**:

```
refs/collab/sync/a73x/patches/4c7189…/events
refs/collab/sync/a73x/patches/4c7189…/rev/8314eb52…
```

Staging is scratch — `sync` fetches into `refs/collab/sync/<remote>/*`, reconciles, then `cleanup_sync_refs` removes it. But it survives a sync that does not reach cleanup, and today's syncs failed partway more than once (the refname conflicts during the layout migration). `git-collab refs` was reporting **357 sync refs** on this clone shortly before; after a clean sync it reports zero, which is what that namespace is supposed to look like at rest.

So a deleted object can come back from a peer *or* from this clone's own leftover scratch. Only after deleting both did it stay gone across a sync (verified: local 0, server 0, still 0 after `git-collab sync`).

The peer path is inherent to the model. **This one is not** — scratch that outlives the operation that created it, and can then re-adopt data the user removed, is a defect rather than a property. Worth handling separately from the naming question above:

- `cleanup_sync_refs` should run even when a sync fails partway, or staging should be cleared at the *start* of a sync as well as the end. A failed sync leaving state that changes the next one's outcome is the surprising part.
- `git-collab refs` already surfaces the count, which is how this was noticed — 357 sitting there was visible and meaningless until it wasn't.