learnGitBranching's app layer carries structural debt — 5 functions to address first

All five top-scoring functions in pcottle/learnGitBranching sit in the structural-debt quadrant — untouched for up to 110 days yet carrying cyclomatic complexity and fan-out high enough to make the next change risky.

Stephen Collins ·
Generated by hotspots · free & open source
Activity Risk13.08Low
Hottest FunctioninitDemo

Antipatterns Detected

exit_heavy3complex_branching2deeply_nested2god_function2long_function2

Run this on your own codebase

Hotspots runs locally in under a minute — no account, no data leaves your machine.

pip
$ pip install hotspots-cli
npm
$ npm install -g @stephencollinstech/hotspots
Run in any repo
$ hotspots analyze .
★ Star on GitHub

Key Points

What is a god function and why does it matter in learnGitBranching?

A god function is one that takes on too many distinct responsibilities — dispatching control flow, managing state, coordinating I/O, and sequencing side effects all in the same body. The problem is coupling: because so many concerns live together, a change to any one of them requires understanding all the others, and callers cannot depend on just the part they need. In learnGitBranching, both `initDemo` and `levelSolved` carry the god_function pattern — `initDemo` with a fan-out of 14 distinct callees and `levelSolved` with a fan-out of 15 — meaning a modification to either function touches a large fraction of the application's surface area. Both functions sit in the debt quadrant with zero touches in the last 30 days and were last changed 110 days ago. That broad coupling is what makes the blast radius high when these functions are next changed, even after that long a period of dormancy.

How do I reduce cyclomatic complexity in JavaScript?

The most effective technique is extract-method refactoring: identify each independent conditional branch and move it into its own named function, reducing the parent function to a dispatcher. A cyclomatic complexity above 10 is worth investigating; above 15 it warrants splitting; `initDemo`'s CC of 19 and `levelSolved`'s CC of 18 both clear that bar. A concrete first step for `initDemo` today: extract the `hgdemo` and `hgdemo2` branches into named functions — each is self-contained and their removal alone would drop the parent CC by roughly 4. For switch-statement complexity like the animation speed logic in `levelSolved`, replacing the switch with a data lookup table (a plain JavaScript object mapping case values to results) eliminates one branch per case while making the logic easier to extend.

Is learnGitBranching actively maintained?

Based on this snapshot at commit 833cec6, none of the five highest-risk functions have been modified in the last 30 days. The most dormant are `initDemo`, `levelSolved`, `getType` in `graph/index.js`, and `main` in `scripts/translate.js`, each last changed 110 days ago. The `getType` in `git/index.js` is the most recently touched, at 46 days since last modification. All five sit in the debt quadrant, meaning high structural complexity with low recent activity. Active maintenance and high structural debt are not mutually exclusive: the repo could be receiving contributions elsewhere in its 815-function codebase while these specific entry points have been left alone. The debt quadrant classification means the risk is latent, not current — but it will become current the day these functions need to change.

How do I reproduce this analysis?

The Hotspots CLI is available at github.com/hotspots-dev/hotspots. After installing, check out the exact commit analyzed here with `git checkout 833cec6`, then run `hotspots analyze . --mode snapshot --explain-patterns --force` from the repository root. The same command works on any local git repository without additional configuration — no `.hotspotsrc.json` is required to get started, though you can add one to exclude generated or vendored paths.

What does activity-weighted risk mean?

Activity-weighted risk multiplies a function's structural complexity — derived from cyclomatic complexity, nesting depth, and fan-out — by its recent commit frequency. The intent is to surface functions that are both hard to understand AND actively changing, because those are the ones most likely to accumulate bugs right now. A function with cyclomatic complexity 80 that hasn't been touched in two years scores considerably lower than one with CC 20 that is modified every week, because the dormant function has lower near-term regression probability even though it looks more complex in isolation. In learnGitBranching, all top functions are in the debt quadrant with zero recent touches, which is why their activity_risk scores are moderate rather than extreme — the structural risk is real, but it isn't being exercised right now.

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

FunctionFileRiskCCNDFO
initDemosrc/js/app/index.js13.119714
levelSolvedsrc/js/level/index.js11.718215
getTypesrc/js/git/index.js9.774
getTypesrc/js/graph/index.js9.774
mainscripts/translate.js9.61258

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

All structural risk is dormant — 45 debt-quadrant functions, zero in the fire quadrant
Debt45OK770

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.

Detected Antipatterns
Exit Heavy×3Exit Heavy
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
src/js/app/index.js
13.08
critical
CC 19
ND 7
FO 14
touches/30d 0

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
src/js/level/index.js
11.75
critical
CC 18
ND 2
FO 15
touches/30d 0

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
src/js/git/index.js
9.7
critical
CC 7
ND 4
FO 0
touches/30d 0

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

getType
src/js/graph/index.js
9.7
critical
CC 7
ND 4
FO 0
touches/30d 0

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
scripts/translate.js
9.6
critical
CC 12
ND 5
FO 8
touches/30d 0

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:

PatternOccurrences
exit_heavy3
complex_branching2
deeply_nested2
god_function2
long_function2

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 →

Related Analyses