<?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: Achedon12</title>
    <description>The latest articles on DEV Community by Achedon12 (@achedon12).</description>
    <link>https://dev.to/achedon12</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%2F4115944%2F67da6ffc-802f-48b4-be4a-9ae3d9aba43d.jpg</url>
      <title>DEV Community: Achedon12</title>
      <link>https://dev.to/achedon12</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/achedon12"/>
    <language>en</language>
    <item>
      <title>Your GitHub template repo is a one-time copy — we found out across 68 sites</title>
      <dc:creator>Achedon12</dc:creator>
      <pubDate>Tue, 08 Sep 2026 14:42:19 +0000</pubDate>
      <link>https://dev.to/achedon12/your-github-template-repo-is-a-one-time-copy-we-found-out-across-68-sites-26j3</link>
      <guid>https://dev.to/achedon12/your-github-template-repo-is-a-one-time-copy-we-found-out-across-68-sites-26j3</guid>
      <description>&lt;p&gt;We run about a hundred repositories. Sixty-eight of them started life the same&lt;br&gt;
way: someone opened a GitHub template repo, clicked &lt;strong&gt;Use this template&lt;/strong&gt;, and&lt;br&gt;
got a working site.&lt;/p&gt;

&lt;p&gt;Last week I counted which version of that template each one was actually&lt;br&gt;
running. Twenty-eight distinct versions were live at the same time, from&lt;br&gt;
&lt;code&gt;1.0.0&lt;/code&gt; to &lt;code&gt;4.35.4&lt;/code&gt;. Four sites were still on major version 1.&lt;/p&gt;

&lt;p&gt;I want to be precise about the cause, because it is not what it looks like.&lt;br&gt;
Nobody had been lazy, and no process had broken down. The sites had diverged&lt;br&gt;
because &lt;strong&gt;there was never a mechanism for them not to.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The copy is one-way, and git will tell you so
&lt;/h2&gt;

&lt;p&gt;A repository created from a GitHub template shares &lt;strong&gt;no commit&lt;/strong&gt; with the&lt;br&gt;
template. Not a distant ancestor — none at all. GitHub squashes the template's&lt;br&gt;
history into a single initial commit in the new repo.&lt;/p&gt;

&lt;p&gt;Which means the obvious thing fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git remote add template https://github.com/acme/service-template.git
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git fetch template
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git merge template/main
&lt;span class="go"&gt;fatal: refusing to merge unrelated histories
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can force it with &lt;code&gt;--allow-unrelated-histories&lt;/code&gt;, and you should not. Git&lt;br&gt;
will then treat every file as a conflicting add, because from its point of view&lt;br&gt;
these are two unrelated projects that happen to have the same filenames.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git rebase&lt;/code&gt; has the same requirement, for the same reason. Both operations are&lt;br&gt;
defined in terms of a merge base, and there isn't one.&lt;/p&gt;

&lt;p&gt;So the template gains a security fix on day 30, a CI change on day 90, a&lt;br&gt;
dependency bump on day 200 — and none of it reaches any child repo. Someone&lt;br&gt;
copy-pastes it into a handful of repos, or it does not happen. At five repos&lt;br&gt;
that is tedious. At sixty-eight it does not happen.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cherry-pick does not need an ancestor
&lt;/h2&gt;

&lt;p&gt;There is exactly one git operation that does not care about a common ancestor:&lt;br&gt;
&lt;code&gt;cherry-pick&lt;/code&gt;. It does not merge histories, it &lt;strong&gt;applies a patch&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;git cherry-pick &lt;span class="nt"&gt;--no-commit&lt;/span&gt; &amp;lt;template-sha&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works perfectly well between two repositories that have never met.&lt;/p&gt;

&lt;p&gt;And it is not a workaround — it produces a strictly better result than copying&lt;br&gt;
files. Commit with the original author and date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%an &amp;lt;%ae&amp;gt;'&lt;/span&gt; &lt;span class="nv"&gt;$SHA&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;%aD &lt;span class="nv"&gt;$SHA&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;%B &lt;span class="nv"&gt;$SHA&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and the upstream fix stays &lt;em&gt;findable&lt;/em&gt; in the child repo's history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;span class="gp"&gt;7c8ddeb Fix: token refresh raced with retry      #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;authored upstream, months ago
&lt;span class="go"&gt;67e64eb Bump actions/checkout to v5
641c0bd Initial commit from template
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;rsync&lt;/code&gt; would have given you one opaque "sync with template" commit, dated&lt;br&gt;
today, authored by whoever ran the script. Six months later, when you are&lt;br&gt;
bisecting, that difference is the whole ballgame.&lt;/p&gt;
&lt;h2&gt;
  
  
  The hard part is not the cherry-pick
&lt;/h2&gt;

&lt;p&gt;Getting the patch to apply is a weekend. Getting it to apply &lt;strong&gt;without&lt;br&gt;
destroying what makes each repo its own&lt;/strong&gt; is where the real work is, and where&lt;br&gt;
we got it wrong more than once.&lt;/p&gt;
&lt;h3&gt;
  
  
  One protection rule is not enough
&lt;/h3&gt;

&lt;p&gt;Our first version had a single list of protected paths: on conflict, the local&lt;br&gt;
repo wins. It lost a site's entire branding within a week.&lt;/p&gt;

&lt;p&gt;The reason is subtle. A template commit can rewrite your logo &lt;strong&gt;cleanly&lt;/strong&gt; —&lt;br&gt;
no conflict, nothing for anyone to arbitrate, no warning. A conflict-only rule&lt;br&gt;
never fires, and the file is silently replaced.&lt;/p&gt;

&lt;p&gt;So there have to be two rules:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Files the repo owns outright&lt;/strong&gt; — logo, favicon, hostnames, &lt;code&gt;.env&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;docker-compose.yml&lt;/code&gt;. Restored from the repo after &lt;em&gt;every&lt;/em&gt; replayed commit,&lt;br&gt;
conflict or not. These are unmergeable by nature: there is no sensible&lt;br&gt;
three-way merge of a PNG or of a hostname.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Files the repo owns partly&lt;/strong&gt; — a theme stylesheet that is 90% template and&lt;br&gt;
10% your palette, a translations directory. Here the local version wins &lt;strong&gt;only&lt;br&gt;
on conflict&lt;/strong&gt;. Protecting them absolutely would deny them every upstream fix&lt;br&gt;
forever; not protecting them would erase your part on the first conflicting&lt;br&gt;
commit.&lt;/p&gt;

&lt;p&gt;Everything else belongs to the template. And every file the template overwrites&lt;br&gt;
gets printed at the end of the run — more on that below.&lt;/p&gt;
&lt;h3&gt;
  
  
  Merge commits are not skippable, they are forbidden
&lt;/h3&gt;

&lt;p&gt;Our first run stopped dead on a repo whose template history contained merges.&lt;br&gt;
&lt;code&gt;cherry-pick&lt;/code&gt; refuses a merge commit unless you name a mainline with &lt;code&gt;-m&lt;/code&gt;, and&lt;br&gt;
if you "unblock" it that way you duplicate content that both parents already&lt;br&gt;
contributed.&lt;/p&gt;

&lt;p&gt;The correct answer is &lt;code&gt;--no-merges&lt;/code&gt;, always. A merge commit carries no content&lt;br&gt;
of its own; both its parents are already in the range you are replaying.&lt;/p&gt;
&lt;h3&gt;
  
  
  Half-applied is worse than not applied
&lt;/h3&gt;

&lt;p&gt;If commit 14 of 20 fails, the tempting thing is to stop and report. Do not.&lt;/p&gt;

&lt;p&gt;The repo now carries part of the template's changes while its version file — and&lt;br&gt;
every dashboard, script and flag keyed off that version — claims a state it is&lt;br&gt;
not in. Every later run computes its range from that lie and compounds it.&lt;/p&gt;

&lt;p&gt;Roll the whole run back to where it started, tag the pre-run state first, and&lt;br&gt;
say which commit stopped you.&lt;/p&gt;
&lt;h3&gt;
  
  
  The 502 that git reported as a success
&lt;/h3&gt;

&lt;p&gt;The incident that shaped the tool more than any design discussion:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;config/nginx/shared.conf&lt;/code&gt; contains the PHP-FPM container name, repeated three&lt;br&gt;
times in &lt;code&gt;fastcgi_pass&lt;/code&gt; directives. It is not a logo, so it was not on anybody's&lt;br&gt;
protected list. The template's version overwrote it. nginx began looking for a&lt;br&gt;
container that did not exist on that host, and the site returned &lt;strong&gt;502 for&lt;br&gt;
every request&lt;/strong&gt; — while &lt;code&gt;git log&lt;/code&gt; showed a clean, successful, complete update.&lt;/p&gt;

&lt;p&gt;Nothing failed. That is exactly what made it bad.&lt;/p&gt;

&lt;p&gt;So now: every file the template wins is listed at the end of the run, by name.&lt;br&gt;
It is the only place a graft can lose something, so it is the one thing that&lt;br&gt;
must never be silent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;! The template won 1 conflicted file:
  config/nginx/shared.conf

This is the only place a graft loses something. If any of these belong to
this repo rather than the template, add them to `protect` in .regraft.yml
and re-run from 641c0bd.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Knowing where you left off
&lt;/h2&gt;

&lt;p&gt;The last piece: on the next run, which commits are missing?&lt;/p&gt;

&lt;p&gt;Storing that in a file means a file to keep in sync, to merge, and to lose. We&lt;br&gt;
put it in the commit itself, as a trailer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fix: token refresh raced with retry

Regraft-source: 3d9e77b0a1c4e8f2d5b9a7c6e3f1d8b2a4c9e7f5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next run reads the most recent one and starts from there. It survives&lt;br&gt;
clones, forks and rebases, because the record lives inside the history it&lt;br&gt;
describes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where this ended up
&lt;/h2&gt;

&lt;p&gt;I extracted the tooling into a CLI, because none of the above is specific to our&lt;br&gt;
stack — it is specific to &lt;em&gt;GitHub template repositories&lt;/em&gt;, which everyone uses&lt;br&gt;
and nobody can update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx regraft init &lt;span class="nt"&gt;--template&lt;/span&gt; https://github.com/acme/service-template.git
npx regraft status     &lt;span class="c"&gt;# read-only: what am I missing?&lt;/span&gt;
npx regraft apply      &lt;span class="c"&gt;# graft it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It drives the &lt;code&gt;git&lt;/code&gt; binary, so the repository can be in any language. There is&lt;br&gt;
also a GitHub Action that opens the pull requests on a schedule, including a&lt;br&gt;
Dependabot-style mode where each template commit becomes its own reviewable PR.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/achedon12/regraft" rel="noopener noreferrer"&gt;github.com/achedon12/regraft&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://achedon12.github.io/regraft/" rel="noopener noreferrer"&gt;achedon12.github.io/regraft&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT. &lt;code&gt;status&lt;/code&gt; touches nothing, so it is safe to point at a repo you care about&lt;br&gt;
just to see the number.&lt;/p&gt;




&lt;p&gt;If you maintain repos created from a template, run the count. Twenty-eight&lt;br&gt;
versions was not a number I expected to find, and I am fairly sure we are not&lt;br&gt;
unusual.&lt;/p&gt;

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