gitbutlerapp/gitbutler · error

Failed to land onto

Error message

Failed to land {branch} onto {target_display}

What it means

This is the fallback message thrown by `branch_land` (crates/but-api/src/land/mod.rs:315) after the push/retry loop finishes without producing a `LandOutcome`. The loop normally exits via fast-forward/merge/already-integrated success or via a concrete push error, so this arm is an internal invariant violation: the landing machinery exhausted its attempts without recording a result or an error. It means GitButler could not determine any landing outcome for the branch onto the target ref.

Solutions

  1. Retry the `but land` command — the target may have stabilized since the race window.
  2. Fetch the target remote (`but pull` / `git fetch`) so the local tracking ref is current, then retry landing.
  3. Check the target branch for concurrent automation (CI merges, bots) and pause it while landing.
  4. If it reproduces consistently, report a bug — this path is documented as unreachable in practice (internal invariant violation).

Example fix

// before
but land feature-x --to main   // fails: Failed to land feature-x onto origin/main
// after
git fetch origin && but pull && but land feature-x --to main
Defensive patterns

Strategy: retry

Validate before calling

null

Type guard

null

Try / catch

// TypeScript (Tauri/SDK caller)
try {
  await api.branchLand({ branch, target });
} catch (e) {
  if (String(e).includes('Failed to land')) {
    await api.fetchTarget(); // refresh tracking refs, then retry once
    await api.branchLand({ branch, target });
  }
}

Prevention

When it happens

Trigger: Calling `branch_land` (or `but land <branch>`) when the internal push loop exits without setting `landed` — theoretically only reachable if every `MAX_PUSH_ATTEMPTS` attempts hit a retryable concurrency race and the guard above failed to fire, i.e. an unreachable-in-practice defensive bail.

Common situations: Hitting a persistent race where the target branch is being moved concurrently (e.g. CI or a teammate pushing to the same base branch during every retry); running an older/patched build where the retry-limit guard behaves differently; internal bugs in the land loop.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/64ac40f060718a6d. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-api/src/land/mod.rs:315

            {
                fetch_target_remote(ctx, &fetch_remote_name)?;
            }
            Err(err) if deliver::is_retryable_concurrency_error(&err) => {
                return Err(err.context(format!(
                    "Target branch kept moving; fetched and retried {MAX_PUSH_ATTEMPTS} times"
                )));
            }
            Err(err) if update_target_locally => {
                return Err(err.context("Failed to update local target branch"));
            }
            Err(err) => return Err(err.context("Failed to push to target branch")),
        }
    }

    let Some(landed) = landed else {
        // The loop only exits without a result when every attempt hit a retryable race; that path
        // already returned an error above, so this is unreachable in practice.
        bail!("Failed to land {branch} onto {target_display}");
    };

    // On the real-remote path, re-fetch so the tracking ref reflects the landed commit, then verify
    // it actually advanced before reconciling — otherwise the reconcile would be a silent no-op.
    if let BranchLandKind::Updated { new_target_oid, .. } = &landed
        && !update_target_locally
    {
        fetch_target_remote(ctx, &fetch_remote_name)?;

        // A concurrent push may have moved the tip *past* our commit, which still counts as the
        // target having advanced to include what we landed — so test reachability, not equality.
        let advanced = {
            let repo = ctx.repo.get()?;
            match peel_target_tip(&repo, &fetch_remote_name, &target_branch_name)? {
                Some(tip) => target_ref_contains(&repo, *new_target_oid, tip)?,
                None => false,
            }
        };

View on GitHub (pinned to 58e5313667)