gitbutlerapp/gitbutler · error

no panic

Error message

no panic

What it means

get_branch_listing_details computes diffstats and merge bases on spawned worker threads (JoinHandle::join) and asserts the join itself cannot fail with `.expect("no panic")`. A join returns Err only if the worker thread panicked; this expect converts a worker panic into a panic on the caller thread. The subsequent `?` still propagates the computation's actual Result errors.

Solutions

  1. Inspect the underlying thread panic: join().unwrap_or_else(|e| e.downcast/payload) and surface the real message
  2. Replace expect with proper error propagation: `diffstats.join().map_err(|e| anyhow!("worker panicked: {e:?}"))??`
  3. Fix the root panic inside the worker closures so they return Results instead of panicking

Example fix

// before
diffstats.join().expect("no panic")?;
merge_bases.join().expect("no panic")?;
// after
diffstats
    .join()
    .map_err(|e| anyhow::anyhow!("diffstats worker panicked: {e:?}"))??;
merge_bases
    .join()
    .map_err(|e| anyhow::anyhow!("merge_bases worker panicked: {e:?}"))??;
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-validate repo state before heavy listing work:
repo.object_database()?.refresh()?; // surfaces corruption as an error, not a thread panic

Type guard

fn join_ok<T>(h: std::thread::JoinHandle<T>) -> Option<T> {
    h.ok()
}

Try / catch

// Map a worker panic to a normal error instead of re-panicking:
match diffstats.join() {
    Ok(result) => result?,
    Err(panic_payload) => return Err(anyhow::anyhow!("diffstats thread panicked: {panic_payload:?}")),
}

Prevention

When it happens

Trigger: Calling get_branch_listing_details when one of the spawned closure threads panics (e.g. an expect inside diffstat computation or merge-base resolution), making join() return Err.

Common situations: A repo state (corrupt object, unusual ref, huge diff) causes a panic inside the thread computing diffstats or merge bases; the surfaced error is 'no panic' rather than the underlying panic message.

Related errors


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

Appendix: source

Thrown at crates/gitbutler-branch-actions/src/branch.rs:870

                    change_tx.send(change.detach()).ok();
                    Ok(std::ops::ControlFlow::Continue(()))
                })?;
            let (number_of_files, lines_added, lines_removed) = rex_rx.recv()?;

            let branch_data = BranchListingDetails {
                name: branch.name,
                lines_added,
                lines_removed,
                number_of_files,
                authors: authors.into_iter().collect(),
                number_of_commits: num_commits,
                stack: branch.stack,
            };
            enriched_branches.push(branch_data);
        }
        (diffstats, merge_bases)
    };
    diffstats.join().expect("no panic")?;
    merge_bases.join().expect("no panic")?;
    Ok(enriched_branches)
}

/// Represents a fat struct with all the data associated with a branch
#[derive(Debug, Clone, Serialize, PartialEq)]
#[cfg_attr(feature = "export-schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct BranchListingDetails {
    /// The name of the branch (e.g. `main`, `feature/branch`), excluding the remote name
    #[cfg_attr(feature = "export-schema", schemars(with = "String"))]
    pub name: BranchIdentity,
    /// The number of lines added within the branch
    /// Since the virtual branch, local branch and the remote one can have different number of lines removed,
    /// the value from the virtual branch (if present) takes the highest precedence,
    /// followed by the local branch and then the remote branches (taking the max if there are multiple).
    /// If this branch has a virtual branch, lines_added does NOT include the uncommitted lines.
    pub lines_added: usize,

View on GitHub (pinned to 58e5313667)