Git Changelog Generator Pitfalls: Merge Commits Hid the Truth

A git changelog generator lost merge-introduced commits due to a linear assumption, revealing git history is a DAG, not a list.
The author built a git changelog generator by diffing two commits. The first version walked `git log A..B`, which under the standard PR workflow showed only merge commits and not the merged feature commits, causing release notes to lose substance. The fix uses `git log --first-parent` for a branch-level summary, then `git log ^parent1 parent2` per merge to expand real commits and deduplicate. The article notes `A..B` includes reverted commits and isn't the actual release. Core lesson: git history is a DAG; linear-assumption tools silently lose information.
A seemingly simple requirement that exposed the essence of git
The author built a git changelog generator that outputs structured release notes by diffing two commits. The first implementation simply walked git log A..B, treating each commit as a point on a straight line—clean, deterministic, but wrong.
The problem showed up in repositories using the standard PR workflow: when running git log A..B against the main branch, merge commits were visible, but the individual feature commits introduced by those merges were not. The generated changelog would only say "Merge pull request #42," leaving readers with no idea what was actually released.

The insidiousness of this bug: the tool itself ran fine and the output format was correct, but information was silently swallowed. The problem only surfaced when someone reading the release notes asked, "What exactly changed in v2.3?"
The fix isn't a bigger diff
The author's approach to the fix is instructive—the problem wasn't that the diff range was too small, but that the wrong object was being traversed. The solution has two layers:
- Use
git log --first-parent A..Bto get a branch-level summary, treating each merge commit as a structural event - For each merge commit, use
git log ^parent1 parent2to enumerate the commits that merge actually introduced
The final output first presents the merge events, then expands the real commits beneath them, deduplicating against what's already in the baseline. This way readers see both the structure and the substance.
Why A..B lies
The original article points out an easily overlooked semantic trap: git log A..B means "reachable from B but not from A." On the graph, this set includes commits on branches that were merged and later reverted—they are "reachable," but that doesn't mean they were "released."
If what you want is "what was actually delivered," you need: reachable from B, minus reachable from A, intersected with first-parent edges, plus the side-branch commits pulled in by each merge. The author states plainly that this isn't solvable with a one-liner—it's a small graph traversal.
--first-parent is a traversal option for git log that only follows the first parent of each merge commit. In the standard GitHub/GitLab PR workflow, the first parent typically corresponds to the target branch (e.g., main), and the second parent corresponds to the merged feature branch. Therefore git log --first-parent naturally skips all side-branch commits and treats only the merge commits themselves as milestones, giving a "branch-level" linear summary. This is good for answering "what major events happened on the mainline," but it is not a complete release manifest—the specific commits that were merged in are all collapsed away.
In contrast, git log ^parent1 parent2 is one form of Rev range syntax: ^parent1 excludes all commits reachable from the first parent, and parent2 denotes the set reachable from the second parent. Subtracting the two yields "commits that exist only on the feature branch and haven't yet entered the mainline." Doing this enumeration once per merge commit surfaces all the real commits pulled in by the merge. This is precisely the core mechanism of the original article's fix.
The deeper lesson: history is a DAG
Git history is a directed acyclic graph, not a list. Any tool that assumes a linear structure is lying to its users, and that lie is invisible until you read the release notes. The graph is the source of truth; --first-parent is just one view. Walking both and reconciling them is the honest answer.
The universal value of this case: many tools built around git (changelogs, impact analysis, version comparison) are prone to falling into the linear-assumption trap. They appear to work but actually lose information. The author's approach insists on determinism—no LLM was used to "rewrite" history, just letting the graph do what a graph should do.
In git's underlying model, each commit is a record containing a tree object (representing the directory snapshot at that time) and zero or more parent commits. A commit with no parents is a root commit; one with two or more parents is a merge commit. "Reachable" refers to the set of all commits that can be reached by starting from some reference and continuously walking back along parent chains. A..B is the set difference between "reachable from B" and "reachable from A."
The reason "reverted commits are still included" occurs is that a revert merely adds a new commit with a reverse patch; the original commit still exists in the history graph and is still reachable from B. Reachable ≠ in effect, and ≠ released. Understanding this makes it clear why A..B alone cannot answer "what was actually delivered": it describes the topological relationships of the graph, not release semantics.
Implications for tool developers
If you're building any tool that depends on git history, there are two checkpoints worth confirming upfront: whether the default A..B semantics match your definition of "release"; and whether merge commits are treated as noise to be ignored or as structural signals in your output. These two decisions directly affect the quality of end users' understanding of "what changed."
One-line Summary
A git changelog generator lost merge-introduced commits due to a linear assumption, revealing that git history is a DAG, not a list.
Paragraph Summary
The author built a git changelog generator based on diffing two commits. The first version simply walked git log A..B, which under the standard PR workflow showed only merge commits and not the merged feature commits, causing release notes to lose substantive content. The fix uses git log --first-parent to get a branch-level summary, then for each merge commit uses git log ^parent1 parent2 to expand the real commits and deduplicate. The article further points out that A..B means "reachable from B but not from A," which includes reverted commits and doesn't equal actual release content. The core lesson: git history is a DAG, and any tool with a linear assumption silently loses information—the graph is the source of truth.
Related articles

Gemini 4 Argon Hands-On: AI Builds Board Games, Constructs Temples, and Trains Robots Autonomously
Gemini 4 Argon hands-on roundup: AI autonomously builds a desk racetrack and temple in Blender, writes a ray tracing engine, trains a humanoid robot to sink 7 shots, plus API pricing and developer access.

The Magic Octagonal Coin: Why Only 9% of People Can Predict Its Flip?
Only 9% of people can correctly predict the Magic Octagon coin flip. This article explains its spatial rotation mechanism, common cognitive pitfalls, and a new hypothesis based on Relevance Theory, exploring the limits of human spatial reasoning.

Sam Altman: AI Is Entering the Era of Scientific Discovery — Math Breakthroughs Will Replicate in Biology and Energy
Sam Altman says AI is entering the era of scientific discovery — math breakthroughs will replicate in biology, materials science, and energy to help develop new drugs and energy sources.