{
  "id": 15299052,
  "title": "My git changelog generator learned that merge commits hide the truth — parents are a graph, not a line",
  "url": "https://urgent.news/2026/10/17/my-git-changelog-generator-learned-that-merge-commits-hide-the-truth",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-10-17T23:01:34.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/imapphelp/my-git-changelog-generator-learned-that-merge-commits-hide-the-truth-parents-are-a-graph-not-a-29k2"
  },
  "original_language": "en",
  "account": "I developed a git changelog generator that examines two commits and produces organized release notes. Initially, it traversed git log A..B, treating each commit as a linear sequence. While this method was straightforward and predictable, it was inaccurate. When applied to a typical pull request workflow, git log A..B on the main branch revealed merge commits but omitted the individual feature commits they incorporated. Consequently, my changelog would list \"Merge pull request #42\" without further details. Humans reading the release notes would gain no insight into what was actually released.\n\nThe solution wasn't a more complex diff; it required altering what I queried. I modified my approach to use git log --first-parent A..B for the branch-level summary. Then, for each merge commit, I employed git log ^parent1 parent2 to enumerate the commits that the merge commit actually introduced. The updated changelog now presents the merge as a structural event, followed by the real commits it introduced, deduped against any commits already present on the base branch. The deeper implication is that git history is a directed acyclic graph (DAG), not a simple list. Any tool that assumes a linear structure is misleading, and this deception remains undetected until someone reviews your release notes and inquires, \"What actually changed in version 2.3?\" The graph serves as the ultimate source of truth.\n\nMoreover, I discovered that git log A..B exhibits symmetry, which can be misleading. While it appears to show commits reachable from both A and B, it actually includes commits from branches that were merged and later reverted. If you desire to determine \"what was shipped,\" you need to calculate reachability from B, subtract reachability from A, intersect this result with first-parent edges, and then add the side-branch commits each merge commit brought in. This process is more complex than a simple one-liner. It involves a small graph traversal. The solution is now available as a deterministic diff at https://x402.freeq.one/tools/changelog_git.html. This tool generates a reliable changelog by accurately reflecting the graph structure of git history, rather than relying on inaccurate linear assumptions.",
  "summary": "I built a git changelog generator that diffs two commits and emits structured release notes. First version walked git log A..B and treated every commit as a straight line. Clean, deterministic, wrong. The bug: on a repo with a normal PR workflow, git log A..B on the main branch shows merge commits but NOT the individual feature commits they brought in. My changelog would say \"Merge pull request…",
  "key_points": [],
  "editors_take": null,
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}