Carry patches as revision refs instead of branches #2

Closed
opened 2026-08-09 16:27:24 +00:00 by a73x · 2 comments
a73x commented 2026-08-09 16:27:24 +00:00 (Migrated from git.a73x.sh)

Spec: docs/superpowers/specs/2026-08-09-patch-revision-refs-design.md

Move each revision to an immutable ref in the patch's own namespace:

refs/collab/patches/<id>/events     the event DAG (today's refs/collab/patches/<id>)
refs/collab/patches/<id>/r/<n>      revision n's commit, write-once

Patches then travel under the refspecs sync already uses (sync.rs:230-233), so contributing needs no 'git push' of a branch and no write access to refs/heads/*.

Fixes two live defects:

  • Revision commits are only reachable while the branch still reaches them, so an author who rebases and force-pushes strips earlier revisions of their blobs. Interdiff and revision-anchored inline comments then point at objects git gc may delete.
  • Duplicate detection matches patches by branch name (patch.rs:106-112), which breaks for agents working on generated, ephemeral worktree branches.

Also removes PatchState.base_commit in favour of a per-revision base; see the follow-up issue on interdiff.


Migrated from git-collab issue c65ac0e2646c3e7aa884672f35d15dcfa16f41a3.

Original author: a73x dev@a73x.sh.

Close reason: Implemented by patch 659f0350, merged

Spec: docs/superpowers/specs/2026-08-09-patch-revision-refs-design.md Move each revision to an immutable ref in the patch's own namespace: refs/collab/patches/<id>/events the event DAG (today's refs/collab/patches/<id>) refs/collab/patches/<id>/r/<n> revision n's commit, write-once Patches then travel under the refspecs sync already uses (sync.rs:230-233), so contributing needs no 'git push' of a branch and no write access to refs/heads/*. Fixes two live defects: - Revision commits are only reachable while the branch still reaches them, so an author who rebases and force-pushes strips earlier revisions of their blobs. Interdiff and revision-anchored inline comments then point at objects git gc may delete. - Duplicate detection matches patches by branch name (patch.rs:106-112), which breaks for agents working on generated, ephemeral worktree branches. Also removes PatchState.base_commit in favour of a per-revision base; see the follow-up issue on interdiff. --- Migrated from git-collab issue `c65ac0e2646c3e7aa884672f35d15dcfa16f41a3`. Original author: a73x <dev@a73x.sh>. Close reason: Implemented by patch 659f0350, merged
a73x commented 2026-08-09 16:51:59 +00:00 (Migrated from git.a73x.sh)

Demonstrated live while merging this session's five agent patches.

Patch 9fada820 recorded revision 1 at commit 91b337da. The branch was rebased onto main and fast-forwarded - an entirely ordinary merge - and afterwards:

git merge-base --is-ancestor 91b337da main    -> NO
git for-each-ref --contains 91b337da | wc -l  -> 0

The commit revision 1 points at is reachable from zero refs. It survives only until the next 'git gc', at which point 'patch log 9fada820' and any interdiff against r1 break permanently, with no warning and no way to recover.

This is exactly what the write-once refs/collab/patches//r/ layout prevents, and it needs no unusual workflow to trigger - just rebase-and-fast-forward, which is the normal way to keep a linear history.

Demonstrated live while merging this session's five agent patches. Patch 9fada820 recorded revision 1 at commit 91b337da. The branch was rebased onto main and fast-forwarded - an entirely ordinary merge - and afterwards: git merge-base --is-ancestor 91b337da main -> NO git for-each-ref --contains 91b337da | wc -l -> 0 The commit revision 1 points at is reachable from zero refs. It survives only until the next 'git gc', at which point 'patch log 9fada820' and any interdiff against r1 break permanently, with no warning and no way to recover. This is exactly what the write-once refs/collab/patches/<id>/r/<n> layout prevents, and it needs no unusual workflow to trigger - just rebase-and-fast-forward, which is the normal way to keep a linear history.
a73x commented 2026-08-09 17:15:29 +00:00 (Migrated from git.a73x.sh)

Confirmed by demonstration: today's branch-tip auto-detection is a silent side effect of unrelated writes, not of reads.

patch comment and patch review both call auto_detect_revision() (src/patch.rs, from comment() ~line 268 and review() ~line 332), which compares the branch's current tip to the last recorded revision and, if they differ, appends a PatchRevision event BEFORE recording the comment or review - with no indication to the user. Reads (patch show, patch log) never trigger it, since state is replayed from recorded events only. So rebasing a patch's branch and then looking at it shows nothing new.

The next unrelated write then manufactures a revision. Reproduced: amend a patch branch's tip, confirm patch log still shows only r1, then run 'patch comment' - the CLI prints 'Comment added.' and nothing else, but patch log now shows r2, timestamped at the comment rather than the code change, with an empty body (0 files changed, +0 -0). Repeating with 'patch review' produced r3 the same way.

patch revise does not go through this helper - it has its own explicit tip check and errors with 'no changes since revision N', which is the intended behaviour.

Since the explicit patch revise command already exists, the auto-detection path is redundant and actively misleading: it records history the user did not ask for, at a time unrelated to the actual code change. Another argument for the explicit-only model in this design.

Confirmed by demonstration: today's branch-tip auto-detection is a silent side effect of unrelated writes, not of reads. patch comment and patch review both call auto_detect_revision() (src/patch.rs, from comment() ~line 268 and review() ~line 332), which compares the branch's current tip to the last recorded revision and, if they differ, appends a PatchRevision event BEFORE recording the comment or review - with no indication to the user. Reads (patch show, patch log) never trigger it, since state is replayed from recorded events only. So rebasing a patch's branch and then looking at it shows nothing new. The next unrelated write then manufactures a revision. Reproduced: amend a patch branch's tip, confirm patch log still shows only r1, then run 'patch comment' - the CLI prints 'Comment added.' and nothing else, but patch log now shows r2, timestamped at the comment rather than the code change, with an empty body (0 files changed, +0 -0). Repeating with 'patch review' produced r3 the same way. patch revise does not go through this helper - it has its own explicit tip check and errors with 'no changes since revision N', which is the intended behaviour. Since the explicit patch revise command already exists, the auto-detection path is redundant and actively misleading: it records history the user did not ask for, at a time unrelated to the actual code change. Another argument for the explicit-only model in this design.
Sign in to join this conversation.
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
a73x/git-collab#2
No description provided.