gitui-org/gitui · error
No branch
Error message
No branch
What it means
CheckoutOptionPopup keeps the target branch in `self.branch: Option<BranchInfo>` (initialized to None in `new`, src/popups/checkout_option.rs:43) and only ever fills it in `open()` (line 80-86). `get_text()` renders the 'Switch to: <branch>' line by calling `self.branch.as_ref().expect("No branch")` at line 60, so if the popup is ever drawn while visible but `open(branch)` has not run, this `expect` panics and takes down the whole TUI process. It is an unstated invariant 'visible implies branch is Some' enforced by panic instead of by the type system.
Solutions
- Make the renderer total: replace the `expect` with a graceful fallback (render 'no branch selected' or skip the line) so a missing branch degrades to UI text instead of a crash
- Enforce the invariant where it is created: set `self.branch = Some(branch)` before `self.show()` in `open()`, and make `show()` unreachable without a branch (or store branch+visibility together)
- Add `debug_assert!(self.branch.is_some() || !self.visible)` in draw to catch invariant breaks in dev builds while shipping the graceful fallback
- Audit callers: grep for `.show()`/visibility manipulation on CheckoutOptionPopup and ensure every path goes through `open(branch)`
Example fix
// before
Span::styled(
self.branch.as_ref().expect("No branch").name.clone(),
self.theme.commit_hash(false),
),
// after
let branch_name = self
.branch
.as_ref()
.map(|b| b.name.clone())
.unwrap_or_else(|| String::from("no branch selected"));
Span::styled(branch_name, self.theme.commit_hash(false)), Defensive patterns
Strategy: type-guard
Validate before calling
// Before showing or drawing the popup, verify it holds a branch:
if popup_branch_name(&popup).is_none() {
log::warn!("checkout popup has no branch selected; not drawing");
return Ok(()); // or popup.hide();
} Type guard
fn popup_branch_name(p: &CheckoutOptionPopup) -> Option<&str> {
p.branch.as_ref().map(|b| b.name.as_str())
} Prevention
- Treat Option fields in UI components as render-time data that must always be checked, never expect-unwrapped
- Set required data before flipping visibility flags in open()-style methods so show() and Some(data) cannot diverge
- Write a unit test that draws every popup freshly constructed (branch: None) to catch panics in render paths
- Prefer making illegal states unrepresentable: store `branch: BranchInfo` and construct the popup per-open instead of keeping a long-lived Option
When it happens
Trigger: Component::draw() running while `visible == true` but `open(branch)` was never called (e.g. a state-restore path, test harness, or refactored caller that invokes `show()` directly); the window between `self.show()?` at line 81 and `self.branch = Some(branch)` at line 83 if a draw could ever interleave; any future code path that unhides the popup generically after it was constructed with `branch: None`. Note `checkout()` (line 89) already handles the None case gracefully — only the render path panics.
Common situations: Adding a new entry point that shows the checkout-options popup without a selected branch (command palette, key-replay, integration tests that instantiate components and force-draw them); refactoring `open()` so `show()` happens before the branch is stored; persisting/restoring popup visibility across tab switches.
Related errors
AI-assisted analysis of gitui-org/gitui@2fa693cb6e (2026-08-16).
Data as JSON: /api/errors/98493a72f0b5400b.
Report an issue: GitHub.
Appendix: source
Thrown at src/popups/checkout_option.rs:60
repo: env.repo.borrow().clone(),
branch: None,
option: CheckoutOptions::KeepLocalChanges,
visible: false,
key_config: env.key_config.clone(),
theme: env.theme.clone(),
}
}
fn get_text(&self, _width: u16) -> Vec<Line<'_>> {
let mut txt: Vec<Line> = Vec::with_capacity(10);
txt.push(Line::from(vec![
Span::styled(
String::from("Switch to: "),
self.theme.text(true, false),
),
Span::styled(
self.branch.as_ref().expect("No branch").name.clone(),
self.theme.commit_hash(false),
),
]));
let (kind_name, kind_desc) = self.option.to_string_pair();
txt.push(Line::from(vec![
Span::styled(
String::from("How: "),
self.theme.text(true, false),
),
Span::styled(kind_name, self.theme.text(true, true)),
Span::styled(kind_desc, self.theme.text(true, false)),
]));
txt
}
View on GitHub (pinned to 2fa693cb6e)