{"record":{"id":"b6dde9e976418c8d","repo":"gitbutlerapp/gitbutler","slug":"worktree-changes-are-always-set-if-there-are-hunks","errorCode":null,"errorMessage":"worktree-changes are always set if there are hunks","messagePattern":"worktree-changes are always set if there are hunks","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-core/src/tree/mod.rs","lineNumber":349,"sourceCode":"                &md,\n                base_rela_path,\n                &path,\n                &mut pipeline,\n                &index,\n            )?;\n            let base_with_patches = apply_hunks(\n                worktree_base.as_bstr(),\n                current_worktree.as_bstr(),\n                &hunks_to_commit,\n            )?;\n            let blob_with_selected_patches = repo.write_blob(base_with_patches.as_slice())?;\n            base_tree_editor.upsert(\n                change_request.path.as_bstr(),\n                current_entry_kind,\n                blob_with_selected_patches,\n            )?;\n        } else {\n            unreachable!(\"worktree-changes are always set if there are hunks\")\n        }\n    }\n\n    let altered_base_tree_id = base_tree_editor.write()?;\n    Ok((altered_base_tree_id, actual_base_tree))\n}\n\n/// Given `hunks_to_keep` (ascending hunks by starting line) and the set of `worktree_hunks_no_context`\n/// (worktree hunks without context), return `(hunks_to_commit, rejected_hunks)`.\n/// `hunks_to_commit` is the headers to drive the additive operation to create the buffer to commit, and `rejected_hunks` is the list of\n/// hunks from `hunks_to_keep` that couldn't be associated with `worktree_hunks_no_context` because they weren't included.\n///\n/// `worktree_hunks` is the hunks with a given amount of context, usually 3, and it's used to quickly select original hunks\n/// without sub-selection, which is needed when no sub-selections are specified for all hunks. Those with sub-selections and without\n/// can be mixed freely though.\n///\n/// `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`,\n/// 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","sourceCodeStart":331,"sourceCodeEnd":367,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-core/src/tree/mod.rs#L331-L367","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif let Some(changes) = change_request.worktree_changes {\n    // apply hunks onto base...\n} else {\n    unreachable!(\"worktree-changes are always set if there are hunks\")\n}\n\n// after\nlet Some(changes) = change_request.worktree_changes else {\n    anyhow::bail!(\n        \"path '{}' has hunks but no worktree changes; recompute the change request\",\n        change_request.path.display()\n    );\n};","handlingStrategy":"validation","validationCode":"// reject inconsistent change requests before tree building\nfor req in &change_requests {\n    if !req.hunks.is_empty() && req.worktree_changes.is_none() {\n        anyhow::bail!(\n            \"path '{}' has hunks but no worktree changes; recompute the request\",\n            req.path.display()\n        );\n    }\n}","typeGuard":"fn has_aligned_worktree_state(req: &ChangeRequest) -> bool {\n    req.hunks.is_empty() || req.worktree_changes.is_some()\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["rust","but-core","hunks","commit-staging","internal-invariant","worktree"],"backgroundTag":"hunk-state-invariant-violated","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}