The dominant risk in pcottle/learnGitBranching isn’t active churn — it’s structural debt sitting dormant. initDemo in src/js/app/index.js tops the list with an activity-weighted risk score of 13.08, hasn’t been touched in 110 days, and carries the god_function and deeply_nested antipatterns that make any future modification high-stakes. Across 815 total functions, 7 are rated critical and all 45 non-trivial risk functions fall into the debt quadrant — zero are in the fire quadrant. I would start with initDemo and levelSolved precisely because they sit at the application’s entry and completion boundaries: when they do get touched, the blast radius will be wide.
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 |
|---|---|---|---|---|---|
initDemo | src/js/app/index.js | 13.1 | 19 | 7 | 14 |
levelSolved | src/js/level/index.js | 11.7 | 18 | 2 | 15 |
getType | src/js/git/index.js | 9.7 | 7 | 4 | — |
getType | src/js/graph/index.js | 9.7 | 7 | 4 | — |
main | scripts/translate.js | 9.6 | 12 | 5 | 8 |
Large Repo Analysis
learnGitBranching 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.
Quadrant and Pattern Overview
815 functions analyzed
Every flagged function in this repo is in the debt quadrant. There are no fire-quadrant functions: nothing complex is being actively modified right now. That sounds reassuring, but it’s the wrong conclusion to draw. Debt-quadrant functions are the ones that hurt you on the day you finally need to change them — and the ones at the top of this list will hurt a lot.
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.Complex Branching×2Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Deeply Nested×2Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.God Function×2God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Long Function×2Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.
The antipattern profile reinforces the debt story: god functions with broad coupling, deeply nested conditional chains, and multiple exit paths are the recurring themes. These patterns compound each other — a function that is both a god function and exit-heavy is hard to test, hard to reason about, and hard to split safely.
Top 5 Hotspots
initDemo — app/index.js
initDemo is the application’s front door. Looking at the source excerpt, it parses query string parameters and dispatches the app into one of at least six distinct demo or level modes — demo, hgdemo, hgdemo2, remoteDemo, level, and a Gist-backed path that fires an asynchronous AJAX call. Each branch populates a commands array with a different sequence of simulated git operations, and at least two of those branches apply a string-join/split trick (commented inline as “hax”) to work around delimiter conflicts.
A cyclomatic complexity of 19 means there are 19 independent execution paths through this function — each one a required test case if you want confidence that a change hasn’t broken demo mode for a specific query string. The max nesting depth of 7 is a strong refactoring signal: at that depth, tracking which conditional scope you’re in requires careful mental bookkeeping, and a misplaced closing brace silently changes behaviour. Fan-out of 14 means the function directly calls 14 distinct functions, making it a hub: a change here can produce unexpected side effects across a wide slice of the application.
The god_function and long_function patterns are both present — and the volume of review comments the last time this file was touched was notably high, which is worth keeping in mind.
This function hasn’t been touched in 110 days. My concrete recommendation: extract each query-string branch into its own named initialiser (initDemoMode, initHgDemoMode, initRemoteDemoMode, initLevelMode, initGistMode). initDemo then becomes a dispatcher of 10 lines. Each extracted function drops to a CC of 2–3 and can be tested independently. That alone eliminates the nesting depth problem because none of the individual cases needs more than two levels of control flow.
levelSolved — level/index.js
levelSolved is the function that runs when a user completes a level. From the source excerpt it does a striking number of things in sequence: it records the solution result, calculates whether the user’s command count beat the par score, dispatches to a global state store, hides the goal visualization, manages animation speed via a multi-case switch on total levels solved, conditionally chains a finish animation that covers both main and origin visualizations, and then conditionally shows a next-level dialog — all wired together as a promise chain.
The cyclomatic complexity of 18 comes largely from the switch statement (which has five explicit cases plus a fallthrough guard), the multiple skip flags (skipFinishDialog, skipFinishAnimation), and the conditional origin visualization branch. What’s structurally distinctive here is that the nesting depth is only 2, while fan-out is 15 — the highest fan-out in this entire list. This is the exit_heavy and god_function combination: broad, flat coupling rather than deep nesting. Fifteen distinct callees means this function is tightly coupled to a large fraction of the application’s surface area. In JavaScript, where some of those calls likely go through event systems or promise callbacks, static fan-out metrics may actually undercount the real dependency surface.
This function hasn’t been touched in 110 days. Given its position — it’s the conclusion of every successful user interaction in the app — any change here risks breaking the completion flow in ways that are hard to catch without end-to-end tests. I’d start by extracting the animation speed calculation into a pure helper (getAnimationSpeed(numSolved)) and the dialog sequencing into a separate showCompletionDialog method. That alone reduces fan-out by consolidating several calls and brings the CC down into single digits.
getType — git/index.js
getType in src/js/git/index.js is a type-discriminator nested inside a larger getOrMakeRecursive function. From the excerpt, it inspects a tree object to determine whether a given ID refers to a commit, branch, HEAD reference, or tag, and throws if none match. The surrounding context then uses the returned type string to drive four more if blocks that each construct the appropriate object — Ref, Branch, Tag, or Commit — recursively resolving parent references.
The cyclomatic complexity of 7 is moderate, and the nesting depth of 4 reflects the chain of if/else if conditions inside getOrMakeRecursive. The exit_heavy pattern flags the throw-at-the-end guard plus the multiple early returns from each type check. Fan-out is 0 because getType itself makes no calls — but the enclosing function that contains it clearly does, and the recursive getOrMakeRecursive calls for parent commits mean the real call graph is deeper than the metric captures.
This function was last changed 46 days ago — the most recently touched entry in this list — and had one active author in the last 90 days, which is a thin ownership profile for a function at the core of the git engine. The actionable step is straightforward: promote getType out of its enclosing closure into a named module-level function, and convert the four downstream type-dispatch blocks into a lookup map keyed on type string. That reduces the CC of the outer function substantially and makes unit-testing the type resolution independently possible.
getType — graph/index.js
This is a near-identical twin of the getType in src/js/git/index.js. The source excerpt shows the same type-discrimination logic — checking commits, branches, HEAD, and tags in the same order, throwing the same error strings — with one implementation difference: where git/index.js uses Array.map to resolve parent commits, graph/index.js uses forEach pushing into a local array.
Two copies of structurally equivalent logic in different modules is a concrete duplication risk. They have diverged once already (the map vs forEach difference), and they will diverge further independently over time. The graph/index.js copy has zero pr_review_comment_density, a single total commit, and no active authors in the last 90 days — it is more dormant than its counterpart and has had less scrutiny.
This function hasn’t been touched in 110 days. The right fix is to extract the shared type-resolution logic into a utility module that both git/index.js and graph/index.js import, then delete both local copies. That consolidates the test surface, eliminates the divergence risk, and makes the recursive object construction testable in isolation from the type-checking logic.
main — scripts/translate.js
main in scripts/translate.js is the CLI entry point for the project’s translation tooling. From the source excerpt, it reads a command-line argument and dispatches to one of six operations — listing locales, checking status, translating levels, translating strings, normalizing, or running all translations for a given locale. Several branches also validate that a required second argument is present, emitting usage errors and calling process.exit(1) if not.
The cyclomatic complexity of 12 and nesting depth of 5 come from the combination of the top-level dispatch chain and the nested argument-validation guards inside each branch. The complex_branching and deeply_nested patterns are both flagged. Fan-out of 8 reflects the calls to listLocales, checkStatus, translateLevels, translateStrings, normalize, and the argument validation paths.
This is tooling code rather than application runtime, which lowers the user-impact risk — but it’s still a maintenance burden for contributors working on localization. It hasn’t been touched in 110 days and has no active authors in the last 90 days. The standard refactoring here is to replace the if/else-if chain with a command map: a plain object keyed on argument string, each value being an async handler function that also encapsulates its own argument validation. That brings the CC of main to 3–4 regardless of how many subcommands are added in the future, and makes adding a new translation operation a two-line change.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 3 |
complex_branching | 2 |
deeply_nested | 2 |
god_function | 2 |
long_function | 2 |
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/pcottle/learnGitBranching
cd learnGitBranching
git checkout 833cec66e87ca575951de519fe10062f360fbc3c
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 →