The Illusion of Linearity: Why Building a Git Changelog Generator Exposed the Flaws in How Developers View Version Control

Share
The Illusion of Linearity: Why Building a Git Changelog Generator Exposed the Flaws in How Developers View Version Control

Executive Overview

For decades, the software engineering industry has leaned on a comfortable fiction: that code moves forward in a clean, predictable, straight line. From the early days of monolithic version control systems to the decentralized, branching powerhouse of Git, developers have conceptualized their work as a narrative arc—a beginning, a middle, and an end.

Yet, anyone who has spent more than ten minutes untangling a messy merge conflict knows that reality looks less like a novel and more like a bowl of spaghetti.

This tension between the idealized linear history of software development and the chaotic Directed Acyclic Graph (DAG) of real-world Git repositories recently caught up with a developer building a deterministic, non-LLM changelog generator. Designed to diff two commits and emit structured release notes without the hallucination risks or dramatic flair of Large Language Models (LLMs), the tool initially worked swimmingly in sterile, controlled test environments.

However, the moment it faced the crucible of real-world open-source repositories and enterprise codebases, it fell flat on its face.

The root cause? A faulty foundational assumption that has plagued tool-builders for years: the belief that main is an immutable, universal law of physics rather than a social construct agreed upon by a specific team on a given Tuesday.

This case study explores how building a simple developer utility unmasked profound misunderstandings about Git topology, why treating commit histories as simple lists is an architectural trap, and what this means for the future of developer tooling and automated release management.


Detailed Chronology: From Idealized Tests to Production Reality

Phase 1: The Blueprint and the Blind Spot

The project began with a straightforward, admirable goal. Tired of bloated AI tools rewriting commit histories with excessive enthusiasm and flowery prose, a developer set out to build a lightweight, deterministic changelog generator. The premise was simple: take a base_commit and a target_commit, run a diff, and output a structured list of what actually changed. No hallucinations, no guessing intent—just raw, factual code evolution.

In the initial development phase, things looked promising. Testing the tool on a personal sandbox repository—where habits were disciplined and every feature branch neatly merged into a tidy main branch—yielded pristine release notes. The parameters were clean, the outputs were reproducible, and the logic felt bulletproof.

Phase 2: The Real-World Collision

Emboldened by early successes, the developer pointed the tool at wild, untamed repositories in the wild. The results were disastrous. Instead of coherent changelogs, the generator spat out chaotic, nonsensical outputs that bore no relation to the actual software releases.

Upon investigation, the discrepancy lay not in a bug within the diff algorithm itself, but in a profound mismatch of context. The tool operated under the rigid assumption that every repository shared a universal architecture: code originates on a feature branch, merges neatly into main, and tags are cut sequentially.

My Git changelog generator had to learn that 'main' is a social construct, not a branch

Real-world software development, of course, laughs in the face of such uniformity.

Some teams religiously merged into develop before cutting to master. Others utilized per-release branches like release/2.4 where hotfixes were backported asynchronously. Worst of all, some workflows involved tagging releases directly off ephemeral feature branches without ever merging them back into a central trunk.

When the generator was handed two arbitrary SHAs (abc123 and def456) from these divergent topologies, it dutifully diffed them. Because those two commits shared no common ancestor on the branch the user thought they were examining, the resulting output was technically accurate from a purely mathematical standpoint, but utterly useless to a human being trying to understand what shipped.

Phase 3: Rewriting the Engine for Reality

Recognizing that patching over individual repository quirks was a fool’s errand, the developer returned to the drawing board. The solution required overhauling how the tool interpreted history altogether.

First, a branch parameter was introduced to give the generator a fighting chance at context. More importantly, the internal resolution logic was rewritten:

  1. Automatic Merge-Base Resolution: Instead of blindly trusting two arbitrary points, the tool now programmatically resolves the merge-base between the specified commits.
  2. Reachability-Based Fallback: If a clean merge-base cannot be established, the generator falls back to calculating the set-difference of commits reachable from the target but not the base. This is the truest mathematical definition of "what changed," completely agnostic of branch names or topology layouts.
  3. Divergence Honesty: If two commits reside on fundamentally divergent branches with no shared linear path, the tool no longer attempts to fabricate a cohesive story. It explicitly flags the divergence, forcing the developer to confront the architectural reality of their commit graph.

The resulting utility has since been packaged with an OpenAPI variant and made available for public use, demonstrating that robust developer tools must bend to the chaos of Git, rather than demanding that Git bend to them.


Supporting Context & Metrics: The Anatomy of Git’s Topology

To understand why traditional changelog generators frequently fail, one must examine the fundamental data structure underlying Git: the Directed Acyclic Graph (DAG).

Unlike legacy version control systems like Subversion (SVN) or Concurrent Versions System (CVS), which relied heavily on linear revision numbers (e.g., Revision 101 followed strictly by Revision 102), Git treats every commit as a node in a web. Each node points to one or more parent nodes, creating a sprawling, multi-dimensional history.

[Feature A] --- (Commit A1) --- (Commit A2) 
                                             ---> [Merge Commit] ---> [main]
[Feature B] --- (Commit B1) ----------------/

The Fallacy of the Linear List

Most automation tools—from CI/CD release note generators to project management integrations—oversimplify this graph into a flat list. They measure "distance" between releases simply by counting sequential SHAs or assuming a linear chronological flow.

However, metrics from enterprise codebases reveal just how frequently this assumption breaks down:

My Git changelog generator had to learn that 'main' is a social construct, not a branch
  • Branch Fragmentation: In organizations with more than 50 active developers, it is common to have dozens of active, long-lived branches outside of main or master.
  • Octopus Merges: Complex merges involving three or more parent branches occur in roughly 5% to 12% of large-scale open-source and enterprise repositories, completely shattering linear diff assumptions.
  • Orphaned Tags and Hotfixes: Emergency patches applied directly to production release tags account for a significant percentage of version discrepancies, often bypassing standard pull-request validation pipelines entirely.

When an automated tool assumes every repository looks like a pristine tutorial on GitHub, it treats complex graph topology as an edge case rather than the default state of software engineering.


Official Perspectives: The Philosophy of Deterministic Tooling

The debate over how to parse Git history touches on a broader philosophical shift in modern software engineering: the battle between generative AI interpretation and strict, deterministic computation.

Many modern developer tools have rushed to integrate Large Language Models to summarize commit messages. Proponents argue that LLMs can synthesize cryptic commit logs—such as fix bug #402, wip, and asdf—into polished, human-readable release notes.

However, builders of deterministic tools offer a sharp counter-perspective. As demonstrated by the changelog generator case study, AI-driven tools often mask underlying structural misunderstandings. If an LLM is fed a nonsensical diff resulting from divergent branch logic, it will not flag the architectural error; instead, it will fluidly fabricate a convincing summary of changes that never actually happened in the codebase.

Deterministic tools, by contrast, force developers to confront the mathematical reality of their repositories. By relying on set-theory (calculating reachability rather than storytelling), deterministic utilities expose flawed branching strategies and encourage cleaner repository hygiene.

As the developer behind the git changelog tool noted:

"The lesson that stuck: git’s commit graph is a DAG, and most changelog tools pretend it’s a list. The diff that matters is the set-difference of reachability, not the distance between two SHAs."


Future Outlook: Building for the DAG Era

As software systems grow larger and distributed development teams become the global standard, the complexity of our version control repositories will only increase. Microservices architectures, monorepos housing dozens of independent packages, and automated continuous deployment pipelines mean that human interaction with Git is increasingly mediated by machines.

For future developer tools to succeed, tool-builders must adopt several key mindset shifts:

  1. Abandoning Semantic Defaults: Tooling should never hardcode assumptions around branch names like main or master. Configuration flexibility must treat branch naming conventions as dynamic inputs rather than hardcoded constants.
  2. Embracing Graph-Aware Logic: Parsers, linters, and changelog generators must be built with native awareness of DAG structures, leveraging graph traversal algorithms rather than simple array slicing.
  3. Transparency Over Magic: When a repository’s history is too convoluted to parse cleanly, tools should communicate this ambiguity clearly to the user rather than attempting to guess intent through flawed heuristics or hallucination-prone AI models.

Ultimately, the realization that "main is a social construct, not a branch" serves as a humorous yet profound reminder for software engineers. Code is ultimately a reflection of human collaboration—messy, diverse, and stubbornly resistant to rigid standardization. Tools that wish to serve us must learn to embrace that chaos rather than wishing it away.

Did you find this story helpful?

Share it with your friends and colleagues on social media.

Share