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 hadView on GitHub (pinned to caf1f223d3)
Solutions
- Populate the worktree changes for every path that carries hunks before calling the tree-building API.
- Validate incoming requests: reject or skip paths where `hunks` is non-empty but worktree changes are `None`.
- 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
- Always derive hunks and worktree changes from the same diff pass, never mixed snapshots.
- After external worktree mutations (revert, checkout), invalidate stored hunks before reuse.
- Validate DTO conversions at the SDK/Tauri boundary so worktree changes cannot be dropped independently of hunks.
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
- we never return these as the status iteration is configured
- BUG: we use 'matching' so there are no directories
- disabled
- BUG: this must have been deactivated
- BUG: `gix` disables this, as it knows we always need to be a
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/b6dde9e976418c8d.
Report an issue: GitHub.