a73x

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.