gitbutlerapp/gitbutler · error
Local ` ` ( ) is out of sync with ` / ` ( ). Run `but pull`…
Error message
Local `{target_branch_name}` ({head}) is out of sync with `{push_remote_name}/{target_branch_name}` ({tracking}). Run `but pull` to resync first. What it means
Before landing, the library advances both the local target branch and its remote tracking ref in one transaction. If the local branch and `{push_remote}/{branch}` have diverged (different peeled IDs), landing would clobber or lose commits, so it refuses with a non-retryable error telling the user to resync with `but pull`.
Solutions
- Run `but pull` (or `git pull --rebase` on the target branch) to resync local and remote, then retry the merge
- Push any local-only commits on the target branch with `git push` before landing
- If the remote history was rewritten, realign with `git fetch` and reset/merge the local branch appropriately
Example fix
# before but merge feature-branch # error: local main out of sync with origin/main # after but pull but merge feature-branch
Defensive patterns
Strategy: try-catch
Validate before calling
let head = repo.revparse_single(format!("refs/heads/{target}").as_str())?.peel_to_id()?;
let tracking = repo.revparse_single(format!("refs/remotes/{remote}/{target}").as_str())?.peel_to_id()?;
if head != tracking { println!("run `but pull` first"); return Ok(()); } Try / catch
match land_result {
Err(e) if e.to_string().contains("out of sync with") => {
run("but pull");
retry_land();
}
other => other?,
} Prevention
- Always `but pull` / fetch before landing, especially in multi-machine setups
- Avoid committing directly on the target branch; keep target branch push-only via settings
- Check `git status -sb` ahead/behind markers before running `but merge`
When it happens
Trigger: Running `but merge`/land while the local target branch HEAD differs from the peeled remote tracking ref — i.e. local commits exist that were never pushed, or the tracking ref is stale/ahead.
Common situations: Developer committed directly on the target branch locally without pushing; another machine pushed and this clone never fetched+pulled; a force-push happened on the remote.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- `but merge` does not yet support triangular remotes
- Cannot merge : it shares no history with /
- Cannot merge : merging into / resulted in conflicts. Rebase…
- Aborting due to empty branch name
- Ad-hoc (single-branch) branch moves are not supported…
AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18).
Data as JSON: /api/errors/34699c709fa652e1.
Report an issue: GitHub.
Appendix: source
Thrown at crates/but-api/src/land/deliver.rs:33
pub(super) fn update_local_target_refs(
repo: &gix::Repository,
new_target_oid: gix::ObjectId,
expected_target_oid: gix::ObjectId,
push_remote_name: &str,
target_branch_name: &str,
) -> anyhow::Result<()> {
let head_ref = format!("refs/heads/{target_branch_name}");
let tracking_ref = format!("refs/remotes/{push_remote_name}/{target_branch_name}");
// The two refs must already point at the same commit. If local `<target>` has diverged from
// `<remote>/<target>`, the shared compare-and-swap below can never succeed (the head edit keeps
// failing), so surface that as a clear, non-retryable error rather than looping on it.
if let (Some(head), Some(tracking)) = (
super::peel_ref(repo, &head_ref)?,
super::peel_ref(repo, &tracking_ref)?,
) && head != tracking
{
bail!(
"Local `{target_branch_name}` ({head}) is out of sync with \
`{push_remote_name}/{target_branch_name}` ({tracking}). Run `but pull` to resync first."
);
}
// Advance both refs to the landed commit in one transaction, failing if either moved meanwhile.
let expected = PreviousValue::ExistingMustMatch(expected_target_oid.into());
let ref_log_message = "GitButler land";
let edits = [
RefEdit::update(
head_ref.try_into()?,
new_target_oid,
expected.clone(),
ref_log_message,
),
RefEdit::update(
tracking_ref.try_into()?,
new_target_oid,View on GitHub (pinned to 58e5313667)