<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Antoine Bolvy</title>
    <description>Personal webpage.</description>
    <link>https://saveman71.com/</link>
    <atom:link href="https://saveman71.com/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Wed, 10 Dec 2025 13:31:19 +0000</pubDate>
    <lastBuildDate>Wed, 10 Dec 2025 13:31:19 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>HiDPI Electron &lt;37 apps on Sway</title>
        <description>&lt;p&gt;Electron 38 ships with Chromium 140 which landed &lt;a href=&quot;https://chromium-review.googlesource.com/c/chromium/src/+/6775426&quot;&gt;this change&lt;/a&gt; which made the Ozone (Chromium’s platform abstraction layer) feature detection “auto” by default (from, like not feature detecting at all, apparently). It bases it’s decision on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;XDG_SESSION_TYPE&lt;/code&gt; environment variable.&lt;/p&gt;

&lt;p&gt;Previously I always had to add those flags to the command line:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/usr/bin/obsidian &lt;span class=&quot;nt&quot;&gt;--enable-features&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;UseOzonePlatform &lt;span class=&quot;nt&quot;&gt;--ozone-platform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;wayland
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That works but is annoying: you either need a terminal open and remind yourself to &lt;em&gt;not&lt;/em&gt; use &lt;a href=&quot;https://albertlauncher.github.io/&quot;&gt;Albert&lt;/a&gt; (the launcher I’m using).&lt;/p&gt;

&lt;p&gt;I also tried patching the installed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.desktop&lt;/code&gt; files:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# copy and patch a desktop entry (one-off)&lt;/span&gt;
desktop-file-install &lt;span class=&quot;nt&quot;&gt;--dir&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HOME&lt;/span&gt;/.local/share/applications /usr/share/applications/obsidian.desktop
&lt;span class=&quot;c&quot;&gt;# then edit ~/.local/share/applications/obsidian.desktop and add the flags to Exec=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It worked, but I was looking for a more permanent solution.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://unix.stackexchange.com/a/768861/93390&quot;&gt;This answer on the Unix Stack Exchange&lt;/a&gt; hinted me to use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ELECTRON_OZONE_PLATFORM_HINT&lt;/code&gt; environment variable, which is supported by electron between version 28 and 38 (at which point it was removed, because unnecessary).&lt;/p&gt;

&lt;p&gt;I initially tried to edit my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.profile&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;ELECTRON_OZONE_PLATFORM_HINT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;wayland
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;However, this wasn’t working:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Albert was still launching apps with low DPI.&lt;/li&gt;
  &lt;li&gt;Launching the app from the terminal (without flags) finally used HiDPI, but I was back to square one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What worked was embracing systemd (that I don’t know enough about) and discovering a new way of setting environment variables:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; ~/.config/environment.d/electron-hidpi.conf
&lt;span class=&quot;nv&quot;&gt;ELECTRON_OZONE_PLATFORM_HINT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;wayland
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Another reboot later and Albert was finally launching apps with HiDPI.&lt;/p&gt;

&lt;p&gt;That will do, until all my apps are not using Electron 38 ;)&lt;/p&gt;
</description>
        <pubDate>Sat, 22 Nov 2025 17:55:00 +0000</pubDate>
        <link>https://saveman71.com/2025/make-electron-hidpi-on-sway</link>
        <guid isPermaLink="true">https://saveman71.com/2025/make-electron-hidpi-on-sway</guid>
        
        
        <category>linux</category>
        
        <category>wayland</category>
        
        <category>sway</category>
        
        <category>electron</category>
        
        <category>hidpi</category>
        
      </item>
    
      <item>
        <title>Do not store your translations in Git</title>
        <description>&lt;p&gt;For years, we committed our source translation files to Git. It seemed like the right thing to do—after all, they’re part of the codebase, right?&lt;/p&gt;

&lt;p&gt;Turns out, we were making our lives unnecessarily complicated.&lt;/p&gt;

&lt;p&gt;I probably Googled too many times how to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make gettext faster&lt;/code&gt; (&lt;a href=&quot;https://angelika.me/2020/09/02/speed-up-the-compilation-of-elixir-projects-that-use-gettext/&quot;&gt;Speed up the compilation of Elixir projects that use Gettext&lt;/a&gt;, &lt;a href=&quot;https://medium.com/multiverse-tech/how-to-speed-up-your-elixir-compile-times-part-1-understanding-elixir-compilation-64d44a32ec6e&quot;&gt;How to speed up your Elixir compile times&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If you’ve been there, keep reading.&lt;/p&gt;

&lt;h1 id=&quot;almost-everything-in-git&quot;&gt;(Almost) everything in Git&lt;/h1&gt;

&lt;p&gt;Our Elixir app uses Gettext, which is pretty much the standard for internationalization. We use Localazy as our translation management platform where our staff translates strings into various languages.&lt;/p&gt;

&lt;p&gt;When I joined, we had around 6,000 strings. Now we’re past 10,000. These translations live in PO files (PO stands for Portable Object, don’t ask), and there’s a POT file (T for Template) that drives our Localazy tool to check for new strings.&lt;/p&gt;

&lt;p&gt;The first part of the workflow went something like this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Developer adds a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gettext(&quot;some new string&quot;)&lt;/code&gt; call in the code&lt;/li&gt;
  &lt;li&gt;Developer runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix gettext.extract&lt;/code&gt; to sync the POT file with the codebase, they’d normally also update the PO file manually, but since we consider the code to be the source of truth, we actually wrote a small bit of code to generate the source language PO from the POT.&lt;/li&gt;
  &lt;li&gt;Developer commits both the code changes AND the PO/POT files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fortunately, we had already moved away from the automated PR downloading the translations and committing them, but we still had generated files committed. “It makes reviewing spelling mistakes easier” was something we told ourselves to make us feel better.&lt;/p&gt;

&lt;h1 id=&quot;the-problem-compile-time-complexity&quot;&gt;The Problem: Compile-Time Complexity&lt;/h1&gt;

&lt;p&gt;Here’s where things got interesting. Since PO files are parsed and gettext calls are inlined at compile time, each locale adds to compile time. We’re talking O(n) complexity where n is the number of locales.&lt;/p&gt;

&lt;p&gt;To keep everything “consistent”, we had this de facto standard: always sync the POT file with the list of gettext calls in the code (the source of truth), then commit both the PO (for the source language) and the POT file.&lt;/p&gt;

&lt;p&gt;This meant that for each commit (if you added even a single new gettext call), you theoretically needed to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix gettext.extract&lt;/code&gt;. This command recompiles the entire project to evaluate every gettext call and come up with the new strings in both PO and POT files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.5 minutes on a beefy laptop&lt;/strong&gt; it took. Yep, big monolith.&lt;/p&gt;

&lt;p&gt;The workflow went something like this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Once your PR is pushed, CI checks that everything is in sync with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix gettext.extract --check-up-to-date&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Once the CI check passes and merged to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;develop&lt;/code&gt;, a daily build sends new strings to Localazy&lt;/li&gt;
  &lt;li&gt;On deployment, we’d call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localazy download&lt;/code&gt; to get all translations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As stated, each PR was blocked by a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix gettext.extract --check-up-to-date&lt;/code&gt; task, as suggested in the &lt;a href=&quot;https://hexdocs.pm/gettext/Mix.Tasks.Gettext.Extract.html&quot;&gt;official docs&lt;/a&gt;, to check that the files were in sync with the code.&lt;/p&gt;

&lt;p&gt;Sounds reasonable, right? Well…&lt;/p&gt;

&lt;h1 id=&quot;why-we-stuck-with-this-workflow-for-so-long&quot;&gt;Why we stuck with this workflow for so long&lt;/h1&gt;

&lt;p&gt;This led to a bunch of different workflows, none of them great:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Some folks would remind themselves to run the extract task before every commit. Takes a minute and a half, breaks your flow, but hey, at least CI won’t fail.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Others were adding all their changes, then tacking on a single commit at the end called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gettext&lt;/code&gt;. Then maybe do a few fixes. Have a failed build. Add another &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gettext&lt;/code&gt; commit. Not ideal!&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For fun here’s an excerpt I got by running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git log --grep=&quot;gettext&quot;&lt;/code&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;49bf8947c0 Fixing post rebasing gettext
1184593889 extracting gettext after rebasing
bed14725d1 re-extract gettext following merge
da3a902c6f gettext rebase inconsistancies fix
fb35ff7db6 gettext
59ba9cea53 Update gettext
9bfc4f0fa7 Update gettext
5157e9f85b Update gettext
d04b956baa Update gettext
da096be3c6 Update gettext
fb9963e867 Update gettext
db0b180dc2 use gettext
458706dc87 Update gettext
4d56537088 chore: update gettext files
7f5c25d2a4 chore: format files and update gettext files
7da1412226 chore: update gettext files
a745b2f3a7 chore: update gettext files (again)
9fe8e478c9 chore: update gettext files
b93e30f897 chore: update gettext files
78ffae2e81 chore: update gettext files
c461756cf0 chore: update gettext files
3f3ca6df16 chore: update gettext files
387cbb2000 chore: update gettext files
69d8629e3e gettext
5960bb2b85 Fixed gettext error.
7944862c4c Update gettext
fcdf619670 Update gettext
4c46452dc8 Update gettext
0c89da460d Update gettext
bfc62e2664 Update gettext
6460b6f64d Use gettext
41c35c816d chore: update gettext files
42bc1eeccd update gettext
375591249d update gettext
34f7d00422 Update gettext stuff after rebase ...
aef7a8ed0e chore: update gettext files
f64b809baa mix gettext
bddd91148a chore: gettext
de0d27b8da mix gettext
516acaa05c chore: gettext and credo
1da19cf8bc mix gettext
4ec345fcd4 chore: update gettext files
849877911e Add gettext
5562a1a5fd Use of gettext_with_link
8697c57525 🌐 gettext
01e72f8aff 🌐 correct gettext for failure document creation
36d676dcce 🌐 updated gettext
df7e8ab5b8 mix gettext
bc80302380 mix gettext
2c5e6108e5 mix gettext
fca475f4df chore: update gettext files
27729fcb4a add virtual field + mix gettext
5cb03227a2 🌐 gettext
31be0b5769 chore: update gettext files
b679fd56fb chore(gettext): 💬 Update gettext keys
d01b0d1cea refactor: 💬 Add missing gettext
205127c537 chore: gettext
d290e1c44d gettext
42e00ddfec 🌐 added more missing gettexts
e8868cda16 🌐 added missing gettext
465f523b09 update gettext
d2442a819e chore: cleanup test gettext
c229b81d35 mix gettext
da5e36e8af mix gettext
af748b8c43 update gettext
534a7b14be mix gettext
9cf4b1fc9e chore: update gettext files
f3b8fa04d1 chore: update gettext files
70df45213c chore: update gettext files
a24d10398b chore: update gettext files
c4a0de5726 update gettext
68b0bda86c 🌐 gettext
edb112487c chore: update gettext files
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;…Yeah. To try to combat this, we had a very specialized Git pre-commit hook (I wrote it) that did the check automatically, so you wouldn’t discover you forgot to run it in CI. I don’t know if many enabled it!&lt;/p&gt;

&lt;p&gt;While a fun exercise, and a gentle reminder that bash is not a real programming language (albeit being the most portable choice, except, well, macOS) and that 400 lines of bash, even if unit tested, will break in any possible way. I believe I must have spent like a full week on that task. All to try having better commits. Duh.&lt;/p&gt;

&lt;p&gt;One day, while reviewing yet another &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gettext&lt;/code&gt; commit, it finally clicked.&lt;/p&gt;

&lt;h1 id=&quot;the-eureka-moment&quot;&gt;The Eureka Moment&lt;/h1&gt;

&lt;p&gt;We don’t have to live like this.&lt;/p&gt;

&lt;p&gt;We already have the source of truth: &lt;strong&gt;the source code itself&lt;/strong&gt;. The gettext calls in our Elixir files are what matters. Everything else is derived. Our translations don’t live in the repo anyway, they’re downloaded from Localazy.&lt;/p&gt;

&lt;p&gt;We could just extract the POT file when needed, when building in CI to upload to Localazy. And we pull all the PO files anyway when building for production. Sounds obvious now that I write about it.&lt;/p&gt;

&lt;p&gt;Two sources of truth. Multiple CI steps. Complex workflows. Pre-commit hooks. Slow extract commands blocking every commit. Conflicts! Do I continue?&lt;/p&gt;

&lt;h1 id=&quot;the-solution-extract-on-demand&quot;&gt;The Solution: Extract On Demand&lt;/h1&gt;

&lt;p&gt;That’s it. No more up-to-date checks blocking every commit. Just extract when needed, in a CI environment, not slowing you down. Upload when appropriate, and download translations as part of your deployment process.&lt;/p&gt;

&lt;p&gt;The diff said it all, with all the POT/POs deleted + 100s of lines of tooling:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;+49 lines added, -58,939 lines removed&lt;/strong&gt;&lt;/p&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;We won’t reclaim the hours of lost build time, but at least that’s some CPU cycles saved for everybody.&lt;/p&gt;

&lt;p&gt;Also, if you find yourself writing bash scripts to enforce a workflow, maybe step back and ask: is there a simpler way?&lt;/p&gt;
</description>
        <pubDate>Wed, 19 Nov 2025 22:43:42 +0000</pubDate>
        <link>https://saveman71.com/2025/do-not-store-your-translations-in-git</link>
        <guid isPermaLink="true">https://saveman71.com/2025/do-not-store-your-translations-in-git</guid>
        
        
        <category>elixir</category>
        
        <category>gettext</category>
        
        <category>translations</category>
        
        <category>git</category>
        
        <category>ci</category>
        
        <category>workflow</category>
        
        <category>localazy</category>
        
      </item>
    
      <item>
        <title>Cleaning up local branches after squash merges</title>
        <description>&lt;p&gt;For years, I’ve been using this handy one-liner to clean up merged branches:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git branch &lt;span class=&quot;nt&quot;&gt;--merged&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Ev&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;(^&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\*&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;|^&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\+&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;|production|develop|master)&quot;&lt;/span&gt; | xargs git branch &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It worked great: fetch the latest changes, run this command, and all the branches that had been merged would get deleted locally. Clean and simple.&lt;/p&gt;

&lt;h1 id=&quot;the-problem-squash-merges-break-everything&quot;&gt;The Problem: Squash Merges Break Everything&lt;/h1&gt;

&lt;p&gt;Then we enabled squash merges on our team’s repositories. Suddenly, my cleanup command stopped working. Because with squash merges, Git doesn’t consider the original branch as “merged” in the traditional sense:&lt;/p&gt;

&lt;p&gt;When you do a regular merge, Git creates a merge commit that has two parents - one from your branch and one from the target branch. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git branch --merged&lt;/code&gt; command can trace this history and knows your branch was incorporated.&lt;/p&gt;

&lt;p&gt;With squash merges, Git takes all your commits, squashes them into a single new commit, and applies that to the target branch. Your original branch history is essentially discarded. From Git’s perspective, your branch was never “merged” - the content was just copied over in a new commit and it has now way of knowing that it was ever merged.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git branch --merged&lt;/code&gt; then returns nothing, and you’re left manually deleting branches or letting them pile up indefinitely. Although I’m keen on letting refactors get out of hand, stashes piling up, and working directory full of uncommitted utility files and tools, branches are the next (PRs are usually up there) best thing to track ongoing work. Having too much around makes the output of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git branch&lt;/code&gt; pretty much useless and then you have to resort to sorting branches by date or some other criteria.&lt;/p&gt;

&lt;h1 id=&quot;a-better-solution&quot;&gt;A Better Solution&lt;/h1&gt;

&lt;p&gt;After getting frustrated with this a few too many times, I asked around my colleagues they just… don’t care. Okay then. Let’s turn to the internet: quick in our search we discover this Stack Overflow post:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/a/56026209/2367848&quot;&gt;How can I delete all git branches which have been “Squash and Merge” via GitHub?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A oneliner that had some drawbacks stands out on the most upvoted answer:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git checkout &lt;span class=&quot;nt&quot;&gt;-q&lt;/span&gt; master &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; git &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-each-ref&lt;/span&gt; refs/heads/ &lt;span class=&quot;s2&quot;&gt;&quot;--format=%(refname:short)&quot;&lt;/span&gt; | &lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;read &lt;/span&gt;branch&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;mergeBase&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git merge-base master &lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git cherry master &lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git commit-tree &lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git rev-parse &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;^{tree}&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$mergeBase&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; _&lt;span class=&quot;si&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;-&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; git branch &lt;span class=&quot;nt&quot;&gt;-D&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The core idea is sound ; it creates a synthetic commit that represents what your branch would look like if it were squash-merged, then uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git cherry&lt;/code&gt; to see if that commit already exists in the target branch. But it’s not immediately obvious what’s happening or why. It’s hard to understand: If it breaks or behaves unexpectedly, you’re in for a world of hurt.&lt;/p&gt;

&lt;p&gt;If it worked 100% of the time, this article wouldn’t exist. I don’t remember the exact details but it wasn’t enough. Also the lack of safety checks, and silent failures made it hard to debug.&lt;/p&gt;

&lt;p&gt;So I took ideas from there and there, and wrote a script (with the help of Gemini 2.5, at the time) that could help automating the process further and be more resilient to errors than the above oneliner.&lt;/p&gt;

&lt;h2 id=&quot;step-1-find-remote-branches-that-were-deleted&quot;&gt;Step 1: Find Remote Branches That Were Deleted&lt;/h2&gt;

&lt;p&gt;The script uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git remote prune --dry-run&lt;/code&gt; to see which remote tracking branches would be pruned:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;deleted_on_remote&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git remote prune &lt;span class=&quot;nv&quot;&gt;$REMOTE_NAME&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--dry-run&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;\[would prune\]&apos;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{print $NF}&apos;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s#^&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REMOTE_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/##&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This gives us a list of branch names that exist locally but have been deleted on the remote. The nice thing is that this is repeatable. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--dry-run&lt;/code&gt; flag makes the prune operation a noop so the script is repeatable.&lt;/p&gt;

&lt;h2 id=&quot;step-2-verify-before-deleting&quot;&gt;Step 2: Verify Before Deleting&lt;/h2&gt;

&lt;p&gt;A remote branch deleted doesn’t mean we should automatically delete the local version. What if there are local changes that weren’t pushed?&lt;/p&gt;

&lt;p&gt;For each branch marked for deletion, it:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Switches to the branch&lt;/li&gt;
  &lt;li&gt;Tries to merge it with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;develop&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;If the merge results in no changes (meaning the branch content was already squash-merged), it’s safe to delete&lt;/li&gt;
  &lt;li&gt;If there are differences, it skips deletion and warns you&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git switch &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
git merge develop &lt;span class=&quot;nt&quot;&gt;--no-edit&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--quiet&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Merge failed for &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;, skipping deletion.&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; git diff &lt;span class=&quot;nt&quot;&gt;--quiet&lt;/span&gt; develop&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Branch &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; has differences with develop, skipping deletion.&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;continue
fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;safety-features&quot;&gt;Safety Features&lt;/h2&gt;

&lt;p&gt;The script includes several safety features:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Autostash: Automatically stashes any uncommitted changes before starting, and pops them when done (or when something goes haywire)&lt;/li&gt;
  &lt;li&gt;Current branch protection: Never deletes the branch you’re currently on&lt;/li&gt;
  &lt;li&gt;Confirmation prompt: Shows you exactly which branches will be deleted and asks for confirmation&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;the-complete-script&quot;&gt;The Complete Script&lt;/h1&gt;

&lt;p&gt;Here’s the full script that you can drop into your project:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# --- Configuration ---&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;REMOTE_NAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;origin&quot;&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# Change if your remote has a different name&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# --- Colors ---&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;\033[0;31m&apos;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;\033[1;33m&apos;&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# For warnings or info&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;\033[0m&apos;&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# No Color&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# --- Autostash Setup ---&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;stash_created&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Function to pop the stash if it was created by this script&lt;/span&gt;
pop_stash&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$stash_created&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;-----------------------------&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Popping autostash...&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    git stash pop &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Warning: &apos;git stash pop&apos; failed. You may need to resolve conflicts manually.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Set a trap to ensure pop_stash is called on any script exit (normal, error, or user abort)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;trap &lt;/span&gt;pop_stash EXIT

&lt;span class=&quot;c&quot;&gt;# --- Safety Check: Ensure we are in a git repository ---&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; git rev-parse &lt;span class=&quot;nt&quot;&gt;--is-inside-work-tree&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Error: Not inside a Git repository.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# --- Autostash Execution ---&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Check for uncommitted changes (staged or unstaged)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; git diff-index &lt;span class=&quot;nt&quot;&gt;--quiet&lt;/span&gt; HEAD &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Uncommitted changes detected. Creating an autostash...&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  git stash push &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;autostash-by-cleanup-script-&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; +%s&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;stash_created&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;true
  echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;-----------------------------&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fi

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Checking for local branches whose remote tracking branches on &apos;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REMOTE_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&apos; have been deleted...&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# --- Step 1: Get list of remote branches deleted on the specified remote ---&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Use &apos;git remote prune --dry-run&apos; which is designed for this.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Output is like: * [would prune] origin/branch-name&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# We extract the part after &apos;origin/&apos;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;deleted_on_remote_raw&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git remote prune &lt;span class=&quot;nv&quot;&gt;$REMOTE_NAME&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--dry-run&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$?&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-ne&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Error checking remote prune status. Output:&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$deleted_on_remote_raw&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
&lt;span class=&quot;k&quot;&gt;fi

&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;deleted_on_remote&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$deleted_on_remote_raw&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;\[would prune\]&apos;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{print $NF}&apos;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s#^&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REMOTE_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/##&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$deleted_on_remote&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;No remote branches seem to have been deleted on &apos;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REMOTE_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&apos; since last fetch/prune.&quot;&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# Optional: Still run fetch -p to actually prune remote refs if needed&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# echo &quot;Running &apos;git fetch -p&apos; to update remote refs...&quot;&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# git fetch -p $REMOTE_NAME&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# --- Step 2: Get list of local branches ---&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Use plumbing &apos;git for-each-ref&apos; for robustness&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;local_branches&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-each-ref&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;%(refname:short)&apos;&lt;/span&gt; refs/heads/&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# --- Step 3: Get the current branch ---&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Use plumbing &apos;git symbolic-ref&apos;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;current_branch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git symbolic-ref &lt;span class=&quot;nt&quot;&gt;--short&lt;/span&gt; HEAD 2&amp;gt;/dev/null&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;current_branch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# --- Step 4: Compare and identify local branches to potentially delete ---&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;declare&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; branches_to_delete &lt;span class=&quot;c&quot;&gt;# Array to hold branches marked for deletion&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;declare&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; branches_to_display &lt;span class=&quot;c&quot;&gt;# Array to hold display lines&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Comparing with local branches:&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;-----------------------------&quot;&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;has_branches_to_delete&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;false

&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;IFS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; local_branch&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# Ignore the current branch - never delete it automatically&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$local_branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$current_branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;branches_to_display+&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;* &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;local_branch&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; (current branch)&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;continue
  fi&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Check if this local branch exists in the list of deleted remote branches&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# Use grep -Fxq for exact, fixed string, quiet match within the list&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Fxq&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$local_branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$deleted_on_remote&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;# Found a match - mark for deletion&lt;/span&gt;
    branches_to_display+&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;- &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;local_branch&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; (remote deleted)&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    branches_to_delete+&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$local_branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;has_branches_to_delete&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;true
  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;# No match - just display normally&lt;/span&gt;
    branches_to_display+&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;local_branch&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;fi
done&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$local_branches&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# --- Step 5: Display the list ---&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;${#&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;branches_to_display&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[@]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-eq&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;No local branches found.&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Print the collected display lines&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;line &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;branches_to_display&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[@]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done
&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;-----------------------------&quot;&lt;/span&gt;


&lt;span class=&quot;c&quot;&gt;# --- Step 6: Check if any branches are marked for deletion ---&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$has_branches_to_delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;No local branches correspond to deleted remote branches.&quot;&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# Optional: Run fetch -p here if you want to ensure pruning happens even if no local match&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# echo &quot;Running &apos;git fetch -p&apos; to update remote refs...&quot;&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# git fetch -p $REMOTE_NAME&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0
&lt;span class=&quot;k&quot;&gt;fi

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# Newline for clarity before prompt&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# --- Step 7: Ask for confirmation ---&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Use &apos;-ei &quot;y&quot;&apos; in read for bash &amp;gt;= 4 (default to &apos;y&apos;)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Fallback for older bash&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; Are you sure you want to delete the &lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;RED&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;local &lt;/span&gt;branches listed above? &lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;y/N&lt;span class=&quot;se&quot;&gt;\)&lt;/span&gt;:&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &quot;&lt;/span&gt; response
&lt;span class=&quot;nv&quot;&gt;response_lower&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$response&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tr&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;[:upper:]&apos;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;[:lower:]&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# portable lowercase&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# --- Step 8: Perform deletion if confirmed ---&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$response_lower&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;y&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Proceeding with deletion...&quot;&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;deleted_count&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;0
  &lt;span class=&quot;nv&quot;&gt;error_count&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;0
  &lt;span class=&quot;c&quot;&gt;# Actually prune the remote tracking refs first&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# echo &quot;Running &apos;git fetch -p $REMOTE_NAME&apos; to sync remote refs...&quot;&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# git fetch -p $REMOTE_NAME&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Deleting local branches...&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;branch &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;branches_to_delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[@]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Deleting local branch &apos;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&apos;... &quot;&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;# Use &apos;git branch -d&apos; (porcelain, but standard and safer as it checks for merged status)&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;# Use &apos;git branch -D&apos; for force deletion if desired.&lt;/span&gt;

    git switch &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    git merge develop &lt;span class=&quot;nt&quot;&gt;--no-edit&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--quiet&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Merge failed for &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;, skipping deletion.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
      git merge &lt;span class=&quot;nt&quot;&gt;--abort&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;error_count++&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;# check if we have some difference with develop&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; git diff &lt;span class=&quot;nt&quot;&gt;--quiet&lt;/span&gt; develop&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then&lt;/span&gt;
      &lt;span class=&quot;c&quot;&gt;# assert slate is pristine before --hard reset&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; git diff &lt;span class=&quot;nt&quot;&gt;--quiet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Local changes detected in &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;, aborting merge.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;continue
      fi
      &lt;/span&gt;git reset &lt;span class=&quot;nt&quot;&gt;--hard&lt;/span&gt; HEAD~1
      &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Branch &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; has differences with develop, skipping deletion.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;error_count++&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;continue
    fi

    &lt;/span&gt;git switch develop

    &lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;git branch &lt;span class=&quot;nt&quot;&gt;-D&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
      &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Deleted.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;deleted_count++&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else
      &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Failed for &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$branch&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;((&lt;/span&gt;error_count++&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fi
  done
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;-----------------------------&quot;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Deletion complete. &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$deleted_count&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; branches deleted.&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$error_count&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-gt&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
      &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$error_count&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; branches failed to delete (see messages above).&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1 &lt;span class=&quot;c&quot;&gt;# Exit with error status if deletions failed&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;fi
else
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Aborted by user. No local branches were deleted.&quot;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;You might still want to run &apos;git fetch -p &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REMOTE_NAME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&apos; to prune remote-tracking references.&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fi

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Wed, 01 Oct 2025 12:12:42 +0000</pubDate>
        <link>https://saveman71.com/2025/cleaning-up-local-branches-after-squash-merges</link>
        <guid isPermaLink="true">https://saveman71.com/2025/cleaning-up-local-branches-after-squash-merges</guid>
        
        
        <category>git</category>
        
        <category>branches</category>
        
        <category>cleanup</category>
        
        <category>squash</category>
        
        <category>merge</category>
        
      </item>
    
      <item>
        <title>Fixing Flaky Elixir Tests with Randomized Query Ordering</title>
        <description>&lt;p&gt;Flaky tests are the bane of a developer’s existence - they pass locally but fail in CI, or worse, pass 9 times out of 10 only to mysteriously fail when you least expect it. A failing CI build leads to fatigue and frustration for everyone involved, so any flaky test should be analyzed and fixed.&lt;/p&gt;

&lt;p&gt;We quickly understood that a good part of those flaky tests had the same root cause: database queries without explicit ordering.&lt;/p&gt;

&lt;h1 id=&quot;the-problem-unstable-sorting-in-database-queries&quot;&gt;The Problem: Unstable Sorting in Database Queries&lt;/h1&gt;

&lt;p&gt;When we write a query like this:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;no&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;u&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;SQL makes no such guarantee unless you explicitly specify an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ORDER BY&lt;/code&gt; clause. However, since PostgreSQL often returns results in the same order due to how data is stored and accessed, this lead to subtle bugs in tests: we might write assertions that expect specific ordering, and they’ll pass most of the time by coincidence.&lt;/p&gt;

&lt;p&gt;But occasionally, when database statistics change or PostgreSQL chooses a different query plan, the order will change and our tests will fail. These are the dreaded flaky tests. Surprisingly, it happens more often in the CI than locally, leading to hard to reproduce failures.&lt;/p&gt;

&lt;h1 id=&quot;a-proposed-solution-enforcing-randomness&quot;&gt;A proposed solution: Enforcing Randomness&lt;/h1&gt;

&lt;p&gt;I came up with a solution that’s both simple and effective: what if we could make the ordering explicitly random during tests? This would turn intermittent failures into consistent ones, forcing us to fix the underlying issues.&lt;/p&gt;

&lt;p&gt;Here’s the trick - we can override Ecto’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prepare_query/3&lt;/code&gt; callback to add a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ORDER BY RANDOM()&lt;/code&gt; to any query that doesn’t already have an ordering specified:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;WebApp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Repo&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;@doc&lt;/span&gt; &lt;span class=&quot;sd&quot;&gt;&quot;&quot;&quot;
  Testing helper that enforces sorts in tests/queries. It helps tracking flaky tests and will make them more obvious as the order won&apos;t
  be stable unless an order_by clause is specified.
  &quot;&quot;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Mix&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:test&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;@impl&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prepare_query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;operation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:all&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;order_bys&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;is_nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;distinct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;combinations&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
          &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Ecto&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Query&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;order_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fragment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;RANDOM()&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prepare_query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_operation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This implementation uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prepare_query/3&lt;/code&gt; callback from &lt;a href=&quot;https://hexdocs.pm/ecto/Ecto.Repo.html#c:prepare_query/3&quot;&gt;Ecto.Repo&lt;/a&gt;, which allows us to transform queries before they’re executed. When it detects a query without ordering, it adds &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ORDER BY RANDOM()&lt;/code&gt; (excluding some specific queries such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DISTINCT&lt;/code&gt; which don’t support/require ordering, naturally).&lt;/p&gt;

&lt;p&gt;The beauty of this approach is that it only affects your test environment and even prevents new flaky tests from being introduced - they’ll fail immediately during development rather than randomly in CI months later.&lt;/p&gt;

&lt;h1 id=&quot;finding-all-the-flaky-tests&quot;&gt;Finding All the Flaky Tests&lt;/h1&gt;

&lt;p&gt;With our new randomized ordering in place, we’ll quickly discover tests that were silently depending on implicit ordering. But there’s a catch - since the ordering is random, we need to run our test suite multiple times to catch all the issues.&lt;/p&gt;

&lt;p&gt;Since I knew about the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix test --failed&lt;/code&gt; command, which runs tests that just failed, I knew there was some kind of &lt;em&gt;manifest&lt;/em&gt; somewhere that would store the failing tests on disk. After some digging, I discovered it was in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_build&lt;/code&gt; directory, more specifically in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_build/test/lib/webapp/.mix/.mix_test_failures&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;So let’s write a script to capture these failures and store them in a more stable file:&lt;/p&gt;

&lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;AddStableFailures&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# The initial failed manifest&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;@failed_manifest&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;_build/test/lib/webapp/.mix/.mix_test_failures&quot;&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# The file to store persistent failures&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;@stable_manifest&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;stable_failures&quot;&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;run&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exists?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;@failed_manifest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;puts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Failed manifest file not found: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;@failed_manifest&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;failed_tests&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
      &lt;span class=&quot;nv&quot;&gt;@failed_manifest&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:erlang&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;binary_to_term&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;elem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;o&quot;&gt;|&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;group_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_mod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;existing_stable_failures&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;read_stable_failures&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;new_stable_failures&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;failed_tests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing_stable_failures&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;no&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tests&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;test_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;test_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

          &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;member?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
            &lt;span class=&quot;no&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;puts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Test already in stable manifest: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;test_name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; from &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
            &lt;span class=&quot;no&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;puts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Adding stable failure: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;test_name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; from &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Write the entire list back to the file as a single term.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;write_stable_failures&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;new_stable_failures&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;write_stable_failures&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;terms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;write!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;@stable_manifest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:erlang&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;term_to_binary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;terms&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;defp&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;read_stable_failures&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;@stable_manifest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;binary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:erlang&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;binary_to_term&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;binary&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:enoent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;AddStableFailures&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s how to use it:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Run your test suite: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix test&lt;/code&gt;, some but not all flaky tests will fail.&lt;/li&gt;
  &lt;li&gt;Run this script to capture failures: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;elixir accumulate_failures.exs&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Repeat steps 1-2 several times to build up a complete list&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After a few iterations, you’ll have a pretty comprehensive list of all the tests affected by ordering issues.&lt;/p&gt;

&lt;h1 id=&quot;fixing-the-tests&quot;&gt;Fixing the Tests&lt;/h1&gt;

&lt;p&gt;Once you’ve identified the flaky tests, fixing them is usually straightforward:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Add explicit ordering to your queries:
    &lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Before&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;u&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# After&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;users&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;u&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;order_by:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;u&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;Sort results in memory if the order doesn’t matter for the database:
    &lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Before&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# After&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sort_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&amp;amp;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sort_by&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Repo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;User&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;&amp;amp;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;Remove order dependence from the test if possible:
    &lt;div class=&quot;language-elixir highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Before&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[%{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;%{&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Bob&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# After&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;any?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&amp;amp;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;any?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;&amp;amp;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Bob&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;the-results&quot;&gt;The Results&lt;/h1&gt;

&lt;p&gt;After implementing this approach at work, we quickly identified and fixed a dozen of flaky tests, most having never been seen before. Our CI became more reliable which is always a good thing.&lt;/p&gt;

&lt;p&gt;What’s more, this approach catches potential flaky tests during development. When a developer writes a new test that implicitly depends on ordering, it fails immediately (well, it’s still random so it might take a few runs) on their machine rather than randomly in CI weeks later.&lt;/p&gt;

&lt;p&gt;As an added bonus, we’ve made our codebase more robust by adding explicit ordering at places we had forgotten about.&lt;/p&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;Flaky tests cause a lot of frustration, but they are surprisingly easy to fix once you know what to look for. A good first thing is to ask your team to start tracking failing builds so you can start accumulating data on the frequency and nature of these issues. This will help you prioritize fixes and ensure that your CI pipeline remains stable over time.&lt;/p&gt;
</description>
        <pubDate>Sat, 30 Aug 2025 12:12:42 +0000</pubDate>
        <link>https://saveman71.com/2025/fixing-flaky-elixir-tests-with-random-ordering</link>
        <guid isPermaLink="true">https://saveman71.com/2025/fixing-flaky-elixir-tests-with-random-ordering</guid>
        
        
        <category>elixir</category>
        
        <category>postgres</category>
        
        <category>testing</category>
        
        <category>flaky-tests</category>
        
        <category>ecto</category>
        
        <category>random</category>
        
      </item>
    
      <item>
        <title>Local Elixir development with ASDF</title>
        <description>&lt;p&gt;Some notes I took while submitting my first PR to the Elixir repo.&lt;/p&gt;

&lt;h1 id=&quot;local-development-of-elixir-with-asdf&quot;&gt;Local development of Elixir with ASDF&lt;/h1&gt;

&lt;p&gt;First clone &lt;a href=&quot;https://github.com/elixir-lang/elixir&quot;&gt;(following the README)&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git clone https://github.com/elixir-lang/elixir
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;elixir
make
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Takes a minute to compile, then binaries sit in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bin/&lt;/code&gt;. I like it a lot that the whole build process is only a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make&lt;/code&gt; away, refreshing!&lt;/p&gt;

&lt;h2 id=&quot;configuring-asdf&quot;&gt;Configuring &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;asdf&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;Now we want to use that version in our project. Our project uses ASDF to manage our tools, so we need to tell ASDF about the local version.&lt;/p&gt;

&lt;p&gt;Edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.tools-version&lt;/code&gt; like so:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-dif&quot;&gt;diff --git a/.tool-versions b/.tool-versions
index 0a011338b4..d5ecff1f5a 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,3 +1,3 @@
-elixir 1.15.6-otp-26
+elixir path:../elixir
 erlang 26.1
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here I cloned the Elixir repo in the parent directory of my project, so I can use a relative path. You can use an absolute path too.&lt;/p&gt;

&lt;h2 id=&quot;testing&quot;&gt;Testing&lt;/h2&gt;

&lt;p&gt;In your repo, run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iex&lt;/code&gt;, and you should see the version you just compiled:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;~/git/elixir-dev elixir-dev&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;
❯ iex
Erlang/OTP 26 &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;erts-14.1] &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;64-bit] &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;smp:20:20] &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;ds:20:20:10] &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;async-threads:1] &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;jit:ns]

Interactive Elixir &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1.17.0-dev&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; - press Ctrl+C to &lt;span class=&quot;nb&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;type &lt;/span&gt;h&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; ENTER &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;help&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                           ^^^
                             |
                             +-- here!
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Mon, 20 May 2024 12:12:42 +0000</pubDate>
        <link>https://saveman71.com/2024/local-elixir-development</link>
        <guid isPermaLink="true">https://saveman71.com/2024/local-elixir-development</guid>
        
        
        <category>elixir</category>
        
        <category>asdf</category>
        
        <category>local</category>
        
        <category>bin</category>
        
        <category>development</category>
        
      </item>
    
      <item>
        <title>Streamlining `git commit --fixup` and `git rebase --autosquash` with `gfrauto`</title>
        <description>&lt;p&gt;One of the things I like about Git is the ability to fixup commits. It’s a great way to keep the
history clean and make sure that changes are grouped together, in logical, atomic commits.&lt;/p&gt;

&lt;p&gt;This makes reviewing easier, and I want to provide tooling to other folks that might not know about
this feature at all – or might not be using it as much as they could because it’s a bit
cumbersome.&lt;/p&gt;

&lt;h1 id=&quot;intro&quot;&gt;Intro&lt;/h1&gt;

&lt;p&gt;The problem with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git commit --fixup&lt;/code&gt; is that it requires you to provide the commit hash of the
commit you want to fixup. The usual workflow is to blame the line you want to fix (which is a pain,
because you’ll have a ton of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Not Committed Yet&lt;/code&gt; lines), copy the hash, and then run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git commit
--fixup &amp;lt;hash&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Helpful tip though, you can get rid of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Not Committed Yet&lt;/code&gt; lines by adding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HEAD&lt;/code&gt; to the blame
command. For example, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git blame HEAD -- &amp;lt;file&amp;gt;&lt;/code&gt;, but that’s not something that the Sublime Text
plugin I use does.&lt;/p&gt;

&lt;p&gt;Then, when you’re ready to squash the fixup commit, you need to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git rebase -i --autosquash
main&lt;/code&gt;, it opens the editor, then save and quit.&lt;/p&gt;

&lt;p&gt;Hopefully, you correctly identified the commit you wanted to fixup, and you’re done.&lt;/p&gt;

&lt;p&gt;Now, imagine we get a PR review, and we have 10s of changes to make. That’s a lot of manual work.&lt;/p&gt;

&lt;h1 id=&quot;automated-fixup-with-gfra&quot;&gt;Automated fixup with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gfra&lt;/code&gt;&lt;/h1&gt;

&lt;p&gt;Now the function I’ll introduce doesn’t solve the problem of identifying the commit you want to
fixup, but it does automate the process of creating the fixup commit and rebasing interactively to
autosquash the changes.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gfra&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    git commit &lt;span class=&quot;nt&quot;&gt;--fixup&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-verify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; git rebase &lt;span class=&quot;nt&quot;&gt;--autosquash&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;^&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Bash experts out there will say “ha, that’s simple”, and it is. Easier than going back again and
again in your history to find the command though.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gfra&lt;/code&gt; name is inspired from the zsh git aliases family, and are just initials for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git fixup
rebase autosquash&lt;/code&gt;, just like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gs&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git status&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;The usage is simple:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gfra &amp;lt;commit-hash&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;automating-commit-fixes-with-gfrauto&quot;&gt;Automating Commit Fixes with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gfrauto&lt;/code&gt;&lt;/h1&gt;

&lt;p&gt;While &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gfra&lt;/code&gt; nails the process of fixing and rebasing a single commit, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gfrauto&lt;/code&gt; takes it a step
further – or more like ten – by automating the detection of the commit that modified a particular
line in a staged file. No more manual blame, copy-pasting, or rebase editing.&lt;/p&gt;

&lt;p&gt;Without further ado, here is the function (zsh syntax for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;read&lt;/code&gt; commands, so be careful if you
use it in bash):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gfrauto&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# Fist, display the changes&lt;/span&gt;
  git diff &lt;span class=&quot;nt&quot;&gt;--staged&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;?Continue? ^C to cancel&quot;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Extract the line number from the diff&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git diff &lt;span class=&quot;nt&quot;&gt;--staged&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--unified&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;0  | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Po&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;^@@ -[0-9]+(,[0-9]+)? \+\K[0-9]+(,[0-9]+)?(?= @@)&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# If we have multiple files, arbitrarily pick the first one&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Line is either a single line or a range of lines&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# E.g. 10 or 10,20&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;# If it&apos;s a range, we need to split it and get the first line&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;, &lt;span class=&quot;nt&quot;&gt;-f1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Extract the file name from the diff&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git diff &lt;span class=&quot;nt&quot;&gt;--staged&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--unified&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;0 | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Po&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;^\+\+\+ ./\K.*&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# If we have multiple files, arbitrarily pick the first one&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$file&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Find the commit that last modified the line in the file&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;commit&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git log &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;,&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;:&lt;span class=&quot;nv&quot;&gt;$file&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--pretty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;format:&lt;span class=&quot;s2&quot;&gt;&quot;%h&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Display the hash, the description of the commit for confirmation&lt;/span&gt;
  git show &lt;span class=&quot;nt&quot;&gt;--no-patch&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;%h %s&quot;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$commit&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;?Continue? ^C to cancel&quot;&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;# Let the magic happen&lt;/span&gt;
  gfra &lt;span class=&quot;nv&quot;&gt;$commit&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;usage-1&quot;&gt;Usage&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Stage your changes, as usual, keep them light since the script is dumb and will just base its
search on the first file it finds. Make sure you only stage changes that belong to the same
commit.&lt;/li&gt;
  &lt;li&gt;Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gfrauto&lt;/code&gt; and confirm the diff, then the found commit.&lt;/li&gt;
  &lt;li&gt;That’s it, it’s fixed up and rebase automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part is that if your shell history dedupes entries, you won’t ever clutter your history with different calls to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gfra&lt;/code&gt; function.&lt;/p&gt;

&lt;h1 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h1&gt;

&lt;p&gt;I’ve used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gfra&lt;/code&gt; for a while and consider it battle tested. The first usage in my zsh history is on
dates from more than 5 years ago and I use it multiple times a day. I however don’t pretend to take
credit on such a simple function.&lt;/p&gt;

&lt;p&gt;For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gfrauto&lt;/code&gt;, it’s been present as a long one-liner in my history, refined over time. Here is it
for posterity:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Do not use this&lt;/span&gt;
gfra &lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;gd &lt;span class=&quot;nt&quot;&gt;--staged&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--unified&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;0  | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Po&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;^@@ -[0-9]+(,[0-9]+)? \+\K[0-9]+(,[0-9]+)?(?= @@)&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;gd &lt;span class=&quot;nt&quot;&gt;--staged&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--unified&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;0 | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Po&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;^\+\+\+ ./\K.*&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; git log &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;,&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;:&lt;span class=&quot;nv&quot;&gt;$file&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--pretty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;format:&lt;span class=&quot;s2&quot;&gt;&quot;%h&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Obviously, it’s more cryptic, handled less edge cases, so I finally took the time to write it down
in a more readable form, and I’m happy to share it today.&lt;/p&gt;
</description>
        <pubDate>Wed, 01 May 2024 12:12:42 +0000</pubDate>
        <link>https://saveman71.com/2024/streamlining-rebase-autosquash</link>
        <guid isPermaLink="true">https://saveman71.com/2024/streamlining-rebase-autosquash</guid>
        
        
        <category>git</category>
        
        <category>rebase</category>
        
        <category>commit</category>
        
        <category>fixup</category>
        
        <category>autosquash</category>
        
        <category>gfrauto</category>
        
      </item>
    
      <item>
        <title>A simple Sublime Text plugin to copy a file path from the project root</title>
        <description>&lt;p&gt;A few days ago, the author of
&lt;a href=&quot;https://packagecontrol.io/packages/SideBarEnhancements&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SideBarEnhancements&lt;/code&gt;&lt;/a&gt; slimmed down his
package removing a lot of functionnality. One I used many times per day was the ability to copy a
file path from the project root to then use in various commands such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mix test &amp;lt;filepath&amp;gt;&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;I wanted to create a standalone plugin that provided similar functionnality. Hopefully, this is
quite simple to do:&lt;/p&gt;

&lt;p&gt;We’ll need to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;write a Python script that Sublime Text can execute as a plugin&lt;/li&gt;
  &lt;li&gt;create a command palette entry for it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;creating-the-python-plugin&quot;&gt;Creating the Python Plugin&lt;/h2&gt;

&lt;p&gt;Open Sublime Text and go to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Tools&lt;/code&gt; &amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Developer&lt;/code&gt; &amp;gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;New Plugin...&lt;/code&gt;. This will open a new tab with
a template for a new plugin. Then we’ll add the following:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sublime&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;sublime_plugin&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;os&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CopyPathFromProjectRootCommand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sublime_plugin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TextCommand&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;edit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Get the current file path
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;file_path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;file_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;sublime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status_message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;No file to copy path from.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
        
        &lt;span class=&quot;c1&quot;&gt;# Find the project root
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;window&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;view&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;folders&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;window&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;folders&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;project_root&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;folder&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;folders&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;file_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startswith&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;folder&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;project_root&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;folder&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;
        
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;project_root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;sublime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status_message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;File is not in any of the loaded projects.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
        
        &lt;span class=&quot;c1&quot;&gt;# Get the relative path from project root and copy it to clipboard
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;relative_path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;relpath&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;file_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;project_root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;sublime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;set_clipboard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;relative_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;sublime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status_message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Path copied to clipboard: &quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;relative_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Save the file (if you created the file using the above menu, it should already be in the right
folder, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;User&lt;/code&gt;).&lt;/p&gt;

&lt;h2 id=&quot;creating-an-entry-in-the-command-palette&quot;&gt;Creating an entry in the command palette&lt;/h2&gt;

&lt;p&gt;Create a new file named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CopyPathFromProjectRoot.sublime-commands&lt;/code&gt; in the same folder, and add the
following JSON code to this file:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;caption&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Copy Path From Project Root&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;copy_path_from_project_root&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;/h2&gt;

&lt;p&gt;You now have a new command in the command palette, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Copy Path From Project Root&lt;/code&gt;. You can use it to
copy the relative path of the current file to the clipboard. That’s it!&lt;/p&gt;

&lt;p&gt;You can also bind the command to a key combination, for example:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;keys&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ctrl+shift+c&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;copy_path_from_project_root&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(add this to your user keybindings file)&lt;/p&gt;
</description>
        <pubDate>Mon, 11 Mar 2024 12:12:42 +0000</pubDate>
        <link>https://saveman71.com/2024/creating-a-simple-sublime-text-plugin</link>
        <guid isPermaLink="true">https://saveman71.com/2024/creating-a-simple-sublime-text-plugin</guid>
        
        
        <category>sublimetext</category>
        
        <category>sublime</category>
        
        <category>plugin</category>
        
        <category>python</category>
        
        <category>clipboard</category>
        
        <category>project</category>
        
        <category>root</category>
        
        <category>relative</category>
        
      </item>
    
      <item>
        <title>Making IEx react to ^D and ^L (quit and clear) with tmux bindings</title>
        <description>&lt;p&gt;I’ve been using tmux for a while now, and I recently started a new position where I’m using Elixir.&lt;/p&gt;

&lt;p&gt;I’ve been using it to run IEx, the Elixir REPL. I’ve been annoyed by the fact that IEx doesn’t react to ^D (quit) and ^L (clear screen) like other REPLs do, so I decided look for a solution.&lt;/p&gt;

&lt;p&gt;Quitting IEX being almost like &lt;a href=&quot;https://stackoverflow.com/questions/30085376/another-way-to-exiting-iex-other-than-ctrl-c&quot;&gt;quitting Vi&lt;/a&gt;, and since the team &lt;a href=&quot;https://github.com/erlang/otp/issues/4414&quot;&gt;doesn’t seem to have any intention&lt;/a&gt; to change this, I decided to use tmux bindings to make it happen.&lt;/p&gt;

&lt;p&gt;I was inspired by &lt;a href=&quot;https://elixirforum.com/t/how-to-fix-iex-ctrl-d-exit-and-ctrl-l-clear-with-autokey/26075&quot;&gt;this post&lt;/a&gt; which uses &lt;a href=&quot;https://github.com/autokey/autokey&quot;&gt;AutoKey&lt;/a&gt; but it only supports X11 and I didn’t like the idea of interfacing too far away from the terminal.&lt;/p&gt;

&lt;p&gt;As it’s not possible to add key bindings directly at the shell level, the next best thing is to use tmux bindings.&lt;/p&gt;

&lt;p&gt;I added the following lines to my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.tmux.conf&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-tmux&quot;&gt;bind -n C-d if -F &quot;#{m:*iex*,#{pane_title}}&quot; &apos;send-keys C-\\&apos; &quot;send-keys C-d&quot;
bind -n C-l if -F &quot;#{m:*iex*,#{pane_title}}&quot; &apos;send-keys clear\n&apos; &quot;send-keys C-l&quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;All it does is checking that the current pane runs iex, and if so, it sends the appropriate key sequence.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;C-\&lt;/code&gt; is the sequence for quitting iex&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clear\n&lt;/code&gt; is the sequence for clearing the screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the pane doesn’t run iex, it just sends the unmodified key sequence.&lt;/p&gt;

&lt;p&gt;I’m pretty happy with the result, and I hope it can help someone else.&lt;/p&gt;
</description>
        <pubDate>Wed, 26 Apr 2023 09:25:46 +0000</pubDate>
        <link>https://saveman71.com/2023/making-iex-react-to-d-and-l-quit-and-clear-with-tmux-bindings</link>
        <guid isPermaLink="true">https://saveman71.com/2023/making-iex-react-to-d-and-l-quit-and-clear-with-tmux-bindings</guid>
        
        
        <category>tmux</category>
        
        <category>elixir</category>
        
        <category>iex</category>
        
      </item>
    
      <item>
        <title>How to remove trailing slash on Jekyll with Cloudflare</title>
        <description>&lt;p&gt;Previously all my blog posts were served from Github pages with a trailing slash, for example:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;https://saveman71.com/2023/some-article/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is because the permalink setting of Jekyll &lt;em&gt;had&lt;/em&gt; a trailing slash included. Wanting to change that for cleanliness and ✨SEO reasons✨, I made the &lt;a href=&quot;https://github.com/saveman71/saveman71.github.io/commit/888a8d34319dffeb8b04bcc8acea09e6cccf4445#diff-ecec67b0e1d7e17a83587c6d27b6baaaa133f42482b07bd3685c77f34b62d883L16-L17&quot;&gt;following change&lt;/a&gt; a few days ago:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/2023-01-09-remove-trailing-slash-cloudflare-jekyll/diff.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;All was well, until I learnt via Google’s search console that I had more and more 404s. &lt;a href=&quot;https://github.com/slorber/trailing-slash-guide&quot;&gt;This guide&lt;/a&gt; came in handy in identifying the culprit: GitHub pages doesn’t do anything about trailing slashes. If it’s there, it considers it a directory and looks for an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.html&lt;/code&gt; file in there. Since the change was made, files were now &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;some-article.html&lt;/code&gt; and not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;some-article/index.html&lt;/code&gt;, thus the 404s.&lt;/p&gt;

&lt;p&gt;This could just be left alone and it could probably fix itself, Google would quicly notice that and remove the old pages, index the new ones; etc. But for, again, SEO reasons, it’s best to properly “migrate” the pages with a 301 redirect, so the SEO of each page isn’t lost in the process.&lt;/p&gt;

&lt;p&gt;Unfortunately, neither GH pages or Jekyll (unless resorting to ugly techniques such as creating the directories, adding meta redirects there, etc.) can actually do these redirects themselves. That’s where Cloudflare comes in. Not the ideal solution, but for such a small “fix”, it’s practical and not an actual dependency.&lt;/p&gt;

&lt;p&gt;I couldn’t find a blog article that would explain how to solve that problem, that’s why I’m writing this!&lt;/p&gt;

&lt;p&gt;Enough talk, let’s get down to business. So you have your CF property, go to URL rules, create a new rule, and:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/assets/images/2023-01-09-remove-trailing-slash-cloudflare-jekyll/cf.png&quot;&gt;&lt;img src=&quot;/assets/images/2023-01-09-remove-trailing-slash-cloudflare-jekyll/cf.png&quot; alt=&quot;Trailing slash removal using cloudflare&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;URL: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://example.com/*/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Destination URL: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://example.com/$1&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;TADAA!&lt;/p&gt;

&lt;p&gt;This was also (briefly) posted as answers to Stack Overflow &lt;a href=&quot;https://stackoverflow.com/a/75055888/2367848&quot;&gt;here&lt;/a&gt; and &lt;a href=&quot;https://stackoverflow.com/a/75055689/2367848&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
</description>
        <pubDate>Mon, 09 Jan 2023 10:02:42 +0000</pubDate>
        <link>https://saveman71.com/2023/remove-trailing-slash-cloudflare-jekyll</link>
        <guid isPermaLink="true">https://saveman71.com/2023/remove-trailing-slash-cloudflare-jekyll</guid>
        
        
      </item>
    
      <item>
        <title>[FR] A Jeune Conducteur PDF</title>
        <description>&lt;p style=&quot;font-size: 0.8em;&quot;&gt;
  Note: This article in french differs from what I usually post there. But IMO it&apos;s also sharing knowledge, and is only relevant for french speakers.
&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;En France, les jeunes conducteurs sont obligés de placer un disque A à l’arrière gauche de leur véhicule.&lt;/p&gt;

&lt;p&gt;Ce disque doit être affiché pendant 3 ans pour les conducteurs ayant suivi un cursus classique, et pendant 2 ans pour ceux ayant effectué la conduite accompagnée.&lt;/p&gt;

&lt;p&gt;Si un jeune conducteur se fait contrôler sans ce disque, il doit payer une amende de 35 euros.&lt;/p&gt;

&lt;p&gt;Voici donc une version faite par mes soins qui ne contient pas de publicité, pas de frioriture, juste le A sur une feuille A4 (à imprimer à l’échelle bien sûr) en format PDF.&lt;/p&gt;

&lt;div style=&quot;text-align: center;&quot;&gt;
  &lt;object type=&quot;application/pdf&quot; data=&quot;/assets/images/2023-01-02-a-jeune-conducteur-pdf/a-jeune-conducteur.pdf&quot; width=&quot;60%&quot; height=&quot;auto&quot; style=&quot;aspect-ratio: 21/30; margin: auto;&quot;&gt;
  &lt;/object&gt;  
&lt;/div&gt;

&lt;div class=&quot;box-links&quot; style=&quot;margin-bottom: 1em;&quot;&gt;
  &lt;a href=&quot;/assets/images/2023-01-02-a-jeune-conducteur-pdf/a-jeune-conducteur.pdf&quot;&gt;&lt;img src=&quot;/assets/images/2023-01-02-a-jeune-conducteur-pdf/a-jeune-conducteur-icon.svg&quot; style=&quot;margin: 0; width: 1.4em; height: 1.4em; margin-right: .4em; margin-top: .1em;&quot; /&gt; en format PDF (à imprimer A4)&lt;/a&gt;
  &lt;a href=&quot;/assets/images/2023-01-02-a-jeune-conducteur-pdf/a-jeune-conducteur.svg&quot;&gt;&lt;img src=&quot;/assets/images/2023-01-02-a-jeune-conducteur-pdf/a-jeune-conducteur-icon.svg&quot; style=&quot;margin: 0; width: 1.4em; height: 1.4em; margin-right: .4em; margin-top: .1em;&quot; /&gt; en SVG (fichier source)&lt;/a&gt;
&lt;/div&gt;

&lt;p&gt;Plastifiez-le et appliquez le à l’aide de double face sur l’arrière gauche du vehicule. Notez que l’affichage sur le par-brise arrière est à priori interdit.&lt;/p&gt;

&lt;p&gt;Le disque A doit être une version homologuée: les versions dessinées à la main ne sont pas autorisées, l’homologation est la suivante (&lt;a href=&quot;https://www.legifrance.gouv.fr/loda/id/JORFTEXT000000713334&quot;&gt;Arrêté du 5 mai 1994&lt;/a&gt;) :&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/2023-01-02-a-jeune-conducteur-pdf/A.jpg&quot; alt=&quot;Homologation&quot; width=&quot;60%&quot; /&gt;&lt;/p&gt;
</description>
        <pubDate>Mon, 02 Jan 2023 11:20:42 +0000</pubDate>
        <link>https://saveman71.com/2023/a-jeune-conducteur-pdf</link>
        <guid isPermaLink="true">https://saveman71.com/2023/a-jeune-conducteur-pdf</guid>
        
        
        <category>other</category>
        
        <category>french</category>
        
      </item>
    
  </channel>
</rss>
