gitbutlerapp/gitbutler · info

anchor is always present in the order at this point

Error message

anchor is always present in the order at this point

What it means

Panic from `expect("anchor is always present in the order at this point")` in `insert_into_order` (but-workspace create_reference.rs:629). The function pushes `anchor` into `order` if missing, then `retain` removes only `new_ref`, so the subsequent `position()` lookup must succeed — the source comment documents exactly this reasoning. It is a locally proven invariant; a panic implies someone changed the push/retain logic so the anchor can be dropped.

Source

Thrown at crates/but-workspace/src/branch/create_reference.rs:629

    /// - [`Position::Above`] takes the anchor's slot, pushing the anchor down; [`Position::Below`]
    ///   goes right after the anchor. This only affects *ordering* — unlike [`Anchor::AtSegment`],
    ///   it never changes which branch owns the commit.
    fn insert_into_branch_stack_order(
        mut order: Vec<gix::refs::FullName>,
        anchor: &gix::refs::FullNameRef,
        new_ref: &gix::refs::FullNameRef,
        position: Position,
    ) -> Vec<gix::refs::FullName> {
        if !order.iter().any(|branch| branch.as_ref() == anchor) {
            order.push(anchor.to_owned());
        }
        order.retain(|branch| branch.as_ref() != new_ref);
        // `anchor` was pushed above if it was missing, and `retain` only drops `new_ref`, so the
        // anchor is guaranteed to still be present here.
        let anchor_idx = order
            .iter()
            .position(|branch| branch.as_ref() == anchor)
            .expect("anchor is always present in the order at this point");
        let insert_idx = match position {
            Position::Above => anchor_idx,
            Position::Below => anchor_idx + 1,
        };
        order.insert(insert_idx, new_ref.to_owned());
        order
    }

    fn is_not_a_directory_ref_edit_error(err: &gix::reference::edit::Error) -> bool {
        matches!(
            err,
            gix::reference::edit::Error::FileTransactionPrepare(
                gix::refs::file::transaction::prepare::Error::Io(io_err)
            ) if io_err.kind() == std::io::ErrorKind::NotADirectory
        )
    }

    fn find_colliding_ref_ancestor(

View on GitHub (pinned to caf1f223d3)

Solutions

  1. No caller-side action — the invariant holds in the shipped code
  2. For maintainers editing this function: keep the push-then-retain order, and assert `anchor != new_ref` upstream if renaming becomes possible
  3. Add a unit test covering anchor==new_ref if you touch this logic

Example fix

// before (fragile after edits)
order.retain(|branch| branch.as_ref() != new_ref);
let anchor_idx = order.iter().position(|b| b.as_ref() == anchor).expect("...");

// after: make the invariant explicit
if anchor == new_ref { return order; }
let Some(anchor_idx) = order.iter().position(|b| b.as_ref() == anchor) else {
    bail!("anchor {anchor} disappeared from branch order");
};
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: Calling branch-order insertion where `anchor == new_ref` combined with a code change that makes retain drop more than `new_ref`; refactors of `insert_into_order` that reorder the push/retain/position steps; only reachable through a modified implementation, not through any input to the shipped one.

Common situations: Contributors editing this helper (e.g. adding rename handling that also removes the old anchor name); downstream forks with custom order-filtering; none for stock GitButler users.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/a4e75ed6d5b8ac8a. Report an issue: GitHub.