<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Çağatay Uncu</title>
    <description>The latest articles on DEV Community by Çağatay Uncu (@cagatayuncuu).</description>
    <link>https://dev.to/cagatayuncuu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4147248%2Fb0191e00-76ee-4d1c-a145-2edae9bcda2d.jpg</url>
      <title>DEV Community: Çağatay Uncu</title>
      <link>https://dev.to/cagatayuncuu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cagatayuncuu"/>
    <language>en</language>
    <item>
      <title>One hotfix, three repos, four surprises: why I wrote a Git Flow doctor</title>
      <dc:creator>Çağatay Uncu</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:40:44 +0000</pubDate>
      <link>https://dev.to/cagatayuncuu/one-hotfix-three-repos-four-surprises-why-i-wrote-a-git-flow-doctor-491g</link>
      <guid>https://dev.to/cagatayuncuu/one-hotfix-three-repos-four-surprises-why-i-wrote-a-git-flow-doctor-491g</guid>
      <description>&lt;p&gt;We still run classic Git Flow at work: &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;develop&lt;/code&gt;, and short-lived&lt;br&gt;
&lt;code&gt;release/*&lt;/code&gt; and &lt;code&gt;hotfix/*&lt;/code&gt; branches. Before you close the tab: yes, trunk-based&lt;br&gt;
development is simpler, and for many teams it is the right call. But plenty of&lt;br&gt;
teams ship versioned software to customers, keep a release line alive, and&lt;br&gt;
live with Git Flow for good reasons.&lt;/p&gt;

&lt;p&gt;What GitHub gives those teams is a pile of separate manual steps. Finishing&lt;br&gt;
one hotfix means: merge to &lt;code&gt;main&lt;/code&gt;, tag the merge commit, publish a GitHub&lt;br&gt;
Release, merge back into &lt;code&gt;develop&lt;/code&gt; (or into the open release branch, if there&lt;br&gt;
is one), delete the branch. Five steps, and nothing tells you when one of them&lt;br&gt;
was skipped.&lt;/p&gt;

&lt;p&gt;Here is the hotfix that made me stop doing this by hand.&lt;/p&gt;
&lt;h2&gt;
  
  
  The hotfix
&lt;/h2&gt;

&lt;p&gt;One fix, shipped in lockstep across three repositories (a backend and two&lt;br&gt;
frontends). The branch was &lt;code&gt;hotfix/2.0.0-hotfix.12&lt;/code&gt;. Four things went wrong,&lt;br&gt;
and none of them was the fix itself.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. "Latest tag" depended on my git config
&lt;/h3&gt;

&lt;p&gt;Our tags look like &lt;code&gt;2.0.0-hotfix.12&lt;/code&gt;. Ask git for the newest tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git tag --merged origin/master --sort=-version:refname | head -1
2.0.0-hotfix.12

$ git -c versionsort.suffix=- tag --merged origin/master --sort=-version:refname | head -1
2.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same repo, same command, different answer, depending on a config key most&lt;br&gt;
people have never heard of. Anything built on "is this hotfix newer than the&lt;br&gt;
latest tag?" gives a different answer on different machines.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. The branch was cut from a stale base
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;hotfix.12&lt;/code&gt; was opened before &lt;code&gt;hotfix.11&lt;/code&gt; was merged. &lt;code&gt;hotfix.11&lt;/code&gt; deleted a&lt;br&gt;
comment block in a &lt;code&gt;web.config&lt;/code&gt;; &lt;code&gt;hotfix.12&lt;/code&gt; added a rule right above that&lt;br&gt;
comment. Result: a merge conflict on &lt;strong&gt;both&lt;/strong&gt; legs of the finish, into&lt;br&gt;
&lt;code&gt;master&lt;/code&gt; and into &lt;code&gt;develop&lt;/code&gt;. We only saw it because someone happened to run&lt;br&gt;
&lt;code&gt;git merge-tree&lt;/code&gt; during review.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. A clean merge didn't mean working code
&lt;/h3&gt;

&lt;p&gt;After the local merges, we built every repo before pushing. One detail from&lt;br&gt;
the backend: Git Bash rewrites &lt;code&gt;/t:Build&lt;/code&gt; into a file path, so MSBuild ran,&lt;br&gt;
built nothing, and exited &lt;code&gt;0&lt;/code&gt;. A "green" verify step that proves nothing.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Someone else pushed in the middle
&lt;/h3&gt;

&lt;p&gt;While the merges were being verified locally, those same local commits were&lt;br&gt;
pushed from a GUI client on the same machine. The commits were identical, so&lt;br&gt;
nothing broke. If they had differed, either unverified code would be on&lt;br&gt;
&lt;code&gt;master&lt;/code&gt;, or the three repos would be left half-released.&lt;/p&gt;

&lt;p&gt;And in the background: four hotfix branches open at once, with nothing to say&lt;br&gt;
which one to finish first.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I built: gitdoctor
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/cagatayuncu/gitdoctor" rel="noopener noreferrer"&gt;gitdoctor&lt;/a&gt; is one bash script&lt;br&gt;
(bash 3.2+, no jq) plus a set of playbooks. The script is &lt;strong&gt;read-only&lt;/strong&gt;: its&lt;br&gt;
only mutation is &lt;code&gt;git fetch&lt;/code&gt;. It checks the repo and tells you what is wrong&lt;br&gt;
and how to fix it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;cagatayuncu/tap/gitdoctor
&lt;span class="nb"&gt;cd &lt;/span&gt;your-repo
gitdoctor &lt;span class="nt"&gt;--format&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs 51 checks: missing back-merges, tags that are not on &lt;code&gt;main&lt;/code&gt;, release&lt;br&gt;
tags without a GitHub Release, PRs opened against the wrong base, branches&lt;br&gt;
&lt;code&gt;main&lt;/code&gt; has moved past, stale branches, missing branch protection, and more.&lt;br&gt;
Each finding comes with a fix and a recipe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARN  flow-branch-behind-main  hotfix/2.0.0-hotfix.12 is 6 commit(s) behind master — its finish would conflict (1 path(s); resolve on the branch first)
      fix: git switch hotfix/2.0.0-hotfix.12 &amp;amp;&amp;amp; git merge origin/master
      recipe: references/fix-recipes.md#flow-branch-behind-main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one finding would have caught surprise #2 before anyone started merging.&lt;br&gt;
The fix it suggests is the right one: merge &lt;code&gt;main&lt;/code&gt; into the hotfix branch and&lt;br&gt;
let the author resolve the conflict there, where they know the change.&lt;/p&gt;

&lt;p&gt;Output is JSON by default, so agents, CI and scripts can use it. &lt;code&gt;--format&lt;br&gt;
text&lt;/code&gt;, &lt;code&gt;markdown&lt;/code&gt; and &lt;code&gt;sarif&lt;/code&gt; are there for people, PR descriptions and GitHub&lt;br&gt;
code scanning. &lt;code&gt;gitdoctor --explain &amp;lt;check-id&amp;gt;&lt;/code&gt; prints the recipe in the&lt;br&gt;
terminal.&lt;/p&gt;
&lt;h2&gt;
  
  
  Finishes you can resume
&lt;/h2&gt;

&lt;p&gt;The part I care about most: a release or hotfix finish is a &lt;strong&gt;probe walk&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gitdoctor &lt;span class="nt"&gt;--probe&lt;/span&gt; finish-hotfix &lt;span class="nt"&gt;--branch&lt;/span&gt; hotfix/2.0.0-hotfix.12 &lt;span class="nt"&gt;--version&lt;/span&gt; 2.0.0-hotfix.12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The probe reports, for each step, whether it is already done: merged to&lt;br&gt;
&lt;code&gt;main&lt;/code&gt;, tagged, tag pushed, back-merged, branch deleted, GitHub Release&lt;br&gt;
created. It works that out from the repo itself (ancestry, then the GitHub PR,&lt;br&gt;
then content equivalence for squash merges, then the tag). There is no state&lt;br&gt;
file to go stale.&lt;/p&gt;

&lt;p&gt;So when CI is red, the network drops or a session dies halfway, you run the&lt;br&gt;
finish again and it continues from the first missing step. Half-finished&lt;br&gt;
releases stop being a thing someone has to notice.&lt;/p&gt;
&lt;h2&gt;
  
  
  The four surprises, fixed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version ordering&lt;/strong&gt; is done in the script with strict SemVer precedence,
never by git's sort. &lt;code&gt;versionScheme: suffix-counter&lt;/code&gt; supports
&lt;code&gt;X.Y.Z-hotfix.N&lt;/code&gt; hotfixes: &lt;code&gt;2.0.0 &amp;lt; 2.0.0-hotfix.9 &amp;lt; 2.0.0-hotfix.12 &amp;lt;
2.0.1&lt;/code&gt;, and the next hotfix suggestion is &lt;code&gt;2.0.0-hotfix.13&lt;/code&gt;, skipping
numbers already held by open branches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict forecast&lt;/strong&gt;: before any merge starts, the probe runs &lt;code&gt;git
merge-tree&lt;/code&gt; for both legs and lists the paths that will conflict.
&lt;code&gt;multiple-hotfix-branches&lt;/code&gt; lists open hotfixes in version order, with the
tag each was cut from and which one to finish first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; commands in &lt;code&gt;.gitflow.json&lt;/code&gt; run on the merged branch before the
tag and the push. They are read from &lt;code&gt;origin/main&lt;/code&gt;, never from the branch
being finished (a branch shouldn't decide what runs on your machine). An
&lt;code&gt;expect&lt;/code&gt; glob checks that the build really produced something, for tools
that exit 0 without doing any work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pushes from another tool&lt;/strong&gt; are met safely: if origin already has exactly
these commits the push is a no-op, and if origin moved elsewhere git
rejects it. Never &lt;code&gt;--force&lt;/code&gt;. And: finish from one tool, in one session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For repos that release together there is a workspace mode (&lt;code&gt;--workspace&lt;br&gt;
.gitflow-workspace.json&lt;/code&gt;): one view of every repo's state, a single "ready"&lt;br&gt;
gate before anything is pushed, and a check that the same tag has the same&lt;br&gt;
type and message everywhere.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the AI part comes in
&lt;/h2&gt;

&lt;p&gt;The playbooks are written for coding agents (Claude Code and Cursor). You say&lt;br&gt;
"finish the hotfix", the agent runs the probe, shows you the forecast, and&lt;br&gt;
performs the merges and pushes, each one after you confirm. The script&lt;br&gt;
decides &lt;em&gt;what is true&lt;/em&gt;; the agent only acts on its JSON.&lt;/p&gt;

&lt;p&gt;You don't need an agent, though. The doctor works on its own, in CI or as a&lt;br&gt;
pre-push hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/gitdoctor.yml (PR comment + critical findings gate)&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cagatayuncu/gitdoctor@v0.4.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Homebrew: &lt;code&gt;brew install cagatayuncu/tap/gitdoctor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Action: &lt;code&gt;uses: cagatayuncu/gitdoctor@v0.4.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Claude Code plugin: &lt;code&gt;/plugin marketplace add cagatayuncu/claude-plugins&lt;/code&gt;,
then &lt;code&gt;/plugin install gitdoctor@cagatayuncu&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Source (MIT): &lt;a href="https://github.com/cagatayuncu/gitdoctor" rel="noopener noreferrer"&gt;https://github.com/cagatayuncu/gitdoctor&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's opinionated about Git Flow, and I'd like to hear where it's wrong for&lt;br&gt;
your setup. Which checks would you add? Issues and PRs are welcome.&lt;/p&gt;

</description>
      <category>git</category>
      <category>ai</category>
      <category>opensource</category>
      <category>github</category>
    </item>
  </channel>
</rss>
