Every repository has a moment nobody talks about.
Not the first commit. Not the launch. Not the sprint where everything clicked. The moment I keep coming back to — the one that made me build this thing — is the gap. The silence. The chapter where nothing happened and everyone pretended it didn't exist.
When I ran Commit Canvas over Flask's history, it found six chapters.
Five were predictable. The sixth stopped me:
The Silence: 31 days of nothing. November 28, 2024 → January 1, 2025.
A changelog treats that gap as an absence. A story treats it as a beat.
That's everything.
The Problem Nobody Names
You've shipped something. Maybe it took two years. Maybe six. There were nights you pushed at 2am, weeks where the repo barely breathed, a sprint that felt like you were running straight into a wall and somehow came out the other side.
And the artifact of all of that is... a commit graph. Some green squares. A number.
git log is honest. It's complete. It is also about as narrative as a shipping manifest.
GitHub Insights tells you how much. Gource makes a beautiful video of your file tree growing — genuinely hypnotic, fifteen years mature, and about as useful for understanding what happened as watching a timelapse of a city and trying to read its history.
The data for a real story is already sitting in your repo: commit dates, authors, gaps, tags, merge topology, churn. What's missing is something that reads it as a sequence of beats instead of a pile of rows.
So I built the reader.
What It Is
Commit Canvas reads a git repository and generates one thing: a single, self-contained HTML file. Chapters. A scrubbable time machine. A project fingerprint. An export studio. All of it rendered from your real history.
git clone https://github.com/ahmadrrrtx/commit-canvas
cd commit-canvas
./run.sh /path/to/your/repo --theme sunset --open
Python 3.8+ and the git CLI. Zero third-party dependencies. The output is one file — roughly 160 KB. Email it. Commit it to the repo. Put it on a USB stick. Open it offline in ten years. There is nothing left to fetch.
No server processes anything. No account. No API key. No analytics. The analysis never leaves your machine.
There's also a web version — paste a public GitHub URL and the same engine runs entirely in your browser tab. And four live demos you can open right now: Flask · Express · Requests · the project's own story.
The Engine Is Honest by Design
The entire analysis runs on two subprocesses. One git log call with a custom delimiter-separated format — fields split by \x1f because commit messages can contain pipes, and \x1f is the one character git promises never to emit inside them. One git for-each-ref for release dates. Everything after that is pure Python.
Out comes a monthly series: commits per month, active days, merges, new contributors, net lines. Almost every story decision flows from that series.
Here is the part I want you to read carefully:
There is no message parsing. No AI. No LLM anywhere in the pipeline. No model reading your commit messages and deciding what your project "means." Just arithmetic over dates — which is exactly why the results are defensible. When someone asks "why does my repo say The Sprint?" you can open the rule and count the thresholds on one screen.
if gaps >= 60 and comeback_strength >= max(3, median * 0.5):
label, arc = "The Return", "return"
elif peak >= 4 * median and peak >= 15:
label, arc = "The Sprint", "sprint"
elif late_velocity >= 1.8 * early_velocity:
label, arc = "The Climb", "climb"
elif early_velocity >= 1.8 * late_velocity:
label, arc = "The Slow Fade", "fade"
elif len(months) >= 6 and max_month < 3 * median:
label, arc = "The Marathon", "marathon"
Everything is relative to your own median. With absolute floors where a relative rule would be silly — a repo that medians 0.4 commits can't fake a Sprint. The >= 15 floor rejects it before the story begins.
Seven Shapes. One Truth.
The pipeline picks one of seven labels for every project:
| Shape | What Triggers It |
|---|---|
| Fresh Start | Less than 5 commits, or under 60 days old |
| The Return | Longest gap ≥ 60 days, then a real comeback |
| The Sprint | Peak month ≥ 4× median and ≥ 15 commits |
| The Climb | Second half ≥ 1.8× faster than the first |
| The Slow Fade | First half ≥ 1.8× faster than the second |
| The Marathon | Six months or more, no spike above 3× median |
| The Journey | Everything else — quiet parts, loud parts, and all of it |
Each shape ships with a one-line summary. The Sprint's reads: "Built in bursts of deep focus — the history remembers every surge."
People always assume those lines are hand-written per project. They're not. Seven strings, selected by rules. That's the tradeoff — sentences you can't configure are sentences you can't accidentally fake.
The Silence
The rule that generates the most reaction is also the simplest:
if gap and gap["days"] >= 21:
"subtitle": f"{human_gap(gap['days'])} of nothing"
Twenty-one days. Not because three weeks is sacred — because anything shorter is a weekend plus a cold. That number took five repos and one embarrassing live demo of my own project to land on.
The honesty constraint is the design law the whole engine sits under: never claim more than the data shows.
The subtitle says "31 days of nothing" and stops there. It doesn't tell you the maintainer was burnt out. It doesn't suggest the project was dying. It doesn't know any of that. Neither do I. The gap happened. That's the chapter. The reader brings the meaning.
Same philosophy in the fingerprint section — commit-time patterns identified as Night Builder, Weekend Hacker, Marathoner, Lone Wolf — closed with a line I keep coming back to: "A fingerprint of Git behavior — not a personality test."
What Was Harder Than Expected
Which clock is real. Streaks were initially computed from committer date. On any repo with rebases or late pushes, a streak can be authored Tuesday and committed Sunday. One wrong demo later: switched to author date. If you build anything on git data — %at is when the work happened, %ct is when the object landed. They are not the same. Most tools quietly pick one and never say which.
Tests that lied. Fixtures committing multiple commits in the same second made streak tests pass for the wrong reason. I had to stagger fixture dates before the tests could fail correctly. A test suite that can't fail is just documentation with extra steps.
Restraint. It would've been so easy — sentiment analysis of commit messages, a productivity score, a burnout warning. Every single one would have been wrong on at least one repo in the demo set. Wrongness is the one thing a tool built on your history cannot afford. Half the design work was deleting features that would have been genuinely cool and genuinely dishonest.
What I'm Not Claiming
No analytics. There is no way for me to know how many people use this — and that's by design. It would be rich to build a tool about not being surveilled and then install beacons on it.
The README mentions pip install commit-canvas. That's aspirational. The package isn't on PyPI yet. ./run.sh or the website are the real paths today.
It's an MIT-licensed utility. Four public demos. Twenty-seven tests. A website with no trackers. That's the whole pitch.
Try It
git clone https://github.com/ahmadrrrtx/commit-canvas
cd commit-canvas
./run.sh ~/code/your-project --theme sunset --open
Add --max-commits 5000 if you're pointing it at something Linux-sized. Private repos work — the analysis never touches a server. Hit the ◐ button inside the story to flip between eight visual editions without re-running anything.
No install? The website takes a public GitHub URL and does the same thing in your tab.
The Ending I Didn't Plan
When I ran Commit Canvas on its own repository, the engine read the timeline its demo file records — a beginning, a launch, then eight weeks of complete silence — and returned the only label that fit:
The Return. "It went quiet. Then it came back. Projects don't do that by accident."
I didn't write that sentence for my own project. The rule found the pattern. The summary was already sitting there waiting.
That's the moment I'd been building toward since June. Not a dashboard. Not a leaderboard. One file, honest arithmetic, and a story your repo already contained.
If this made you curious about what your own repo's chapters look like — the GitHub repo is here. A star helps more people find it, and it takes two seconds. Either way, run it on something you've spent real time on. The silence chapter has a way of hitting different when it's yours.
website try it here
Top comments (2)
The Flask finding is the most compelling part of the post: a 31-day gap that a changelog renders as nothing, and a maintainer reading it back as a beat. That reframe is real, not decoration.
I'd be curious how the algorithm handles the messy side of history, because that's where narrative tools usually fall apart. Authors collapse under several identities unless you apply a mailmap, and a force-push or a long-lived branch rewrite can move the "chapters" you detected after the fact. Does Commit Canvas run off
git logtopology as it exists today, so a rebase can silently rewrite the story?Also: how are chapter boundaries actually decided — commit-density clustering, tag boundaries, or a gap threshold? If it's density-based, the Silence is a legitimate finding; if it's mostly time-based, you're naming absence and calling it structure, which is a different claim. Worth stating in the README, because people will show the output to managers and it will be read as analysis either way.
Would love to run it against a repo with heavy squash-merge on main.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.