The highest activity-weighted risk score in glow is 16.03, belonging to Update in ui/ui.go, and it sits in the debt quadrant: cyclomatic complexity of 58, fan-out of 23, and 52 days since last change. That makes it structural debt with a large blast radius when next touched, not a live regression risk. Of the 145 functions I analyzed in glow at commit 8b0600f, 33 land in the critical band. Four of the five functions I would review first are debt-quadrant message and argument handlers; the fifth, executeCLI in main.go, is the only one in the fire quadrant.
The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).
Top 5 Hotspots
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
Update | ui/ui.go | 16.0 | 58 | 4 | 23 |
handleDocumentBrowsing | ui/stash.go | 15.4 | 58 | 3 | 28 |
sourceFromArg | main.go | 14.7 | 29 | 4 | 15 |
update | ui/pager.go | 14.6 | 35 | 3 | 25 |
executeCLI | main.go | 14.4 | 29 | 2 | 29 |
145 functions analyzed
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.God Function×6God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Long Function×3Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Complex Branching×2Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Stale Complex×1Stale Complex
High structural complexity but untouched for a long time — structural debt that will bite whoever opens it next.
The quadrant split tells me how to read this repository. Of 145 functions, 41 are debt and 21 are fire. The top five are not a uniform group: four have not been touched in the last year, and one was touched once, 10 days ago. I would treat them as two kinds of work: a planned refactor for the debt functions, and a close-review habit for the one function that is both complex and moving.
One caveat before the details. High activity-weighted risk means structurally complex and, to varying degrees, recently active. It does not mean the code is buggy, and nothing here is a judgement on the maintainers. A Bubble Tea Update function that switches on message and key types will naturally accumulate branches. The question is whether the branching has outgrown what can be tested and reasoned about comfortably.
Update — ui.go
I would start with Update. It has the highest activity-weighted risk score in the repository (16.03) and the deepest nesting in the top five (4), on top of a cyclomatic complexity of 58.
From the source excerpt, this is the root model’s message handler. It opens with a fatal-error short circuit, then switches on message type, with a nested switch on key strings inside the key-press case (escape, r, q, left/h/delete, ctrl+z, ctrl+c) and further cases for window size, local file search, fetched markdown, and rendered content. The metrics fit that shape: a type switch containing a key switch containing state checks, with many early return m, tea.Quit and return m, tea.Batch(...) exits. That is the exit-heavy pattern, and every distinct return is a path someone has to cover in a test. Fan-out of 23 means it also reaches into the stash model, the pager model, styles, file finding, and Glamour rendering. That is the god-function pattern: a change to any of those collaborators can ripple back here.
It was last changed 52 days ago and has zero touches in the last year, so this is overdue-for-refactoring territory rather than an active churn problem. The file-level history is mild: 7 commits total, no bug-linked commits, no reverts, a bug-fix rate of 43%, and one distinct author in the last 90 days. I read that as maintainability debt, not a defect signal. I would extract the key-press branch into its own method and move per-message-type handling into small functions (window size, local file search, markdown fetched, content rendered), leaving Update as a dispatcher.
handleDocumentBrowsing — stash.go
handleDocumentBrowsing matches Update on cyclomatic complexity (58) and has a fan-out of 28, second only to executeCLI among the five, with an activity-weighted risk score of 15.4. It last changed 52 days ago, so it shares Update’s status: structural debt, not an active hotspot.
The excerpt shows a long key-press switch: cursor movement (k/j and their ctrl and arrow variants), jump to start and end (home/g, end/G), clearing a filter, next and previous section (tab, shift+tab, L, H), a file re-find on F, opening an editor on e, and opening a document on enter. Several branches repeat the same guard (no sections, or the filter is in the filtering state) and the same wrap-around index arithmetic for sections, which makes a good extraction candidate. The function is flagged as exit-heavy, god-function and long-function, and the long-function flag is the one I would act on first. Extract-method into one handler per key group (navigation, section switching, document opening) would cut the metric substantially without changing behavior.
The file’s history shows a bug-fix rate of 80% over 5 commits, with no bug-linked commits and no reverts, and 2 distinct authors in the last 90 days. That is a small sample, so I take it as a nudge to review this one with a little more care than Update, not as evidence the function is defective.
sourceFromArg — main.go
sourceFromArg is the stalest of the five. It hasn’t been changed in 546 days, which is why it carries the stale-complex pattern alongside complex branching, exit-heavy and god-function. Its cyclomatic complexity is 29, with nesting depth 4 and fan-out of 15.
The excerpt shows why. It resolves a command-line argument into a markdown source by trying a sequence of strategies: stdin when the argument is a dash, a GitHub or GitLab README URL, a generic HTTP(S) URL with a protocol check and status check, a directory walked for README-style filenames, and finally a plain file open with an absolute-path conversion. Go’s explicit error handling shows up as many separate error returns (unsupported protocol, failed GET, bad status, missing markdown source, failed open, failed absolute path), and each one is a path to test. The directory-walk branch also uses a closure with its own error-to-abort convention, which adds nesting depth that cyclomatic complexity alone does not capture.
Because the function is dormant, the risk is blast radius when someone next needs to add an input type. The file has 10 commits, a bug-fix rate of 60%, no bug-linked commits and no reverts, and a pull-request review comment density of 3.1, the highest in the top five. I would split it into one resolver per input kind (stdin, URL, directory, file) behind a small ordered list, so adding a new kind does not mean editing a 29-path function.
update — pager.go
The pager’s update mirrors the root Update at a smaller scale: cyclomatic complexity of 35, nesting depth of 3, fan-out of 25, and 52 days since last change. It is a debt-quadrant function with an activity-weighted risk score of 14.59.
The excerpt shows a key switch (quit or escape back to browse state, top and bottom, half-page up and down, open editor at an approximate line, copy to clipboard, reload, toggle help) followed by cases for rendered content, reload, editor finished, window size, and status-message timeout, ending with a pass-through to the viewport’s own update. Two branches stand out as worth isolating. The editor branch computes a line number from scroll percentage and logs it, and the copy branch writes through two clipboard mechanisms. Both are side-effecting logic sitting inside a dispatcher, and pulling each into a named method would make the key switch read as a table.
History is mild: 8 commits at file level, a bug-fix rate of 50%, no bug-linked commits, no reverts, and one distinct author in the last 90 days.
executeCLI — main.go
executeCLI is the only fire-quadrant function in the top five, and that changes the argument. It was touched once in the last year, 10 days ago, so it is both complex and recently changed, with an activity-weighted risk score of 14.4. Nesting depth is a shallow 2, but its fan-out of 29 is the highest in the top five, just above handleDocumentBrowsing’s 28.
The excerpt shows a function that reads the full source, strips frontmatter, derives a base URL, decides whether the input is code or markdown, builds a Glamour renderer with style, word wrap and base URL options, optionally wraps code in a fenced block, renders, applies text sizing, and then branches into three display modes: external pager (parsing the pager command from the environment), the TUI, or a plain write to the output writer. That is read, transform, render and display in one function, and each stage has its own error return, which accounts for the exit-heavy flag.
The recent change lines up with the text-sizing work visible elsewhere in the data: the ApplyTextSizing call here sits alongside other functions in utils/textsize.go that also show one touch 10 days ago. I would put the review focus on the seam between executeCLI and the text-sizing helpers, since new behavior is landing in a function that already couples to 29 others. The first refactor is easy to name: separate render (everything up to ApplyTextSizing) from display (the three-way switch), so each can be tested without the other. File-level history shows 10 commits, a bug-fix rate of 60%, no bug-linked commits and no reverts, and one distinct author in the last 90 days.
What sits just below the top five
The next group shows where activity is concentrated. Several fire-quadrant functions in utils/textsize.go and ui/pager.go each show one touch 10 days ago, which reads as one feature under active development rather than scattered churn.
What I would do first
If I had a single sprint, I would extract the key-press branches from Update, handleDocumentBrowsing and update into named handlers. Those three account for the biggest share of structural risk and none is changing right now, which makes this a safe time to refactor. Separately, I would add targeted tests around executeCLI and the text-sizing code while that feature is still moving.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 9 |
god_function | 6 |
long_function | 3 |
complex_branching | 2 |
stale_complex | 1 |
These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.
See more analyses with these patterns: complex_branching, exit_heavy, god_function, long_function, stale_complex.
Reproduce This Analysis
git clone https://github.com/charmbracelet/glow
cd glow
git checkout 8b0600fa445535b9c53194c0d4f7fe3cf700ef3e
hotspots analyze . --mode snapshot --explain-patterns --force
To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.
I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →