xargs and the unruly tags: A tale of two commands
Tags: git, shell
Published: 2019-05-26 16:18:11

---
title: xargs and the unruly tags: A tale of two commands
published: true
tags: git,shell
canonical_url: https://thomashartmann.dev/blog/xargs-and-the-unruly-tags
---
I thought I was really clever when I configured [my blog's](https://thomashartmann.dev/) CI/CD pipeline to tag commits that got deployed and push the tags back into the repo, but I’m rarely as clever as I like to think: I had forgotten to put the proper checks in place to avoid these tag pushes triggering subsequent runs of the pipeline, and things got a little … <em>out of hand</em>.


<p>I’d gone to bed just after pushing an update, and when I arose to check on it, I found that the deploy tagging stage had been running over and over and over and over and … you get the point. Thankfully, it had failed after about 130 rounds, so it could have been a lot worse, but I <em>was</em> left with a large amount of useless and unwanted tags in the remote repo.</p>
<p>So how do you fix something like this? Yup, <code>xargs</code> to the rescue!</p>
<h2 id="where-theres-a-will">Where there’s a will …</h2>
<p>At first, I didn’t really know how I’d go about it. I was hoping git would have some nice, built-in functionality for mass-deleting remote tags, but while I have found in retrospect that it does (see <a href="#postmortem">the postmortem</a>), I couldn’t find it at the time.</p>
<p>However, because all the tags were for a specific commit, I <em>did</em> know that I could list all the relevant tags separated by newlines, using <code>git tag --contains &lt;SHA&gt;</code>.</p>
<p>So, with some helpful advice from Stack Overflow and <a href="http://theknarf.com/">this guy</a>, I constructed this little command which sorted me out just fine:</p>
```sh
git tag --contains 9216e97ce7e66090f79eba4d1abe6548d72dd638 \
| xargs -I % git push origin :refs/tags/%
```
<p>Now, I’d come across <code>xargs</code> before, even done the ol’ copying and pasting from Stack Overflow trick, but it had always looked really complicated and no-one had ever told me why I’d need it or what it does; so I just carried on in blissful ignorance. Not this time, though. It was time to figure out what was going on.</p>
<h2 id="groking-xargs">Groking <code>xargs</code>
</h2>
<p>The way <code>xargs</code> was sold to me was: “execute a command for each item in a list”. It’s actually more powerful than that, but that’s a great place to start.</p>
<p>Let’s use the <code>man</code> page to find out what that <code>-I %</code> bit means :</p>
<dl>
<dt><code>-I</code></dt>
<dd>
<p><strong>replace-str</strong>: “Replace occurrences in the initial-arguments with names read from standard input”</p>
<p>The string to use to indicate where to place arguments in the command to run. In the command above, we chose to use <code>%</code>, but you’re not limited to this.</p>
<p>Similar to <code>printf</code> and format strings in general, this places your arguments at your desired place in the command. In our case it both limits us to using one argument (tag) at a time, and it lets us append it to <code>:refs/tags/</code> without being separated by a space.</p>
</dd>
</dl>
<p>That means that in the above snippet, <code>xargs</code> would, for each tag listed, run the command <code>git push origin :refs/tags/&lt;tag_name&gt;</code>, which pushes that tag with an empty reference, thereby deleting it.</p>
<p>If all you want is to put the argument at the end of the command, you can even do without the <code>-I</code>. Say you want to recursively delete all the <code>.swp</code> files in a directory:</p>
```sh
find -name "*.swp" | xargs rm
```
<p>Be aware, though, that without either using a <code>-I</code> or <code>-n</code> (to limit the number of arguments to use for each command), <code>xargs</code> will split the list you give it into sizeable chunks and apply as many arguments to the command as it can each time. That means that in this case, it’d likely end up looking something like this:</p>
```sh
rm a.swp b.swp c.swp ...
```
<p>which is usually fine and what you want, but keep this in mind for when it isn’t.</p>
<p>This is only scratching the surface of what <code>xargs</code> can do, but it’s enough to make it do some pretty heavy lifting. It might not be something to reach for very often, but for when you do need it, it’s a great tool to have in your belt.</p>
<h2 id="postmortem">Postmortem</h2>
<p><span id="postmortem"></span> Now, you might have noticed that I did a <code>git push</code> for each tag that I was deleting, and you might be thinking that for over a hundred tags, it must have taken quite some time. You would be right. Luckily, I was working on something else, so I could happily let it run in the background. But we can do better!</p>
<p><code>xargs</code> has an option <code>-P</code> or <code>--max-procs</code>, which you can use to decide how many processes to run in parallel. The default is 1, but if you set it to 0, it will run as many as it can. This could have saved us quite some time, assuming git would let us run multiple <code>push</code> operations from the same repo at the same time. But there is an even better way:</p>
<p>As outlined in <a href="https://stackoverflow.com/a/12791414">this Stack Overflow response</a>, you can use a whitespace-separated list of tag names (<code>&lt;tags&gt;</code>) with <code>git push</code>; so we could have run <code>git push --delete origin &lt;tags&gt;</code> to achieve the same outcome as deleting them one by one.</p>
<p>If we rewrite the command from earlier, we can both simplify it <em>and</em> do it all in a single push:</p>
```sh
git tag --contains 9216e97ce7e66090f79eba4d1abe6548d72dd638 \
| xargs git push --delete origin
```
<p>… yeah, that would have been a lot more efficient 😅</p>