specs/012-patch-branch-refactor/research.md
Ref: Size: 3.8 KiB
# Research: Patch-as-Branch Refactor
## Decision 1: Branch Name Storage in PatchCreate Event
**Decision**: Add an optional `branch` field to `Action::PatchCreate`. When present, it replaces `head_commit` as the source of truth. When absent (old-format patches), fall back to `head_commit`.
**Rationale**: Adding an optional field maintains backward compatibility — old events deserialize without the field, new events include it. The `head_commit` field is kept but becomes redundant for new patches (it can store the branch tip at creation time for reference).
**Alternatives considered**:
- Replace `head_commit` entirely → breaks backward compat
- Store branch name outside the DAG (e.g., in a separate ref) → adds complexity, loses atomicity
- Use a new Action variant `PatchCreateV2` → awkward, fragments the event model
## Decision 2: How to Resolve Branch to Current Head
**Decision**: Use `repo.refname_to_id(&format!("refs/heads/{}", branch))` to resolve the branch to its current tip commit. This is always live — never stale.
**Rationale**: This is the fundamental shift. Instead of storing a snapshot OID, we look up the branch ref every time. If the developer pushes new commits, the next `patch show` automatically reflects them.
**Alternatives considered**:
- Cache the head OID → defeats the purpose of live branch tracking
- Use `revparse_single` on the branch name → works but `refname_to_id` is more explicit
## Decision 3: Staleness Detection
**Decision**: Use `repo.merge_base(base_tip, branch_tip)` to find the common ancestor, then `repo.graph_ahead_behind(branch_tip, base_tip)` to get (ahead, behind) counts.
**Rationale**: git2 provides `graph_ahead_behind` which returns exactly what we need — how many commits the branch is ahead (patch size) and how many the base has moved (staleness). No need to shell out to `git rev-list`.
**Alternatives considered**:
- Shell out to `git rev-list --count` → unnecessary, git2 has the API
- Walk the revwalk manually → `graph_ahead_behind` does this already
## Decision 4: Diff Generation
**Decision**: Use three-dot diff semantics — diff between merge-base and branch tip. This shows only the changes introduced by the branch, not changes on the base since the branch diverged.
**Rationale**: This matches `git diff base...branch` which is what reviewers expect. The existing `generate_diff` already does tree-to-tree diff; it just needs to use merge-base instead of base tip directly.
**Alternatives considered**:
- Two-dot diff (base tip vs branch tip) → includes base changes, confusing for review
- Show both → over-complicated for the common case
## Decision 5: Merge Strategy
**Decision**: Use `repo.merge_analysis()` + `repo.merge()` from git2 for the actual merge, matching standard `git merge` behavior. Already partially implemented in the existing `patch::merge()`.
**Rationale**: The existing merge function already does most of this — it resolves the head commit, does analysis, and merges. The change is just where the head commit comes from (branch ref instead of stored OID).
## Decision 6: CLI Changes
**Decision**: Change `--head` to `--branch` (or `--head` becomes optional, defaulting to current branch name). The `--base` flag stays as-is.
**Rationale**: The mental model shifts from "submit this commit for review" to "submit this branch for review". The flag name should reflect this.
## Decision 7: Radicle-Inspired Patterns
**Decision**: Adopt Radicle's principle of separation — branches are pure git, the collab DAG is pure review. Don't try to store code state in the DAG.
**Rationale**: Radicle stores patches as COBs (collaborative objects) that reference branches, not commits. Their experience shows this model scales well for peer-to-peer collaboration. Our simpler append-only DAG achieves the same separation.