gitbutlerapp/gitbutler · error · anyhow::Error

Could not restore native GitHub stack membership after the p

Error message

Could not restore native GitHub stack membership after the push failed:
{}

What it means

During review updates GitButler first dissolves native GitHub 'stack' membership, pushes, then re-registers stacks via `restore_native_stacks()`. Each `but_github::stacks::create()` failure is collected (formatted with `{err:#}` to flatten context chains), and if any failed the aggregate bail fires after the push already failed — so the repo is back on old refs but GitHub's stack graph may still be dissolved. The inner lines name each failing stack's PR list and cause.

Source

Thrown at crates/but-forge/src/review.rs:2511

    preferred_forge_user: &Option<crate::ForgeUser>,
    forge_repo_info: &crate::forge::ForgeRepoInfo,
    stacks: &[Vec<i64>],
    storage: &but_forge_storage::Controller,
) -> Result<()> {
    let crate::forge::ForgeRepoInfo { owner, repo, .. } = forge_repo_info;
    let preferred_account = preferred_forge_user.as_ref().and_then(|user| user.github());
    let mut errors = Vec::new();
    for pull_requests in stacks {
        if let Err(err) =
            but_github::stacks::create(preferred_account, owner, repo, pull_requests, storage).await
        {
            errors.push(format!("{err:#}"));
        }
    }
    if errors.is_empty() {
        Ok(())
    } else {
        anyhow::bail!(
            "Could not restore native GitHub stack membership after the push failed:\n{}",
            errors.join("\n")
        )
    }
}

async fn github_review_bodies(
    preferred_account: Option<&but_github::GithubAccountIdentifier>,
    owner: &str,
    repo: &str,
    reviews: &[ForgeReviewUpdate],
    storage: &but_forge_storage::Controller,
) -> Result<(Vec<Option<Option<String>>>, Vec<String>)> {
    let mut bodies = Vec::with_capacity(reviews.len());
    let mut errors = Vec::new();
    for review in reviews {
        if review.update_description {
            bodies.push(Some(review.body.clone()));

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Read the inner error lines: 401/403 points to auth (re-authenticate the GitHub account in GitButler), 403 rate-limit says to wait; other codes name the stack/PRs involved.
  2. Re-run the push/review update once auth and network are healthy — successful subsequent updates recreate correct stack membership.
  3. If GitHub still shows dissolved stacks, manually re-stack via the PR UI or `but` forge tooling once all member PRs are open.
  4. Verify no member PR was closed/merged mid-operation; GitHub refuses stacks containing closed PRs (a restorable stack needs >= 2 open members).
Defensive patterns

Strategy: try-catch

Try / catch

// After the push already failed, a stack-restore failure is best-effort:
// log it, keep the original push error primary, and surface a follow-up action.
match restore_native_stacks(&user, &info, &stacks, &storage).await {
    Ok(()) => {}
    Err(restore_err) => {
        tracing::warn!("{restore_err:#}");
        notify_user("GitHub stack membership may be dissolved; re-run sync once re-authenticated to repair");
        // do NOT mask the original push failure with restore_err
    }
}

Prevention

When it happens

Trigger: A push failure while updating reviews, followed by a rollback where re-creating GitHub stack membership fails too — expired OAuth token, rate limiting, network drop, or GitHub refusing a stack that now contains a closed PR.

Common situations: Token revoked or expired mid-session; GitHub API secondary rate limits during rapid push/retry cycles; one PR in the stack closed/merged while the push was in flight; flaky connectivity.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/0e9f6126332f46c1. Report an issue: GitHub.