{"record":{"id":"98493a72f0b5400b","repo":"gitui-org/gitui","slug":"no-branch","errorCode":null,"errorMessage":"No branch","messagePattern":"No branch","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/popups/checkout_option.rs","lineNumber":60,"sourceCode":"\t\t\trepo: env.repo.borrow().clone(),\n\t\t\tbranch: None,\n\t\t\toption: CheckoutOptions::KeepLocalChanges,\n\t\t\tvisible: false,\n\t\t\tkey_config: env.key_config.clone(),\n\t\t\ttheme: env.theme.clone(),\n\t\t}\n\t}\n\n\tfn get_text(&self, _width: u16) -> Vec<Line<'_>> {\n\t\tlet mut txt: Vec<Line> = Vec::with_capacity(10);\n\n\t\ttxt.push(Line::from(vec![\n\t\t\tSpan::styled(\n\t\t\t\tString::from(\"Switch to: \"),\n\t\t\t\tself.theme.text(true, false),\n\t\t\t),\n\t\t\tSpan::styled(\n\t\t\t\tself.branch.as_ref().expect(\"No branch\").name.clone(),\n\t\t\t\tself.theme.commit_hash(false),\n\t\t\t),\n\t\t]));\n\n\t\tlet (kind_name, kind_desc) = self.option.to_string_pair();\n\n\t\ttxt.push(Line::from(vec![\n\t\t\tSpan::styled(\n\t\t\t\tString::from(\"How: \"),\n\t\t\t\tself.theme.text(true, false),\n\t\t\t),\n\t\t\tSpan::styled(kind_name, self.theme.text(true, true)),\n\t\t\tSpan::styled(kind_desc, self.theme.text(true, false)),\n\t\t]));\n\n\t\ttxt\n\t}\n","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/gitui-org/gitui/blob/2fa693cb6ed431b21ebc300dd02e83c2476699ce/src/popups/checkout_option.rs#L42-L78","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)`"],"exampleFix":"// before\nSpan::styled(\n    self.branch.as_ref().expect(\"No branch\").name.clone(),\n    self.theme.commit_hash(false),\n),\n\n// after\nlet branch_name = self\n    .branch\n    .as_ref()\n    .map(|b| b.name.clone())\n    .unwrap_or_else(|| String::from(\"no branch selected\"));\nSpan::styled(branch_name, self.theme.commit_hash(false)),","handlingStrategy":"type-guard","validationCode":"// Before showing or drawing the popup, verify it holds a branch:\nif popup_branch_name(&popup).is_none() {\n    log::warn!(\"checkout popup has no branch selected; not drawing\");\n    return Ok(()); // or popup.hide();\n}","typeGuard":"fn popup_branch_name(p: &CheckoutOptionPopup) -> Option<&str> {\n    p.branch.as_ref().map(|b| b.name.as_str())\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["rust","panic","expect","option","ratatui","tui","invariant-violation","git"],"backgroundTag":"unwrap-on-none-panic","analyzedSha":"2fa693cb6ed431b21ebc300dd02e83c2476699ce","analyzedAt":"2026-08-16T23:07:36.562Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}