a73x

specs/012-patch-branch-refactor/plan.md

Ref:   Size: 3.7 KiB

# Implementation Plan: Patch-as-Branch Refactor

**Branch**: `012-patch-branch-refactor` | **Date**: 2026-03-21 | **Spec**: [spec.md](spec.md)

## Summary

Refactor patches from storing commit OID snapshots to referencing git branches. The review DAG (comments, reviews, inline comments) stays as-is. The code changes are tracked by the branch itself. This aligns git-collab with how git natively works and with Radicle's proven model of separating review from code.

## Technical Context

**Language/Version**: Rust 2021 edition
**Primary Dependencies**: git2 0.19, clap 4 (derive), serde/serde_json 1, chrono 0.4, ed25519-dalek 2
**Testing**: cargo test
**Project Type**: CLI tool with TUI dashboard

## Design Decisions

### 1. Event Schema Change

Add optional `branch: Option<String>` to `Action::PatchCreate` in `src/event.rs`. Serde's `skip_serializing_if = "Option::is_none"` ensures old events without this field deserialize correctly. The `head_commit` field is kept — for new patches it stores the branch tip at creation time (for reference), for old patches it remains the source of truth.

### 2. PatchState Branch Resolution

In `src/state.rs`, `PatchState` gains a `branch: Option<String>` field. When building state from the DAG:
- If `branch` is `Some`, the `head_commit` is resolved live from `refs/heads/{branch}` via `repo.refname_to_id()`
- If `branch` is `None`, use the stored `head_commit` (backward compat)

New computed method `PatchState::staleness(repo) -> (ahead, behind)` using `repo.graph_ahead_behind()`.

### 3. CLI Changes

In `src/cli.rs`, `PatchCmd::Create`:
- `--head` becomes `--branch` (short: `-B`). Defaults to current branch name (from `repo.head()`).
- `--head` is kept as hidden/deprecated alias for backward compat.
- If `--branch` points to a detached HEAD or commit OID, auto-create a branch `collab/patch/{short-oid}`.

### 4. Diff via Merge-Base

In `src/patch.rs`, `generate_diff()` changes:
- For branch-based patches: compute merge-base between base and branch tips, diff merge-base tree against branch tip tree (three-dot semantics)
- For old patches: keep current behavior (direct tree diff)

### 5. Merge via Branch

In `src/patch.rs`, `merge()` changes:
- For branch-based patches: resolve branch tip live, merge the branch into base (same git merge, just different head resolution)
- For old patches: keep current behavior

### 6. Revise Becomes Optional

`patch revise` still works but is no longer required. For branch-based patches, pushing to the branch IS the revision. The `revise` command can optionally record a note in the DAG.

### 7. Duplicate Patch Prevention

Before creating a new patch, scan all open patches to check if any already reference the same branch. Error if found.

### 8. TUI Updates

In `src/tui.rs`, patch detail view:
- Show branch name instead of (or alongside) head commit
- Show staleness info: "3 commits ahead, 2 behind main"
- Show "branch not found" if branch was deleted

## Source Code

```text
src/
├── event.rs    # Add optional `branch` field to PatchCreate
├── state.rs    # PatchState: add branch field, live head resolution, staleness
├── cli.rs      # Change --head to --branch, default to current branch
├── patch.rs    # Update create/merge/diff/revise to use branch resolution
├── lib.rs      # Update dispatch for new CLI args
├── tui.rs      # Show branch name and staleness in patch detail
```

## Migration Strategy

1. **No data migration needed** — old events without `branch` field deserialize with `branch: None`, triggering old-format code paths
2. **New patches automatically use branch model** — `patch create` always populates `branch`
3. **Old patches work indefinitely** — the `head_commit` fallback is permanent, not a temporary shim