At commit 2c87c82, cli/cli has 3,152 analyzed functions, 578 of which score in the critical band — and every one of the top five hotspots sits in the fire quadrant, meaning structurally complex code that is actively changing right now. The highest-scoring function, Main in internal/ghcmd/cmd.go, carries an activity-weighted risk score of 19.43 with a cyclomatic complexity of 19, a maximum nesting depth of 8, and a fan-out of 68 distinct callees — last changed 13 days ago. I would start there, then work down through createRun, ExtBrowse, publishRun, and forkRun, each of which combines double-digit branching complexity with broad coupling and recent commit activity that makes every change a live regression surface.
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 |
|---|---|---|---|---|---|
Main | internal/ghcmd/cmd.go | 19.4 | 19 | 8 | 68 |
createRun | pkg/cmd/release/create/create.go | 17.0 | 23 | 5 | 41 |
ExtBrowse | pkg/cmd/extension/browse/browse.go | 16.7 | 26 | 4 | 87 |
publishRun | pkg/cmd/skills/publish/publish.go | 16.7 | 10 | 6 | 41 |
forkRun | pkg/cmd/repo/fork/fork.go | 16.4 | 12 | 5 | 58 |
Large Repo Analysis
cli is a large repository. To stay within memory constraints, this analysis used hybrid touch mode: structural complexity — CC, ND, FO — is measured precisely for every function. Git activity is tracked at the function level (via git log -L) only for files with 5 or more commits in the last 30 days; other files use a file-level approximation. Rankings therefore surface functions that are both structurally complex and in the most actively-changing parts of the codebase. Dormant code with high structural complexity will rank lower than it would under a full per-function analysis — to surface it, run hotspots analyze . --per-function-touches on a machine with sufficient memory.
Repository overview
Of cli/cli’s 3,152 functions analyzed at commit 2c87c82, 578 land in the critical band and another 864 in high. The quadrant picture is dominated by fire: 1,441 functions are both structurally complex and actively changing. Only one function sits in the debt quadrant — chooseVerifier in pkg/cmd/attestation/verification/sigstore.go, which hasn’t been touched in 43 days and carries its risk as dormant structural debt rather than live churn.
3,152 functions analyzed
Across the five top hotspots, five antipattern types repeat. Every function is flagged as a god function and a long function, all five show complex branching, four show deep nesting, and six exit-heavy instances appear across the set. That last pattern — multiple early-return paths scattered through a single function body — means each function demands a disproportionately large test matrix to achieve meaningful coverage.
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.Complex Branching×5Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.God Function×5God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Long Function×5Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Deeply Nested×4Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
Main — cmd.go
Main is the process entry point for the gh binary. Looking at the source excerpt, it bootstraps configuration, selects an I/O stream strategy, assembles telemetry dimensions, then branches across three telemetry states — disabled, logged, and enabled — each of which has its own sub-conditions (GHES detection, sample-rate parsing from an environment variable, flusher selection). That nested switch-within-switch structure is exactly what drives the nesting depth to 8 and the cyclomatic complexity to 19.
The fan-out of 68 is the most consequential number here. Main reaches into configuration, I/O streams, telemetry, CI detection, agent detection, executable path resolution, and more — all in a single function body. Any change to one of those 68 dependencies has a plausible path through this function, and the 8-level nesting means reasoning about those paths requires holding a large mental stack. The exit_heavy flag reinforces this: the function returns exitError in at least one telemetry-configuration branch visible in the excerpt, meaning the exit paths are not consolidated.
This function was touched 1 time in the last 30 days and last changed 13 days ago, placing it in the fire quadrant as an active regression surface. The external signals show that the most recent recorded change to this file was classified as a bug fix — thin historical data, based on a single observed commit, but worth noting.
Recommendation: Extract the telemetry initialization block — the nested switch on telemetryState plus the mightBeGHESUser check — into a dedicated initTelemetryService function that accepts config and I/O streams and returns a ghtelemetry.Service. That single extraction removes roughly half the branching paths from Main and makes the telemetry decision tree independently testable.
createRun — create.go
createRun orchestrates the full lifecycle of creating a GitHub release from the CLI. The source excerpt shows the function handling at least five distinct responsibility clusters before it even reaches the network call to create the release: it validates whether there are new commits since the last release, resolves the tag name interactively or from flags, verifies the tag exists remotely when requested, reads local git tag annotations, and manages the existingTag state variable that threads through subsequent decisions.
A cyclomatic complexity of 23 means there are 23 independent execution paths through this function — each one a required test case if you want to achieve full branch coverage. The nesting depth of 5 is moderate but combines with the branching to make the state transitions between existingTag, opts.TagName, opts.VerifyTag, and opts.NotesFromTag hard to trace. Fan-out of 41 means changes to upstream helpers — tag fetchers, prompters, git clients, repo resolvers — all have a direct line into this function’s behavior.
With 1 touch in the last 30 days and a last-change 13 days ago, this is live territory. The exit_heavy and god_function flags confirm what the excerpt shows: multiple early returns from error conditions scattered across the function body, with no single exit-path consolidation.
Recommendation: Split createRun along its natural seams. The tag-resolution block (prompt for existing tag, prompt for new tag name, verify remote tag) is a coherent sub-operation that can become a resolveTagName function returning (tagName string, existingTag bool, err error). That alone would drop the CC of the remaining createRun by roughly a third and give the tag-resolution logic its own test surface.
ExtBrowse — browse.go
ExtBrowse implements the interactive TUI for browsing and managing gh extensions. The source excerpt shows it constructing an entire tview application inline: creating the application instance, building the layout hierarchy (outer flex, inner flex, header, filter field, list, readme pane, help bar, pages), wiring event handlers, and launching the app — all within one function body. The loadSelectedReadme closure defined inline spawns a goroutine (go loadSelectedReadme()) to fetch readme content asynchronously, which introduces a concurrency surface that structural metrics alone don’t fully capture.
The fan-out of 87 is the highest in the top five and reflects the breadth of this function’s reach: tview layout primitives, tcell color constants, extension list management, readme fetching, progress indicators, and the goroutine-based update queue. At CC 26, this is the most branchy function in the set. The combination makes ExtBrowse the hardest function in the set to reason about when something goes wrong — a bug in the readme pane scroll behavior, for instance, could originate in any of dozens of callees.
In Go, the inline goroutine inside loadSelectedReadme that calls app.QueueUpdateDraw is a classic pattern for tview concurrency, but it also means there is a data-race surface between the goroutine and the main event loop that only manifests under specific timing conditions — the kind of thing CC and ND metrics don’t measure.
Recommendation: Extract the TUI construction into a buildExtBrowseUI function that returns the assembled component graph, and move the event-handler wiring (filter changes, key captures, page navigation) into separate wireExtBrowseHandlers functions. This separates the layout concern from the behavior concern and brings ExtBrowse down from a god function to a coordinator.
publishRun — publish.go
publishRun is the most structurally unusual entry in this list. Its cyclomatic complexity of 10 is the lowest of the five — moderate by most standards — but its nesting depth of 6 and fan-out of 41 push the activity-weighted risk score to 16.65. The source excerpt explains why: the function iterates over discovered skills, and for each skill it reads a SKILL.md file, parses its frontmatter, then runs a nested series of validation checks (name presence, name-to-directory match, spec compliance). That per-skill validation loop with its accumulating diagnostics slice is the source of the depth-6 nesting — a for loop containing if/else chains containing further conditional validation blocks.
The path pkg/cmd/skills/publish/publish.go suggests this is part of a newer or evolving feature area — GitHub Skills publishing isn’t one of the original gh verbs. The external signals are notably different from the other four functions: two total commits, two distinct authors in the last 90 days, and a review-comment density of 2.0 per pull request. That review comment density is the only non-zero value for this metric across all five hotspots, indicating that this function has already attracted reviewer attention — a reasonable signal that the current structure was debated during review.
The deeply_nested and exit_heavy flags reflect what the excerpt confirms: early continue statements inside the skill loop and nested if/else validation trees that accumulate complexity at the nesting level even when individual branch counts stay moderate.
Recommendation: Extract the per-skill validation logic into a validateSkill(skill, dirName string, content []byte) []publishDiagnostic function. This flattens the nesting in publishRun by one full level, eliminates the need to track dirName and skillPath inside the outer loop, and makes the validation rules independently testable — which matters because those rules (name format, directory match, spec compliance) are exactly the kind of thing that grows over time.
forkRun — fork.go
forkRun handles the gh repo fork command end-to-end. The source excerpt shows a clear three-phase structure: repository argument resolution (detecting whether the argument is a URL, a git SSH string, or a plain owner/repo name), the API call to fork the repository, and post-fork state management (detecting whether the fork already existed by comparing creation timestamps, then optionally cloning or setting up remotes). The argument-resolution phase alone has three branches with different URL parsing strategies, each with its own error return paths.
A fan-out of 58 reflects how much coordination forkRun does: URL parsers, git SSH parsers, repo name parsers, the HTTP client, the API fork call, progress indicators, color schemes, I/O TTY detection, and the downstream clone/remote-setup logic that the excerpt hints at. Cyclomatic complexity of 12 and nesting depth of 5 are consistent with a function that has grown feature by feature — each new option (organization forks, fork naming, default-branch-only) adding another conditional branch without restructuring the existing logic.
The source comment visible in the excerpt is itself a complexity signal: the team has documented an API quirk (fork creation returns the existing fork with no status-code change) inline in the function body, which means the workaround logic — comparing CreatedAt timestamps — is tightly coupled to forkRun rather than encapsulated in the API layer.
Recommendation: Move the repository-argument resolution into a resolveRepoToFork(opts *ForkOptions) (ghrepo.Interface, bool, error) function — the boolean indicating whether the caller is inside the parent repo. This removes the three-way URL/SSH/name dispatch from forkRun’s main body and gives the resolution logic its own test cases covering each input format independently.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 6 |
complex_branching | 5 |
god_function | 5 |
long_function | 5 |
deeply_nested | 4 |
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.
Reproduce This Analysis
git clone https://github.com/cli/cli
cd cli
git checkout 2c87c82fa2484f26c7181cbf31be406c943fdaaf
hotspots analyze . --mode snapshot --explain-patterns --force --hybrid-touches 5
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 →