a73x

8d33fc5a

Avoid double DAG walk after auto_detect_revision

a73x   2026-03-21 19:08

Change auto_detect_revision to return Option<Revision> instead of u32,
so callers can push the new revision onto their in-memory PatchState
rather than re-materializing the entire DAG via from_ref_uncached.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

diff --git a/src/patch.rs b/src/patch.rs
index dba1b83..21d7e8b 100644
--- a/src/patch.rs
+++ b/src/patch.rs
@@ -6,47 +6,55 @@ use crate::error::Error;
use crate::event::{Action, Event, ReviewVerdict};
use crate::identity::get_author;
use crate::signing;
use crate::state::{self, PatchState, PatchStatus};
use crate::state::{self, PatchState, PatchStatus, Revision};

/// Auto-detect whether the branch tip has changed since the last recorded revision.
/// If it has, append a PatchRevision event. Returns the current revision number.
/// If it has, append a PatchRevision event and return the new `Revision` so callers
/// can update their in-memory `PatchState` without re-walking the DAG.
fn auto_detect_revision(
    repo: &Repository,
    ref_name: &str,
    patch: &PatchState,
    sk: &ed25519_dalek::SigningKey,
) -> Result<u32, Error> {
) -> Result<Option<Revision>, Error> {
    let current_rev = patch.revisions.last().map(|r| r.number).unwrap_or(0);

    // Try to resolve the branch tip
    let tip_oid = match patch.resolve_head(repo) {
        Ok(oid) => oid,
        Err(_) => return Ok(current_rev), // Branch deleted or unavailable
        Err(_) => return Ok(None), // Branch deleted or unavailable
    };

    let last_commit = patch.revisions.last().map(|r| r.commit.as_str()).unwrap_or("");
    let tip_hex = tip_oid.to_string();

    if tip_hex == last_commit {
        return Ok(current_rev);
        return Ok(None);
    }

    // Branch tip changed — insert a PatchRevision event
    let commit = repo.find_commit(tip_oid)?;
    let tree_oid = commit.tree()?.id();
    let author = get_author(repo)?;
    let timestamp = chrono::Utc::now().to_rfc3339();
    let event = Event {
        timestamp: chrono::Utc::now().to_rfc3339(),
        timestamp: timestamp.clone(),
        author,
        action: Action::PatchRevision {
            commit: tip_hex,
            commit: tip_hex.clone(),
            tree: tree_oid.to_string(),
            body: None,
        },
        clock: 0,
    };
    dag::append_event(repo, ref_name, &event, sk)?;
    Ok(current_rev + 1)
    Ok(Some(Revision {
        number: current_rev + 1,
        commit: tip_hex,
        tree: tree_oid.to_string(),
        body: None,
        timestamp,
    }))
}

pub fn create(
@@ -209,14 +217,10 @@ pub fn comment(
    let patch = PatchState::from_ref(repo, &ref_name, &id)?;

    // Auto-detect revision (runs for all comment types to record branch changes)
    let current_rev = auto_detect_revision(repo, &ref_name, &patch, &sk)?;

    // Re-read state after potential revision insertion
    let patch = if current_rev != patch.revisions.last().map(|r| r.number).unwrap_or(0) {
        PatchState::from_ref_uncached(repo, &ref_name, &id)?
    } else {
        patch
    };
    let mut patch = patch;
    if let Some(rev) = auto_detect_revision(repo, &ref_name, &patch, &sk)? {
        patch.revisions.push(rev);
    }

    let author = get_author(repo)?;

@@ -280,14 +284,10 @@ pub fn review(
    let patch = PatchState::from_ref(repo, &ref_name, &id)?;

    // Auto-detect revision
    let current_rev = auto_detect_revision(repo, &ref_name, &patch, &sk)?;

    // Re-read state after potential revision insertion
    let patch = if current_rev != patch.revisions.last().map(|r| r.number).unwrap_or(0) {
        PatchState::from_ref_uncached(repo, &ref_name, &id)?
    } else {
        patch
    };
    let mut patch = patch;
    if let Some(rev) = auto_detect_revision(repo, &ref_name, &patch, &sk)? {
        patch.revisions.push(rev);
    }

    let rev = if let Some(target) = target_revision {
        if !patch.revisions.iter().any(|r| r.number == target) {
@@ -366,13 +366,14 @@ pub fn merge(repo: &Repository, id_prefix: &str) -> Result<PatchState, crate::er
    }

    // Auto-detect revision before merge
    auto_detect_revision(repo, &ref_name, &p, &sk)?;
    let mut p = p;
    if let Some(rev) = auto_detect_revision(repo, &ref_name, &p, &sk)? {
        p.revisions.push(rev);
    }

    // Check merge policy
    let config = read_collab_config(repo);
    if config.require_approval_on_latest {
        // Re-read state after potential revision insertion
        let p = PatchState::from_ref_uncached(repo, &ref_name, &id)?;
        let latest_rev = p.revisions.last().map(|r| r.number).unwrap_or(0);
        let has_approval = p.reviews.iter().any(|r| {
            r.verdict == ReviewVerdict::Approve && r.revision == Some(latest_rev)