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

  1. Run `but pull` (or `git pull --rebase` on the target branch) to resync local and remote, then retry the merge
  2. Push any local-only commits on the target branch with `git push` before landing
  3. 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

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


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)