Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fix "Specified HEAD didn't match actual HEAD^{tree}" error on branch apply#13735

Draft
mtsgrd wants to merge 1 commit into
masterfrom
head-didnt-match-head
Draft

Fix "Specified HEAD didn't match actual HEAD^{tree}" error on branch apply#13735
mtsgrd wants to merge 1 commit into
masterfrom
head-didnt-match-head

Conversation

@mtsgrd
Copy link
Copy Markdown
Contributor

@mtsgrd mtsgrd commented May 9, 2026

Users are hitting this error when applying a branch. It's caused by a race condition: apply() reads prev_head_id from the workspace graph and passes it to safe_checkout, but between those two calls the file watcher can run update_workspace_commit, which moves the gitbutler/workspace ref to a new commit with a different tree.

Inside merge_worktree_changes_into_destination_or_keep_snapshot in checkout/utils.rs, the caller's stale source_tree_id was compared against repo.head_tree_id_or_empty() and the mismatch triggered a bail!().

The fix removes that assertion and always reads the tree from repo.head_tree_id_or_empty(). This is correct because worktree diffs (via the git index) are always relative to the actual HEAD tree, not whatever the caller cached — so the snapshot base must match.

Includes regression test: checkout_succeeds_when_source_tree_diverges_from_head

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a race-condition failure when applying a branch by making worktree-change snapshotting use the repository’s actual HEAD^{tree} (instead of a potentially stale caller-provided tree), preventing the "Specified HEAD didn't match actual HEAD^{tree}" abort.

Changes:

  • Removed the strict “caller-provided source tree must match actual HEAD tree” assertion and instead always base snapshots on repo.head_tree_id_or_empty().
  • Marked the source_tree_id parameter as unused (_source_tree_id) to reflect the updated behavior.
  • Added a regression test covering checkout success when the caller’s source tree diverges from actual HEAD.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
crates/but-core/src/worktree/checkout/utils.rs Stops bailing on HEAD-tree mismatch; always uses actual HEAD tree as snapshot base for consistency with worktree diffs.
crates/but-core/tests/core/worktree/checkout.rs Adds regression test reproducing the stale-source-tree scenario and asserting checkout succeeds while preserving worktree changes.

Comment on lines +79 to +83
// Worktree changes are always relative to the actual HEAD tree (via the index),
// so the snapshot must use it as its base. If the caller's source tree diverges
// (e.g. due to a concurrent workspace commit update), we use the actual HEAD tree
// to keep the snapshot consistent with the worktree diff.
let source_tree_id = actual_head_tree_id.detach();
@mtsgrd mtsgrd changed the base branch from guard-undefined-branch-statuses to master May 9, 2026 20:13
@mtsgrd mtsgrd force-pushed the head-didnt-match-head branch from 8f138e6 to 1cabea6 Compare May 9, 2026 20:13
@mtsgrd mtsgrd marked this pull request as draft May 9, 2026 21:05
@mtsgrd mtsgrd force-pushed the head-didnt-match-head branch from 1cabea6 to cd0089c Compare May 10, 2026 12:27
…apply

Users are hitting this error when applying a branch. It's
caused by a race condition: `apply()` reads `prev_head_id` from the
workspace graph and passes it to `safe_checkout`, but between those two
calls the file watcher can run `update_workspace_commit`, which moves
the `gitbutler/workspace` ref to a new commit with a different tree.

Inside `merge_worktree_changes_into_destination_or_keep_snapshot`, the
caller's stale `source_tree_id` was compared against
`repo.head_tree_id_or_empty()` and the mismatch triggered a `bail!()`.

The fix removes that assertion and always reads the tree from
`repo.head_tree_id_or_empty()`. This is correct because worktree diffs
(via the git index) are always relative to the actual HEAD tree, not
whatever the caller cached — so the snapshot base must match.

Includes regression test:
`checkout_succeeds_when_source_tree_diverges_from_head`

Remove unused source_tree_id parameter

Remove the now-unused source_tree_id parameter from
merge_worktree_changes_into_destination_or_keep_snapshot and its
callers. The parameter was unused in the function body and
documentation, so eliminating it cleans up the function signature,
related docs, and the call site in function.rs to avoid passing an
unnecessary value. This simplifies the API and reduces confusion about
the expected inputs.
@mtsgrd mtsgrd force-pushed the head-didnt-match-head branch from cd0089c to fa63bcd Compare May 10, 2026 13:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants