gitbutlerapp/gitbutler · error · anyhow::Error
Ad-hoc (single-branch) branch moves are not supported inside
Error message
Ad-hoc (single-branch) branch moves are not supported inside transactions
What it means
The transactional wrapper around branch moves asserts that the underlying but_workspace::branch::move_branch produced no new_tip/branch_stack_order. If it did, the move was an ad-hoc single-branch move (a legacy, non-transactional shape), which transactions cannot record atomically. The ensure! fires to refuse rather than half-apply.
Source
Thrown at crates/but-transaction/src/lib.rs:457
/// field is ever set rather than silently dropping a metadata reorder or a required checkout.
///
/// [`Outcome::new_tip`]: but_workspace::branch::move_branch::Outcome::new_tip
/// [`Outcome::branch_stack_order`]: but_workspace::branch::move_branch::Outcome::branch_stack_order
pub fn stack_branch_on(
&mut self,
source_branch: &FullNameRef,
target_branch: &FullNameRef,
) -> anyhow::Result<()> {
let (ws_meta, new_tip, branch_stack_order) = self.rebase(|editor, _| {
let outcome = but_workspace::branch::move_branch(editor, source_branch, target_branch)?;
Ok((
(outcome.ws_meta, outcome.new_tip, outcome.branch_stack_order),
MaterializeWithoutCheckout::No,
outcome.rebase,
))
})?;
anyhow::ensure!(
new_tip.is_none() && branch_stack_order.is_none(),
"Ad-hoc (single-branch) branch moves are not supported inside transactions"
);
self.record_workspace_metadata_update(ws_meta)?;
Ok(())
}
pub fn tear_off_branch(&mut self, source_branch: &FullNameRef) -> anyhow::Result<()> {
let ws_meta = self.rebase(|editor, _| {
let outcome = but_workspace::branch::tear_off_branch(editor, source_branch, None)?;
Ok((
outcome.ws_meta,
MaterializeWithoutCheckout::No,
outcome.rebase,
))
})?;View on GitHub (pinned to caf1f223d3)
Solutions
- Use the non-transactional single-branch move API for ad-hoc moves
- Use the transaction API only for operations that move entire stacks and return no per-branch tip/order changes
- If you maintain the workspace layer, return the stack-shaped outcome (no new_tip/branch_stack_order) for transactional callers
Defensive patterns
Strategy: type-guard
Validate before calling
// before the transactional call, confirm the move is stack-shaped, not ad-hoc
let outcome_preview = but_workspace::branch::move_branch(&editor, &src, &tgt)?;
if outcome_preview.new_tip.is_some() || outcome_preview.branch_stack_order.is_some() {
return Err(anyhow!("ad-hoc move — use the non-transactional API"));
} Type guard
fn is_stack_move(outcome: &MoveBranchOutcome) -> bool {
outcome.new_tip.is_none() && outcome.branch_stack_order.is_none()
} Try / catch
match tx.move_branch(&src, &tgt) {
Err(e) if e.to_string().contains("Ad-hoc") => { /* route to non-transactional move API */ }
other => other,
} Prevention
- Match the API to the operation: whole-stack moves through transactions, single-branch moves through the ad-hoc API
- Watch for regression tests covering both move shapes when refactoring branch move code
When it happens
Trigger: Calling the transaction-based move_branch(source, target) where the workspace operation resolves to a single-branch (ad-hoc) move instead of a full stack move — e.g. moving a branch whose stack contains other managed branches such that only one branch relocates.
Common situations: UI or API code migrated from the ad-hoc move API onto the transaction API without restricting it to whole-stack moves; reordering branches in a multi-branch workspace via the wrong entry point.
Related errors
- cannot mix operations that require `materialize` and `materi
- No uncommitted changes assigned to branch: {branch_name}
- Unable to determine target commit for unassigned change: {}
- Cannot unapply anonymous segments yet even if they have a st
- Cannot position reference below unborn segment '{}'
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/5ffede1d5ef864d7.
Report an issue: GitHub.