gitbutlerapp/gitbutler · error

worktree-changes are always set if there are hunks

Error message

worktree-changes are always set if there are hunks

What it means

In `but_core::tree` (tree/mod.rs), when committing selected hunks the code applies `hunks_to_commit` onto the worktree base and expects the corresponding worktree change data to exist — hence `unreachable!("worktree-changes are always set if there are hunks")`. The invariant is: a `ChangeRequest` carrying hunks must also carry the worktree changes those hunks were computed from. The panic fires when hunks are present but the worktree-change slot is `None`, i.e. an internally inconsistent change request.

Source

Thrown at crates/but-core/src/tree/mod.rs:349

                &md,
                base_rela_path,
                &path,
                &mut pipeline,
                &index,
            )?;
            let base_with_patches = apply_hunks(
                worktree_base.as_bstr(),
                current_worktree.as_bstr(),
                &hunks_to_commit,
            )?;
            let blob_with_selected_patches = repo.write_blob(base_with_patches.as_slice())?;
            base_tree_editor.upsert(
                change_request.path.as_bstr(),
                current_entry_kind,
                blob_with_selected_patches,
            )?;
        } else {
            unreachable!("worktree-changes are always set if there are hunks")
        }
    }

    let altered_base_tree_id = base_tree_editor.write()?;
    Ok((altered_base_tree_id, actual_base_tree))
}

/// Given `hunks_to_keep` (ascending hunks by starting line) and the set of `worktree_hunks_no_context`
/// (worktree hunks without context), return `(hunks_to_commit, rejected_hunks)`.
/// `hunks_to_commit` is the headers to drive the additive operation to create the buffer to commit, and `rejected_hunks` is the list of
/// hunks from `hunks_to_keep` that couldn't be associated with `worktree_hunks_no_context` because they weren't included.
///
/// `worktree_hunks` is the hunks with a given amount of context, usually 3, and it's used to quickly select original hunks
/// without sub-selection, which is needed when no sub-selections are specified for all hunks. Those with sub-selections and without
/// can be mixed freely though.
///
/// `hunks_to_keep` indicate that they are a selection of either old or new by marking the other side with `0,0`, i.e. `-1,2 +0,0` selects *old* `1,2`,
/// and `-0,0 +2,3` selects *new* `2,3`. Our job here is to rebuild the original hunk selections from that, as if the user had

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Populate the worktree changes for every path that carries hunks before calling the tree-building API.
  2. Validate incoming requests: reject or skip paths where `hunks` is non-empty but worktree changes are `None`.
  3. After external worktree mutations, recompute hunks and worktree changes together from one diff instead of mixing snapshots.

Example fix

// before
if let Some(changes) = change_request.worktree_changes {
    // apply hunks onto base...
} else {
    unreachable!("worktree-changes are always set if there are hunks")
}

// after
let Some(changes) = change_request.worktree_changes else {
    anyhow::bail!(
        "path '{}' has hunks but no worktree changes; recompute the change request",
        change_request.path.display()
    );
};
Defensive patterns

Strategy: validation

Validate before calling

// reject inconsistent change requests before tree building
for req in &change_requests {
    if !req.hunks.is_empty() && req.worktree_changes.is_none() {
        anyhow::bail!(
            "path '{}' has hunks but no worktree changes; recompute the request",
            req.path.display()
        );
    }
}

Type guard

fn has_aligned_worktree_state(req: &ChangeRequest) -> bool {
    req.hunks.is_empty() || req.worktree_changes.is_some()
}

Prevention

When it happens

Trigger: Building a `ChangeRequest` with `hunks` populated but worktree changes left unset; reusing hunks computed against a worktree snapshot whose diff was later cleared; serialization round-trips (Tauri/SDK boundary) dropping the worktree-changes field while keeping hunks.

Common situations: Frontend sends partially-populated change requests; stale UI state after the worktree is reverted externally; DTO conversions that make worktree changes optional independently of hunks.

Related errors


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