a73x

dedc4707

Extract auto-detect merge logic into PatchState::check_auto_merge()

a73x   2026-03-22 10:12

Deduplicate the merge detection code that was inline in both from_ref()
(post-cache-hit path) and from_ref_uncached(). Both now call the shared
private method check_auto_merge(&mut self, repo).

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

diff --git a/src/state.rs b/src/state.rs
index 14580cb..5b5cfb2 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -368,6 +368,28 @@ impl PatchState {
        Ok((ahead, behind))
    }

    /// Auto-detect merge: if the patch is still Open and its head is
    /// reachable from the base branch tip, the user merged it outside of
    /// git-collab.  We compare the current base tip to the base_commit
    /// recorded at creation time — if it has moved to include the patch
    /// head, the patch is merged.
    fn check_auto_merge(&mut self, repo: &Repository) {
        if self.status != PatchStatus::Open {
            return;
        }
        let Ok(patch_head) = self.resolve_head(repo) else { return };
        let base_ref = format!("refs/heads/{}", self.base_ref);
        let Ok(base_tip) = repo.refname_to_id(&base_ref) else { return };
        let base_moved = self.base_commit.as_ref()
            .map(|bc| bc != &base_tip.to_string())
            .unwrap_or(false);
        let reachable = base_tip == patch_head
            || repo.graph_descendant_of(base_tip, patch_head).unwrap_or(false);
        if base_moved && reachable {
            self.status = PatchStatus::Merged;
        }
    }

    pub fn from_ref(
        repo: &Repository,
        ref_name: &str,
@@ -376,25 +398,12 @@ impl PatchState {
        // Check cache first
        if let Some(mut cached) = cache::get_cached_state::<PatchState>(repo, ref_name) {
            // The cache may return a stale Open status if the patch was merged
            // outside of git-collab (e.g. git merge) since the DAG tip hasn't
            // changed.  Re-run the auto-detect merge check for Open patches.
            if cached.status == PatchStatus::Open {
                if let Ok(patch_head) = cached.resolve_head(repo) {
                    let base_ref_name = format!("refs/heads/{}", cached.base_ref);
                    if let Ok(base_tip) = repo.refname_to_id(&base_ref_name) {
                        let base_moved = cached.base_commit.as_ref()
                            .map(|bc| bc != &base_tip.to_string())
                            .unwrap_or(false);
                        let reachable = base_tip == patch_head
                            || repo.graph_descendant_of(base_tip, patch_head).unwrap_or(false);
                        if base_moved && reachable {
                            cached.status = PatchStatus::Merged;
                            // Update the cache with the corrected status
                            if let Ok(tip) = repo.refname_to_id(ref_name) {
                                cache::set_cached_state(repo, ref_name, tip, &cached);
                            }
                        }
                    }
            // outside of git-collab since the DAG tip hasn't changed.
            cached.check_auto_merge(repo);
            if cached.status == PatchStatus::Merged {
                // Update the cache with the corrected status
                if let Ok(tip) = repo.refname_to_id(ref_name) {
                    cache::set_cached_state(repo, ref_name, tip, &cached);
                }
            }
            return Ok(cached);
@@ -536,28 +545,7 @@ impl PatchState {

        if let Some(ref mut s) = state {
            s.last_updated = max_timestamp;

            // Auto-detect merge: if the patch is still Open and its head
            // is reachable from the base branch tip, the user merged it
            // outside of git-collab.  We compare the current base tip to
            // the base_commit recorded at creation time — if it has moved
            // to include the patch head, the patch is merged.
            if s.status == PatchStatus::Open {
                if let Ok(patch_head) = s.resolve_head(repo) {
                    let base_ref = format!("refs/heads/{}", s.base_ref);
                    if let Ok(base_tip) = repo.refname_to_id(&base_ref) {
                        // Base must have moved from where it was at creation
                        let base_moved = s.base_commit.as_ref()
                            .map(|bc| bc != &base_tip.to_string())
                            .unwrap_or(false);
                        let reachable = base_tip == patch_head
                            || repo.graph_descendant_of(base_tip, patch_head).unwrap_or(false);
                        if base_moved && reachable {
                            s.status = PatchStatus::Merged;
                        }
                    }
                }
            }
            s.check_auto_merge(repo);
        }
        state.ok_or_else(|| git2::Error::from_str("no PatchCreate event found in DAG").into())
    }