<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><generator uri="https://gohugo.io/" version="0.147.5">Hugo</generator><title>André Arko</title><link href="http://andre.arko.net/atom.xml" rel="self"/><link href="http://andre.arko.net/"/><updated>2026-05-19T05:31:46+00:00</updated><id>http://andre.arko.net/</id><author><name>André Arko</name><email>andre@arko.net</email></author><entry><title type="html">Software developers have become their own joke</title><link href="https://andre.arko.net/2026/04/13/software-developers-have-become-their-own-joke/"/><updated>2026-04-13T01:17:52-07:00</updated><id>http://andre.arko.net/2026/04/13/software-developers-have-become-their-own-joke/</id><content type="html"><![CDATA[<p>Creating software is complicated. It&rsquo;s hard to figure out exactly what you need to build without a lot of trial and error. It almost always requires both exploring possible options <em>and</em> refining something until it works really well. But those things aren’t the same! Your research prototype is <em>not</em> a good product that people will happily pay for.</p>
<p>Back in the olden days, when software literally came from BigCo R&amp;D departments, we managed to invent Unix, and the mouse, and GUIs, and Ethernet, and TCP/IP, and a ton of other stuff we all use constantly today. Those research divisions didn&rsquo;t ship viable consumer products, though. Doug Englebart demoed a mouse-driven GUI in 1968, but you couldn&rsquo;t buy a home computer with a mouse-driven GUI until 1979, and they didn&rsquo;t become commercially popular until the Macintosh in 1984.</p>
<p>Even years or decades of research wasn&rsquo;t enough, and years (or decades!) of development work also needed to be done before the results was ready for people to use. Early literature about creating software, written by Fred Brooks and his peers, seems to contain the internalized view that both R and D are required. That&rsquo;s not surprising, since R&amp;D departments created most software back then, but we seem to have lost track of that connection.</p>
<p>Even though our jobs are descended from those R&amp;D labs of yore, we somehow lost the industry job of “software researcher”, and only &ldquo;software developer&rdquo; remains. Instead, research happens in academia, where an argument and some pseudocode is all you need to publish a paper. In that world, development is effectively non-existent.</p>
<p>(I admit the division isn&rsquo;t perfectly clear-cut. Sometimes academics will start companies around their research that create a product, or more likely get acquired to add a feature to a product. And sometimes Linus Torvalds will just build a new operating system, without doing any academic research on it, and it will get so popular everyone uses it. The point is that industry and academia have each publicly claimed one half of R&amp;D while disowning the other.)</p>
<p>The broader separation of research and development into academia and industry is really unfortunate, because good software needs both research <em>and</em> development as inputs. If you don&rsquo;t do any research, you can’t identify which parts will be hard (or impossible) until after it’s too late. You also won’t have a good idea of what parts are important until after you’ve put in most or all of the work to create the parts that don’t matter. If you don&rsquo;t do development, you won&rsquo;t ever have something robust enough that other people can use it successfully.</p>
<p>Meanwhile, on the other side, it feels like developers work hard to convince themselves there are no research aspects involved in their jobs. We call anything research-ish by another name, like “design”, “user experience”, “prototyping”, “de-risking”, “a spike”, and a lot of other funny euphemisms that avoid referring to the work as research. It seems like we&rsquo;re trying to convince ourselves that we don’t do Research any more, because we are just Developers.</p>
<p>This cultural lack of clarity around research in software development spaces really hit hard for me this week, as I read yet another treatise on working with LLM-driven agents for development. The two most popular takes that I have seen are “these tools are a fundamental shift in the nature of software development” and “these tools change nothing about building software at all”. Then the two sides start screaming at each other about how the other side is delusional and time will prove them completely wrong, and I lose interest.</p>
<p>If we instead start from the premise that all software work requires research (where the problem space must be explored) and development (where solutions must be implemented and refined), there’s something hiding in the sometimes messy overlap between those two ideas that I&rsquo;m not seeing come up in any discussions.</p>
<p><strong>No one can take the output of software research and treat it like it&rsquo;s the output of software development.</strong> Not Bell Labs, not Xerox PARC, not Microsoft middle managers, and not &ldquo;solo founders managing a team of AI agents&rdquo; today.</p>
<p>Unfortunately, seeing a prototype and becoming convinced it&rsquo;s complete is not a new problem. It&rsquo;s been the bane of software development possibly since the very beginning, when (apocryphally) a manager would review a mockup and conclude the project was now complete and could be shipped to customers immediately. Today, instead of telling that story as a joke, software developers have have somehow turned themselves into the boss from the joke, shouting that it&rsquo;s time to ship the research prototype because it &ldquo;looks finished&rdquo;. How did we do this to ourselves?</p>
<p>It seems like, back when we always had to do all the work ourselves, it was harder for software developers to be confused this way. If a developer knows they skipped every validation and edge case, it&rsquo;s much easier to realize it&rsquo;s not finished. If an LLM agent says &ldquo;here&rsquo;s a comprehensive implementation&rdquo;, without mentioning all the validations and edge cases it skipped, many (and possibly most) developers will not notice the parts that are missing.</p>
<p>This phenomenon is bad for a lot of reasons, including one reason you have probably already thought of: we’re going to get a lot more software claiming to be &ldquo;comprehensive&rdquo; and &ldquo;fully implemented&rdquo; when it’s really a partially finished prototype that’s full of holes.</p>
<p>In a world full of research prototypes being pitched as completed development work, life is about to get worse for everyone who uses software. The docs are even more wildly wrong than they were before, customer support is telling you that your problem is solved by a feature that doesn&rsquo;t exist, and company leadership is so excited they are planning to fire as many humans as possible so they can have more of it.</p>
<p>I don&rsquo;t want worse software! The software we already have is mostly terrible. Not only much worse software, but also much more of it, is pretty much my worst case scenario. What I actually want is better software, even if that means less of it.</p>
<p>Unfortunately, instead of making better software, software developers have decided to become the butt of their own joke, shipping software that doesn&rsquo;t work, with a footnote that says they know it doesn&rsquo;t work but they are still shipping it. I don&rsquo;t see any way to stop it, but I hate it anyway.</p>
]]></content></entry><entry><title type="html">Towards an Amicable Resolution with Ruby Central</title><link href="https://andre.arko.net/2026/04/02/towards-an-amicable-resolution-with-ruby-central/"/><updated>2026-04-02T11:06:38-07:00</updated><id>http://andre.arko.net/2026/04/02/towards-an-amicable-resolution-with-ruby-central/</id><content type="html"><![CDATA[<p>Last week, three members of Ruby Central’s board published <a href="https://rubycentral.org/news/a-message-from-the-ruby-central-board/">a new statement about RubyGems and Bundler</a>, and this week they published <a href="https://rubycentral.org/news/rubygems-fracture-incident-report/">an incident report on the events last year</a>. The first statement reports that Ruby Central has now completed a third audit of RubyGems.org’s infrastructure: first <a href="https://github.com/ossf/alpha-omega/blob/main/alpha/engagements/2025/Ruby%20Central/update-2025-10.md#unauthorized-access-and-credentials-rotation">by the sole remaining RubyGems.org maintainer</a>, the second <a href="https://github.com/ossf/alpha-omega/blob/main/alpha/engagements/2026/Ruby%20Central/update-2026-01.md#aws-security-audit">by Cloud Security Partners</a>, and the third by Hogan Lovells.</p>
<p>In all three cases, Ruby Central found <a href="https://rubycentral.org/news/rubygems-org-aws-root-access-event-september-2025/">no evidence of compromised end user data, accounts, gems, or infrastructure availability</a>. I hope this can conclusively put to rest the idea that I have any remaining access to the RubyGems.org production systems, or that I caused any harm to the RubyGems.org service at any time.</p>
<p>I also appreciate that Ruby Central is taking its share of responsibility, recognizing that its lack of communication with the former maintainers (including me) created confusion and frustration that contributed, in part, to how we ended up where we are today.</p>
<p>Ruby Central board members Freedom, Brandon, and Ran state that their intent is now to work towards an amicable resolution. I salute their new commitment, and would like to do my part to help the RubyGems community move past these unfortunate events, with a resolution that puts the dispute fully behind us, and allows all of us to move forward.</p>
<p>For my part, despite my claims against Ruby Central, and the threats they have directed against me, I am willing to completely settle all of my disputes with them, and pledge to take no legal action against Ruby Central regarding any of their actions prior to today.</p>
<p>In exchange, I am requesting two things.</p>
<p>First, I am asking Ruby Central to drop their legal threats, including releasing their claims against me and reimbursing my legal costs. Those costs arise from Ruby Central’s actions, including litigation threats, other escalations, and most recently contacting law enforcement. In addition to forcing me to retain counsel, these actions caused considerable stress and disruption. I am willing to provide invoices to ensure the reimbursement precisely matches only my actual costs.</p>
<p>Second, I am asking Ruby Central lay our disagreement to rest with a public statement acknowledging that I did no harm to the RubyGems.org service.</p>
<p>If Ruby Central fully drops their legal claims, and states I did not harm the RubyGems.org service, I would consider our disagreement amicably settled.</p>
]]></content></entry><entry><title type="html">How to Install a Gem</title><link href="https://andre.arko.net/2026/03/24/how-to-install-a-gem/"/><updated>2026-03-24T18:55:18-07:00</updated><id>http://andre.arko.net/2026/03/24/how-to-install-a-gem/</id><content type="html"><![CDATA[<p><small>This post was originally given as a talk at <a href="https://sfruby.com">SF Ruby Meetup</a>. The <a href="https://speakerdeck.com/indirect/how-to-install-a-gem/">slides</a> are also available.</small></p>
<iframe class="speakerdeck-iframe" style="border: 0px; background: padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" frameborder="0" src="https://speakerdeck.com/player/3bf2a60d63cb4a40a23c0e0dccb6a601" title="How to install a gem" allowfullscreen="true" data-ratio="1.7777777777777777"></iframe>
<h3 id="its-more-complicated-than-it-sounds">It&rsquo;s more complicated than it sounds</h3>
<p>Hello, and welcome to <em>How To Install A Gem</em>. My name is André Arko, and I go by @indirect on all the internet services. You might know me from being 1/3 of the team that shipped Bundler 1.0, or perhaps the 10+ years I spent trying to keep RubyGems.org up and running for everyone to use.</p>
<p>More recently, I&rsquo;ve been working on new projects: <a href="https://rv.dev"><code>rv</code></a>, a CLI to install Ruby versions and gems at unprecedented speeds, and <a href="https://gem.coop">gem.coop</a>, a community gem server designed from the ground up so Bundler and <code>rv</code> can install gems faster and more securely than ever before.</p>
<p>So, with that introduction out of the way, let’s get started: do you know how to install a gem? Okay, that’s great! You can come up and give this talk instead of me. I’ll just sit over here while you write the rest of this post.</p>
<p>Slightly more seriously, do you know how <code>gem install</code> converts the name that you give it into a URL to download a .gem file? It’s called the “compact index”, and we’ll see how it works very soon.</p>
<p>Next, who in the audience knows how to unpack a .gem file? Do you know what format .gem files use, and what&rsquo;s inside them? We’ll look at gem structure and gemspec files as well.</p>
<p>Then, do you know where to put the files from inside the gem? Where do all of these files and directories get put on disk so we can use them later? Does anyone know off the top of their head?</p>
<p>Once those files have been unpacked into the correct places, the last thing we need to know is how to require them. How do these unpacked files on disk get found by Ruby, so you can <code>require &quot;rails&quot;</code> and have that actually work?</p>
<p>This exercise was mostly to show that using gems every day actually skips over most of the way they work underneath. So let’s look at what a gem is, and examine how they work. By the end of this talk, you’ll know what’s inside a gem, how RubyGems figures out what to download, and where and how that download gets installed so you can use it.</p>
<p>And if you already everything we just talked about, please feel free to go straight to <a href="https://rv.dev">rv.dev</a> and start sending us pull requests!</p>
<h3 id="how-does-a-name-become-a-gem-url">How does a name become a .gem URL?</h3>
<p>First, we&rsquo;re going to look at how the name of a gem becomes a URL for a .gem file. Let&rsquo;s use <code>rails</code> as our example. Historically, there have been at least five or six different ways to look up information about a gem based on its name, but today there is one canonical way: the compact index. It&rsquo;s so simple that you can do it yourself using curl. Just run <code>curl https://gem.coop/info/rails</code>, and you&rsquo;ll be able to read the exact output that every tool uses to look up the versions of a gem that exist. Each line in the file describes one version of the gem, so let&rsquo;s look at one line.</p>
<pre><code>❯ curl -s https://gem.coop/info/rails | tail -n 1
8.1.3 actioncable:= 8.1.3,actionmailbox:= 8.1.3,actionmailer:= 8.1.3,actionpack:= 8.1.3,actiontext:= 8.1.3,actionview:= 8.1.3,activejob:= 8.1.3,activemodel:= 8.1.3,activerecord:= 8.1.3,activestorage:= 8.1.3,activesupport:= 8.1.3,bundler:&gt;= 1.15.0,railties:= 8.1.3|checksum:6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3,ruby:&gt;= 3.2.0,rubygems:&gt;= 1.8.11
</code></pre>
<p>We can break down that line with <code>split(&quot; &quot;)</code>, and tackle each part one at a time.</p>
<p>First, <code>8.1.3</code>. That&rsquo;s the version of <code>rails</code> that this line is about. So we now know for sure that <code>rails 8.1.3</code> exists.</p>
<p>Next, a list of dependencies. The <code>rails</code> gem (version <code>8.1.3</code>) declares dependencies on a bunch of other gems: <code>actioncable</code>, <code>actionmailbox</code>, <code>actionmailer</code>, <code>actionpack</code>, <code>actiontext</code>, <code>actionview</code>, <code>activejob</code>, <code>activemodel</code>, <code>activerecord</code>, <code>activestorage</code>, <code>activesupport</code>, <code>bundler</code>, and <code>railties</code>. Each dependency has a version requirement attached, and for almost every gem it is exactly version <code>8.1.3</code>, and only version <code>8.1.3</code>. For <code>bundler</code>, Rails is a little bit more flexible, and allows any version <code>1.15.0</code> and up.</p>
<p>The final section contains a checksum, a ruby requirement, and a rubygems requirement. The checksum is a sha256 hash of the .gem file that contains the gem, so after we download the gem we can check to make sure we have the right file by comparing that checksum.</p>
<p>For this version of Rails, the required Ruby version is <code>3.2.0</code> or greater, and the required RubyGems version is <code>1.8.11</code> or greater. It&rsquo;s up to the client to do something with that information, but hopefully you&rsquo;ll see an error if you are using Ruby or RubyGems that&rsquo;s too old.</p>
<p>Great! So now we know the important information: Rails version <code>8.1.3</code> is real, and strong, and is our friend. We can download it, and check the checksum against the checksum we were given in the info file line. Let&rsquo;s do that now:</p>
<pre><code>❯ curl -sO https://gem.coop/gems/rails-8.1.3.gem
❯ sha256sum rails-8.1.3.gem
6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3  rails-8.1.3.gem
</code></pre>
<p>Notice that the checksum produced by <code>sha256sum</code> exactly matches the checksum we previously saw in our line from the info file: <code>6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3</code>. That lets us know that we got the right file, and there were no network or disk errors.</p>
<h3 id="what-exactly-is-in-a-gem">What exactly is in a gem?</h3>
<p>Now that we have the gem, we can investigate: what exactly is inside a gem? At this point, we&rsquo;re going to pivot from the <code>rails</code> gem to the <code>railties</code> gem. There&rsquo;s a good reason for that, and the reason is&hellip; the <code>rails</code> gem doesn&rsquo;t actually have any files in it. So it&rsquo;s a bad example. In order to show off what a gem looks like when it has files in it, we&rsquo;ll use <code>railties-8.1.3.gem</code> instead.</p>
<p>So, we have our .gem file downloaded with curl. What do we do now? The first piece of secret knowledge that we need: gems are tarballs. That means we can open them up with regular old <code>tar</code>. Let&rsquo;s try it.</p>
<pre><code>❯ tar xfvz railties-8.1.3.gem
x metadata.gz
x data.tar.gz
x checksums.yaml.gz
</code></pre>
<p>So what&rsquo;s inside the .gem tarball is&hellip; another tarball. And also two gzipped files. Let&rsquo;s look at the files first.</p>
<pre><code>❯ gzcat checksums.yaml.gz
---
SHA256:
  metadata.gz: 8326ab6cc8e325055394ebd19c41d895e9ebd48e4752ec90d5c4675935516e6e
  data.tar.gz: 4152d8f55ae639d899f1cb6c54e1e93bb158bb76026b253482c5ae0343ac5aec
SHA512:
  metadata.gz: f6aa3390b6b1699255f1dbc6c6f24c6d9c18d3bfa48f10b6d720595384b4d1bb26f92232adb7011d3b1e7e977ca775cd253a12a135fe83eaa21e10dd0f14f779
  data.tar.gz: a001cebc5b97f627336a3e8d394c4ecac4a5d2b9e62c82de3b484470a58deac7c8ff0a8e8b497843386b1639c9cbfdfabee2cc7b2d483469e00a0b01da6bd41d
</code></pre>
<p>As you might expect from its name, the <code>checksums.yaml.gz</code> file is a gzipped YAML file, containing checksums for the other two files. It&rsquo;s maybe a bit silly to have multiple layers of checksumming here, but it does confirm that the outer layer of tarball and zip was removed without any errors.</p>
<p>Okay, so what&rsquo;s inside <code>metadata.gz</code>? The answer is&hellip; Ruby, sort of. It&rsquo;s a YAML-serialized instance of the <code>Gem::Specification</code> class. We can see exactly what was put into this object at the time the gem was built.</p>
<p>After snipping out the YAML that lists the dependencies (which we already looked at, because they are included in the info file), what&rsquo;s left is some relatively simple information about the gem. Author, author&rsquo;s email, description, homepage, license, various URLs.</p>
<pre><code>❯ gzcat metadata.gz
--- !ruby/object:Gem::Specification
name: railties
version: !ruby/object:Gem::Version
  version: 8.1.3
platform: ruby
bindir: exe
executables:
- rails
require_paths:
- lib
authors:
- David Heinemeier Hansson
summary: Tools for creating, working with, and running Rails applications.
description: 'Rails internals: application bootup, plugins, generators, and rake tasks.'
email: david@loudthinking.com
homepage: https://rubyonrails.org
licenses:
- MIT
metadata:
  bug_tracker_uri: https://github.com/rails/rails/issues
  changelog_uri: https://github.com/rails/rails/blob/v8.1.3/railties/CHANGELOG.md
  documentation_uri: https://api.rubyonrails.org/v8.1.3/
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
  source_code_uri: https://github.com/rails/rails/tree/v8.1.3/railties
  rubygems_mfa_required: 'true'
rubygems_version: 4.0.6
specification_version: 4
files:
- CHANGELOG.md
- MIT-LICENSE
- RDOC_MAIN.md
- README.rdoc
- exe/rails
- lib/minitest/rails_plugin.rb
- lib/rails.rb
[...]
</code></pre>
<p>For the purposes of installing and using the gem, we care about exactly six pieces of information: <code>name</code>, <code>version</code>, <code>platform</code>, <code>bindir</code>, <code>executables</code>, and <code>require_paths</code>.</p>
<p>We&rsquo;re going to combine those items with the files in the remaining <code>data.tar.gz</code> file to get our unpacked and installed gem.</p>
<h3 id="what-gets-unpacked-and-where">What gets unpacked, and where?</h3>
<p>Now that we know what&rsquo;s in the gem specification, let&rsquo;s look at what&rsquo;s inside the data tarball. It matches up very closely with the long list of entries in the <code>files</code> array in the gemspec.</p>
<pre><code>❯ mkdir railties-8.1.3
❯ tar xfvz data.tar.gz -C railties-8.1.3
x CHANGELOG.md
x MIT-LICENSE
x RDOC_MAIN.md
x README.rdoc
x exe/rails
x lib/minitest/rails_plugin.rb
x lib/rails.rb
[...]
</code></pre>
<p>So now we have a bunch of files. Where are we going to put these files? Enter: the magic of RubyGems. The scheme that RubyGems has come up with is largely shaped by the constraints of how Ruby finds files to require, which we&rsquo;re going to look at soon. For now, it is enough for us to know that RubyGems keeps track of a list of directories, a lot like the way <code>$PATH</code> works for your shell to find commands to run. To find the current directory, you can run <code>ruby -e 'puts Gem.dir</code>. Here&rsquo;s what that looks like:</p>
<pre><code>❯ ruby -e 'puts Gem.dir'
/Users/andre/.gem/ruby/4.0.0
❯ ls ~/.gem/ruby/4.0.0 | xargs -L1 echo
bin
build_info
cache
doc
extensions
gems
plugins
specifications
</code></pre>
<p>From this list, we can see that RubyGems organizes its own files into a few directories. To install a gem, we&rsquo;re going to need to put the files we have into each of those directories, with specific paths and filenames.</p>
<p>Just to recap, the files we need to place somewhere are:</p>
<ul>
<li>railties-8.1.3.gem (the .gem file itself)</li>
<li>metadata.gz (the YAML Gem::Specification object from inside the gem)</li>
<li>the unpacked data.tar.gz files (the contents of the gem)</li>
</ul>
<p>So let&rsquo;s move the files into the directories we see RubyGems offers.</p>
<p>First, cache the .gem file so RubyGems doesn&rsquo;t need to download it again later:</p>
<pre><code>mv railties-8.1.3.gem ~/.gem/ruby/4.0.0/cache/
</code></pre>
<p>Then, add the gem specification so that RubyGems will be able to find it. There&rsquo;s a small twist here, which is that the <code>specifications</code> directory doesn&rsquo;t contain YAML files, it contains Ruby files. So we also need to convert the YAML file back into a Ruby object, and then write out the Ruby code to create that object into a file that RubyGems can load later.</p>
<pre><code>❯ gunzip metadata.gz
❯ ruby -ryaml -e 'puts YAML.unsafe_load_file(&quot;metadata&quot;).to_ruby' &gt; ~/.gem/ruby/4.0.0/specifications/railties-8.1.3.specification
</code></pre>
<p>Next, we need to put the files that make up the contents of the gem into the <code>gems/</code> directory.</p>
<pre><code>❯ mv railties-8.1.3 ~/.gem/ruby/4.0.0/gems/
❯ ls ~/.gem/ruby/4.0.0/gems/railties-8.1.3
CHANGELOG.md
exe
lib
MIT-LICENSE
RDOC_MAIN.md
README.rdoc
</code></pre>
<p>One more thing we need to do: set up the executables provided by the gem.</p>
<p>You can check out the files that RubyGems generates by looking in <code>~/.gem/ruby/4.0.0/bin</code>, but for our purposes we just need to tell RubyGems what gem and executable it needs to run, so we can do that:</p>
<pre><code>❯ cat &lt;&lt;EOF &gt; ~/.gem/ruby/4.0.0/bin/rails
#!/usr/bin/env ruby
require &quot;rubygems&quot;
Gem.activate_and_load_bin_path(&quot;railties&quot;, &quot;rails&quot;)
EOF
❯ chmod +x ~/.gem/ruby/4.0.0/bin/rails
</code></pre>
<p>And with that, we&rsquo;ve installed the gem! You can run the <code>rails</code> file that we just created to prove it:</p>
<pre><code>❯ ~/.gem/ruby/4.0.0/bin/rails
Usage:
  rails COMMAND [options]

You must specify a command:

  new          Create a new Rails application. &quot;rails new my_app&quot; creates a
               new application called MyApp in &quot;./my_app&quot;
</code></pre>
<p>As we wrap up here, there are three aspects of gems that we haven&rsquo;t touched on at all: docs, extensions, and plugins. We don&rsquo;t have time to talk about them today in this meetup talk slot. Hopefully a future (longer) version of this talk will have space to include all of those things, because they are all super interesting, I promise.</p>
<p>In the meantime, I will have to direct you to the <a href="https://ruby.github.io/rdoc/">docs for RDoc</a> to learn more about docs, to <a href="https://github.com/spinel-coop/rv/blob/75786fe29c55452abfc725d43165a1b3035a552e/crates/rv/src/commands/clean_install.rs#L1504">the source code of <code>rv</code></a> or <a href="https://github.com/ruby/rubygems/blob/master/lib/rubygems/ext/ext_conf_builder.rb">RubyGems itself</a> if you want to learn more about gem extensions and plugins.</p>
<h3 id="how-does-require-find-a-gem">How does <code>require</code> find a gem?</h3>
<p>There&rsquo;s one last thing to figure out before we wrap up: how does <code>require</code> find a gem for us to be able to use it? To explain that, we&rsquo;ll have to drop down to some basic Ruby, and then look at the ways that RubyGems monkeypatches Ruby&rsquo;s basic <code>require</code> to make it possible to have gems with versions.</p>
<p>The first thing to know about <code>require</code> is that it works exactly like <code>$PATH</code> does in your shell. There&rsquo;s a global Ruby variable named <code>$LOAD_PATH</code>, and it&rsquo;s an array of paths on disk. When you try to require something, Ruby goes and looks inside each of those paths to see if the thing you asked for is there.</p>
<p>You can test this out for yourself in just a few seconds! Let&rsquo;s try it.</p>
<pre><code>❯ mkdir lib
❯ echo 'puts &quot;this is some ruby&quot;' &gt; lib/my-file.rb
❯ ruby -Ilib -e 'puts $LOAD_PATH.first; require &quot;my-file&quot;'
/Users/andre/lib
this is some ruby
</code></pre>
<p>The Ruby CLI flag <code>-I</code> lets you add directories to the <code>$LOAD_PATH</code> variable, and then the <code>require</code> function looks inside that directory to find a file with the name that you gave to require. No magic, just a list to check against for files on disk.</p>
<p>Now that you understand how the <code>$LOAD_PATH</code> variable makes <code>require</code> work, how does RubyGems work? You can&rsquo;t just put ten different versions of <code>rake</code> into the <code>$LOAD_PATH</code> and expect <code>require</code> to still work. RubyGems handles multiple versions of the same <code>rake.rb</code> file by monkeypatching <code>require</code>.</p>
<p>Let&rsquo;s look at what happens when we <code>require &quot;rails&quot;</code>, which is a file located inside the <code>railties</code> gem that we just installed. RubyGems starts by looking at all of the gem specifications, including the one we saved earlier. In each specification, it combines the name and version with the values in <code>require_paths</code> to come up with a path on disk.</p>
<p>So for our just-installed <code>railties</code> gem, that would mean a path of: <code>~/.gem/ruby/4.0.0/gems/railties-8.1.3/lib</code>.  RubyGems knows that directory contains a file named <code>rails.rb</code>, so it is a candidate to be &ldquo;activated&rdquo;, which is what RubyGems calls it when a gem is added to your <code>$LOAD_PATH</code>.</p>
<p>As long as internal bookkeeping shows that no other versions of <code>railties</code> have already been added to the <code>$LOAD_PATH</code>, we&rsquo;re good! RubyGems adds this specific directory to the <code>$LOAD_PATH</code>, and delegates to the original implementation of <code>require</code>. Require finds the file at <code>~/.gem/ruby/4.0.0/gems/railties-8.1.3/lib/rails.rb</code>, reads it, and evaluates it.</p>
<h3 id="gem-installed-congratulations">Gem installed, congratulations</h3>
<p>With that, we&rsquo;ve done it! We have found, downloaded, unpacked, and installed a gem so that Ruby is able to run a command and load ruby files, without ever touching the <code>gem install</code> command.</p>
<p>If you&rsquo;re interested in contributing to an open source project that works a lot with gems, we would love to work with you on <code>rv</code>, where we are working to create the fastest Ruby and gem manager in the world.</p>
<p>And of course, if your company could use faster, easier, or more secure gems for developers, for CI, and for production deployments, we can help. We&rsquo;d love to talk to you and you can find our contact information at <a href="https://spinel.coop">spinel.coop</a>.</p>
]]></content></entry><entry><title type="html">Four months of Ruby Central moving Ruby backward</title><link href="https://andre.arko.net/2026/03/03/four-months-of-ruby-central-moving-ruby-backward/"/><updated>2026-03-03T01:50:25-08:00</updated><id>http://andre.arko.net/2026/03/03/four-months-of-ruby-central-moving-ruby-backward/</id><content type="html"><![CDATA[<p>From the moment RubyGems was first created in 2004, <strong>Ruby Central provided <em>governance</em> without claiming <em>ownership</em></strong>, to support the Ruby community. Providing governance meant creating processes to provide stability and predictability. Avoiding ownership meant allowing the community to contribute, to the point where unpaid volunteers created and controlled the entirety of RubyGems.org for many years.</p>
<p><strong>Last year, Ruby Central flipped that successful formula on its head</strong>. They now claim <em>ownership</em> of both Bundler and RubyGems, but refuse to provide <em>governance</em>. Ruby Central now claims sole control over all code and decisions, despite paying for only a few percent of the work required to create and sustain the projects across 22 years. Instead of providing stable and predictable processes, Ruby Central <a href="https://joel.drapper.me/p/rubygems-takeover/">suddenly hijacked the Bundler and RubyGems codebases</a> away from the existing maintainers, shut out the community, and started issuing the threats to sue.</p>
<p>When confronted by the former maintainers after the hijacking, Marty Haught of Ruby Central stated (in <a href="https://gofile.me/6g3JG/oTY79Vk3b">a recorded video call</a>) on September 17 that &ldquo;yeah, we shouldn’t have changed that&rdquo;. On September 18, Marty went on to write:</p>
<blockquote>
<p>In the past, we&rsquo;ve made the mistake of conflating ownership of the code with ownership of the infra, and vice versa, and we&rsquo;d like to straighten this out so that we aren&rsquo;t put in a legal bind that requires us to take control of the entire codebase when, we all agree, that is not proper or correct given the existing model.</p></blockquote>
<p>In the words of Ruby Central itself, <strong>&ldquo;we all agree, [taking control of the entire codebase] is not proper or correct.&rdquo;</strong> Since the beginning of this conflict, Ruby Central has privately admitted it was wrong to hijack the GitHub organization and steal the repos, but has refused to acknowledge this in public. Unfortunately, despite privately admitting their actions were wrong, Ruby Central has publicly continued to dig their hole deeper. Instead of owning up to their mistake, they secretly negotiated a deal with Matz for ruby-core to <a href="https://rubycentral.org/news/ruby-central-statement-on-rubygems-bundler/">take over the stolen RubyGems and Bundler repository</a>, further violating the project governance policies.</p>
<p>If this situation were just about me personally, I could believe it sprang from from individual disagreements. Ruby Central <a href="https://rubycentral.org/news/rubygems-org-aws-root-access-event-september-2025/" title="Incident Response Timeline">claims they had good reasons</a> to unilaterally kick me out of the project, even though <a href="https://andre.arko.net/2025/10/09/the-rubygems-security-incident/">I don&rsquo;t think their claims hold water</a>. With that said, regardless of what you think about me personally, the other <strong>five long-term maintainers have never gotten any explanation</strong> of why they were suddenly kicked out or bypassed entirely, all in violation of existing project governance.</p>
<p>In <a href="https://www.youtube.com/watch?v=nKpo68g9dE">her only public interview</a> about the situation, Ruby Central Executive Director Shan Cureton defended stealing Bundler from its team of fifteen years by saying the removed team &ldquo;didn&rsquo;t need to have the story, and it wasn&rsquo;t their story to have&rdquo;. Ruby Central has made their position clear: <strong>if they steal your project, you are not entitled to know their reasons</strong>, and neither is anyone else. There is nothing &ldquo;community-oriented&rdquo; about stealing the most-used gem in Ruby and refusing to share your reasons with the community.</p>
<p>Despite Ruby Central’s unacceptable treatment of both projects and maintainers, the former RubyGems and Bundler team said <a href="https://andre.arko.net/2025/10/26/we-want-to-move-ruby-forward/">we want to move Ruby forward</a>. <strong>We offered Ruby Central a path</strong> to move past their illegitimate GitHub takeover, past their vicious personal attacks, and past their threats to sue us.</p>
<p>It has been four months since we made that offer, and <strong>Ruby Central has not accepted</strong>.</p>
<p>While declining to accept our offer, Ruby Central has nonetheless found the time to <a href="https://github.com/ruby/rubygems/pull/9187">propose new governance documents for RubyGems</a>. In those documents, they explicitly require existing maintainers approve adding or removing team members. That rule was already present in the previous governance, and is <strong>the exact rule that Ruby Central violated to execute their takeover</strong>. When asked why they violated the previous governance, and why the new governance would be any more trustworthy, Ruby Central refused to respond substantively, and then the question itself was hidden by <a href="https://github.com/ruby/rubygems/pull/9187#issuecomment-3703919204">marking it &ldquo;off topic&rdquo;</a>.</p>
<p>Instead of working to resolve the situation, Ruby Central has spent 4 months rejecting requests for an explanation, while repeatedly threatening to sue me personally. After Ruby Central suddenly took over the Bundler repo, <a href="https://andre.arko.net/2025/09/25/bundler-belongs-to-the-ruby-community/">I sent them a standard trademark notice</a>. They replied with a threat to sue me. When I later informed Ruby Central I had learned they violated state employment law, they simply replied with the same threat to sue me again. They are threatening to sue me for &ldquo;hacking&rdquo; them, despite their own analysis publicly concluding <a href="https://rubycentral.org/news/rubygems-org-aws-root-access-event-september-2025/">&ldquo;no evidence that user data or production operations were harmed&rdquo;</a>.</p>
<p>Without seeking common ground, or even looking for some sort of resolution we can just live with and move on from, <strong>Ruby Central has offered all of us — nothing</strong>. Ruby Central has made no offer in reply to outreach from the other five maintainers. To me, after four grueling months of private &ldquo;negotiation&rdquo;, their entire offer is nothing more than to refrain from suing. But only if I agree to everything that they want.</p>
<p>They say I must agree that I have no claim on the name Bundler, despite helping create it and leading the Bundler team for the last 15 years. They say I must agree I was paid legally and fairly, when California law clearly states I was not. <strong>They say I must agree that Ruby Central can take over open source projects</strong> they host, any time they feel like it, with no explanation, and no consequences.</p>
<p>I don&rsquo;t agree.</p>
<p>Letting this situation stay unaddressed sets a dangerous precedent for all open source projects written in Ruby. <strong>Ruby Central has resolved nothing. Don&rsquo;t let their delaying tactics convince you otherwise.</strong> The Ruby community cannot trust Ruby Central with control over our gems until there is accountability for destroying <a href="https://nesbitt.io/2025/12/22/package-registries-are-governance-as-a-service.html">the very governance they were supposed to be providing</a>.</p>
<p><strong>Until accountability arrives, take action</strong>. Tell Ruby Central they owe everyone an explanation for violating the project governance around six long-term maintainers, not just me. Don&rsquo;t sponsor, attend, or speak at RubyConf. Contribute to projects that aren&rsquo;t controlled by Ruby Central.</p>
<p>The exiled maintainers are working on new projects, with a focus on clear governance, long-term financial sustainability, and community input: Join the <a href="https://gem.coop/updates/5/">gem.coop</a> beta, and stop using RubyGems.org. Use <a href="https://github.com/duckinator/jwl">jwl</a> instead of RubyGems. Use <a href="https://rv.dev"><code>rv</code></a> or <a href="https://github.com/RubyElders/ruby-butler/">Ruby Butler</a> instead of Bundler.</p>
<p>A better world is possible! Ruby Central might want to keep Ruby in the past, but <strong>we can work together to build Ruby a future</strong>.</p>
]]></content></entry><entry><title type="html">Announcing &lt;code>rv clean-install&lt;/code></title><link href="https://andre.arko.net/2026/01/07/rv-clean-install/"/><updated>2026-01-07T12:31:16+08:00</updated><id>http://andre.arko.net/2026/01/07/rv-clean-install/</id><content type="html"><![CDATA[<p><small>Originally posted <a href="https://spinel.coop/blog/rv-clean-install/">on the Spinel blog</a>.</small></p>
<p>As part of our <a href="https://andre.arko.net/2025/08/25/rv-a-new-kind-of-ruby-management-tool/">quest to build a fast Ruby project tool</a>, we&rsquo;ve been hard at work on the next step of project management: installing gems. As we&rsquo;ve learned over the last 15 years of working on Bundler and RubyGems, package managers are really complicated! It&rsquo;s too much to try to copy all of rbenv, and ruby-build, and RubyGems, and Bundler, all at the same time.</p>
<p>Since we can&rsquo;t ship everything at once, we spent some time discussing the first project management feature we should add after Ruby versions. Inspired by <code>npm</code> and <code>orogene</code>, we decided to build <code>clean-install</code>. Today, we&rsquo;re releasing the <code>rv clean-install</code> command as part of <a href="https://github.com/spinel-coop/rv/releases/tag/v0.4.0"><code>rv</code> version 0.4.</a></p>
<p>So, what is a clean install? In this case, clean means &ldquo;from a clean slate&rdquo;. You can use <code>rv ci</code> to install the packages your project needs after a fresh checkout, or before running your tests in CI. It&rsquo;s useful by itself, and it&rsquo;s also concrete step towards managing a project and its dependencies.</p>
<p>Even better, it lays a lot of the groundwork for future gem management functionality, including downloading, caching, and unpacking gems, compiling native gem extensions, and providing libraries that can be loaded by Bundler at runtime.</p>
<p>While we don&rsquo;t (yet!) handle adding, removing, or updating gem versions, we&rsquo;re extremely proud of the progress that we&rsquo;ve made, and we&rsquo;re looking forward to improving <code>rv</code> based on your feedback.</p>
<p>Try running <code>brew install rv; rv clean-install</code> today, and see how it goes. Is it fast? Slow? Are there errors? What do you want to see next? <a href="https://github.com/spinel-coop/rv/issues/new">Let us know what you think</a>.</p>
]]></content></entry><entry><title type="html">Why are &lt;code>exec&lt;/code> and &lt;code>run&lt;/code> so confusing?</title><link href="https://andre.arko.net/2025/12/14/exec-vs-run/"/><updated>2025-12-14T21:20:10-08:00</updated><id>http://andre.arko.net/2025/12/14/exec-vs-run/</id><content type="html"><![CDATA[<p><small>Originally posted <a href="https://spinel.coop/news/exec-vs-run/">on the Spinel blog</a>.</small></p>
<p>While working on <a href="https://rv.dev">rv</a>, there&rsquo;s a specific question that has come up over and over again, in many different forms. In the simplest possible form, it boils down to:</p>
<blockquote>
<p>What is the difference between <code>rv exec</code> and <code>rv run</code>? Why have both?</p></blockquote>
<p>We haven&rsquo;t finished implementing either <code>rv exec</code> or <code>rv run</code> yet, but every time one or the other comes up in a conversation, everything instantly becomes more confusing.</p>
<p>This post will summarize the history of <code>exec</code> and <code>run</code> in Bundler, npm, Cargo, and uv. Once we have the history laid out, we can take a look at what we plan to do in rv, and you can <a href="https://github.com/spinel-coop/rv/discussions/235">give us your feedback</a>.</p>
<h2 id="bundler-exec">Bundler <code>exec</code></h2>
<p>Bundler manages project-specific packages, but not generally available &ldquo;global&rdquo; commands. Project-specific packages installed with Bundler can include their own commands.</p>
<p>While working on Bundler 1.0, we needed a standard way to do something new: run commands completely scoped inside a project, rather than scoped to the entire Ruby installation on the current machine. We tried both a wrapper command (<code>bundle exec COMMAND</code>) and generating dedicated scripts in the project&rsquo;s <code>bin/</code> directory. With binstubs, you could run <code>bin/rake</code> to get the project rake, and <code>rake</code> to get the global rake.</p>
<p>I personally preferred the binstub approach, but it was <code>bundle exec</code> that ultimately became the popular way to use commands inside your project. My theory is that it won because you can use it to run any command, including <code>ruby</code>, or <code>bash</code>, or anything else you want.</p>
<h2 id="rubygems-exec">RubyGems <code>exec</code></h2>
<p>Somewhat confusingly (inspired by the <code>npm exec</code> command explained below) there is a separate <code>gem exec</code> command that is not related to Bundler and instead installs and runs a command from a package. RubyGems only manages global packages and commands, so <code>gem exec</code> is more of a convenience to make it easier to globally install and run a package with just one command.</p>
<h2 id="npm-run-and-exec">npm <code>run</code> and <code>exec</code></h2>
<p>npm manages both project-specific and global packages, and can install any package so its commands are available either only within a project or globally across every project that uses the same version of Node.</p>
<p>The project-focused design of npm expects commands from project packages to be run by first adding the command to <code>package.json</code> in the <code>script</code> section, and the run via <code>npm run SCRIPT</code>. This is even more inconvenient than Bundler&rsquo;s binstubs, and so I think there was pent-up demand to be able to &ldquo;just run a command directly&rdquo;. That was eventually provided by <code>npm exec</code> and its alias <code>npx</code>.</p>
<p>The <code>npx COMMAND</code> setup makes it very easy to run any command, whether the package is in the local project or not, whether a script is set up or not, and even whether the package is installed at all or not. Simply running the command is enough to install the needed package and run its command.</p>
<p>It&rsquo;s especially helpful to have <code>npx</code> available when you need to create a new project by running an npm package, since it&rsquo;s a huge pain to create a project and install a package and set up a script just so you can run the package to overwrite your project. The most popular example of this I am aware of is <code>npx create-react-app</code>, but it&rsquo;s a popular idiom for many packages that contain commands.</p>
<h2 id="cargo-run-and-install">Cargo <code>run</code> and <code>install</code></h2>
<p>Cargo is simalarly a package manager, but unlike Ruby and Node, project-level packages do not include commands, and package commands are installed globally. Library packages are added to a project with <code>cargo add</code>, while command packages are installed globally with <code>cargo install</code>. Once a package is installed globally, it can simply be available on your <code>$PATH</code>, and Cargo no longer has to be involved in running it.</p>
<p>The <code>cargo run</code> command is extremely limited in scope, and only allows you to build and run a binary created by your own project &ndash; it does not work to run commands from packages, either project-local or global.</p>
<h2 id="uv-exec-and-run">uv <code>exec</code> and <code>run</code></h2>
<p>In uv, the <code>exec</code> command seems to be most strongly inspired by <code>npm exec</code>, including having its own short alias of <code>uvx</code>. The <code>uv exec</code> command is exclusively for running commands directly from packages, automatically installing them if necessary. To give an example, that means you can use <code>uv exec github-backup</code> to install and run the github-backup command from the Python package named github-backup, whether or not that packge is included in your current project.</p>
<p>Conversely, the <code>uv run</code> command is closer to <code>bundle exec</code>: it installs and configures Python, installs project packages if inside a project, and then runs a command from <code>$PATH</code> or runs a file. That means <code>uv run</code> can be used for both <code>uv run bash</code> to get a shell with only your project&rsquo;s Python and packages, and can also be used as <code>uv run myscript.py</code> to run a script file directly.</p>
<h2 id="summary">Summary</h2>
<p><code>bundle exec</code> runs:</p>
<ul>
<li>commands created by your package</li>
<li>commands from project packages (like <code>bundle exec rails</code>)</li>
<li>commands from $PATH (like <code>bundle exec bash</code>)</li>
<li>scripts from files (like <code>bundle exec ./myscript.rb</code>)</li>
</ul>
<p><code>npm run</code> runs:</p>
<ul>
<li>project-defined script commands (like <code>npm run my-project-script</code>), which can call:
<ul>
<li>commands from project packages (like <code>generate_pdf.js</code>)</li>
<li>commands from $PATH (like <code>bash</code>)</li>
<li>scripts from files (like <code>./myscript.js</code>)</li>
</ul>
</li>
</ul>
<p><code>npm exec</code> installs and runs:</p>
<ul>
<li>non-project commands from any package (like <code>npx create-react app</code>)</li>
</ul>
<p><code>cargo run</code> builds and runs:</p>
<ul>
<li>commands created by your package</li>
</ul>
<p><code>uv run</code> installs python and any project packages, then runs:</p>
<ul>
<li>commands from project packages (like <code>uv run datasette</code>)</li>
<li>commands from $PATH (like <code>uv run python</code>)</li>
<li>scripts from files (like <code>uv run ./myscript.py</code>)</li>
<li>project-defined script commands (like <code>uv run my-project-script</code>), which can call:
<ul>
<li>commands from project packages (like <code>github-backup</code>)</li>
<li>commands from $PATH (like <code>bash</code>)</li>
<li>scripts from files (like <code>./myscript.py</code>)</li>
</ul>
</li>
</ul>
<p><code>uv exec</code> installs python and the named package, then runs:</p>
<ul>
<li>non-project commands from any package (like <code>uv exec github-backup</code>)</li>
</ul>
<h2 id="the-question-for-rv">The question for <code>rv</code></h2>
<p>With all of that background now set up, what should <code>rv exec</code> do? What should <code>rv run</code> do?</p>
<p>To be the most like Bundler, we should use <code>rv exec</code> to run commands for a project. To be the most like <code>npm</code> and <code>uv</code>, we should use <code>rv exec</code> to install and run a single package with a command.</p>
<p>Today, we&rsquo;re leaning towards an option that includes all of the useful functionality from every command above, and aligns Ruby tooling with JS tooling and Python tooling:</p>
<p><code>rv run</code> installs ruby and any project packages, then runs:</p>
<ul>
<li>commands from project packages (like <code>rv run rspec</code>)</li>
<li>commands from $PATH (like <code>rv run bash</code>)</li>
<li>scripts from files (like <code>rv run ./myscript.rb</code>)</li>
<li>project-defined script commands (like <code>rv run my-project-script</code>), which can call:
<ul>
<li>commands from project packages (like <code>rspec</code>)</li>
<li>commands from $PATH (like <code>bash</code>)</li>
<li>scripts from files (like <code>./myscript.rb</code>)</li>
</ul>
</li>
</ul>
<p><code>rv exec</code> installs ruby and the named package, then runs:</p>
<ul>
<li>non-project commands from any package (like <code>rv exec rails</code>)</li>
</ul>
<p>If we try to combine those two commands into one command, we quickly run into the ambiguity that has been so frustrating to handle in Bundler for all of these years: if you <code>rv run rake</code>, do you want the global rake package? Or do you want the project-local rake package? How can we know which you want?</p>
<p>In my opinion, <code>uv</code> solves this relatively elegantly by having <code>uvx</code> always run a package globally, and <code>uvr</code> always run a package locally inside the current project, if one exists.</p>
<p>What do you think? <a href="https://github.com/spinel-coop/rv/discussions/235">Let us know in the GitHub discussion about this post.</a></p>
]]></content></entry><entry><title type="html">Operating Rails: what about after you deploy?</title><link href="https://andre.arko.net/2025/11/20/operating-rails/"/><updated>2025-11-20T10:52:56+09:00</updated><id>http://andre.arko.net/2025/11/20/operating-rails/</id><content type="html"><![CDATA[<p><small>This post was originally given as a talk at <a href="https://rockymtnruby.dev">Rocky Mountain Ruby</a>. The <a href="https://speakerdeck.com/indirect/operating-rails-what-about-after-you-deploy">slides</a> and <a href="https://www.youtube.com/watch?v=WP2fWUBPGfI">video</a> are also available.</small></p>
<script defer class="speakerdeck-embed" data-id="64254bb94df74086b8ece33274a25524" data-ratio="1.7777777777777777" src="//speakerdeck.com/assets/embed.js"></script>
<p>Welcome! This is meant to serve as an introduction to deployment and operations for newer developers, but it&rsquo;s also a checklist that I refer back to even after 20 years of deploying Rails apps to production. What needs to be covered when going to production the first time? What needs to be covered when going to production for the 1000th time?</p>
<p>Before we get into those details, let me introduce myself. I&rsquo;m André Arko, better known as @indirect on the internet. My biggest Ruby claim to fame is leading the Bundler and RubyGems OSS team for the last 15 years or so. Ruby Central recently kicked out the existing team, so the former RubyGems team has set up a new community-focused gem server at <a href="https://gem.coop">https://gem.coop</a>. I hope you&rsquo;ll check it out.</p>
<p>But I&rsquo;m here to talk about deploying and operating Rails applications, so let&rsquo;s get going. I built my first Rails application in 2004, and trying to get that application off my laptop so other people could use it is what started me down the path to madness and devops. It&rsquo;s been a long journey, and I have learned many things from over 20 years of watching Rails apps break in production.</p>
<p>Let&rsquo;s set the scene: an excited Ruby developer follows a Rails tutorial, creating a blog, a todo app, or some other cool idea. They walk through the code, the tests, the features. They finish the tutorial, and have a full Rails application, something cool that they want to show off to other people! That&rsquo;s when they realize… this is only on their laptop, and no one else can see or use it.</p>
<p>The most useful tutorials will mention hosting options at this point, maybe Heroku, Fly.io, Render, Railway, or DigitalOcean. If they&rsquo;re unhelpful tutorials, they might mention AWS, GCP, or Azure. But no matter what service they suggest, coding tutorials need to stay focused, and refer developers out to a hosting service when they want to deploy.</p>
<p>This is a problem, because the tutorials about how to deploy on those hosting sites all say things like &ldquo;of course this introduction does not include how to secure your secrets in production&rdquo;. In fact, hosting tutorials usually just wave their hands and skip over most aspects of deploying and operating a true production application. I&rsquo;m not sure how they expect a new dev to learn these skills, exactly, but that&rsquo;s what I&rsquo;m going to try to cover in this talk today.</p>
<p>So our hypothetical Rails developer has followed a hosting tutorial, and has a shiny new application and database deployed to Heroku, or Fly.io, or Render, or whatever. Now is when they actually start to run up against the real problems of running in production: are the secrets secure? is the database backed up? what happens if there&rsquo;s an exception? how do you debug something that only happens on the server?</p>
<p>Let&rsquo;s break down each of the new kinds of preparation needed by category, and walk through what you need to do, and why you need to do it. Ultimately, everything that we are going to talk about today is about <em>confidence</em>. Everything happening in deployment and operations, in devops, in SRE, is all being done to increase confidence in the application—your own, your coworkers, or your customers.</p>
<p>The areas we&rsquo;re going to look at today are Errors, Data, Speed, Security, and what usually gets called Lead Time. Lead Time is the amount of time it takes for a change to go from a commit to live in production. Investing in a shorter Lead Time can return absolutely outsized benefits in every other area, because it means you can try a change, learn from it, and make another change in reaction, faster and faster. We&rsquo;ll talk a bit more about it at the end.</p>
<h3 id="errors">Errors</h3>
<p>To start with, let&rsquo;s focus on Errors. Conceptually, an error is any time that your user isn&rsquo;t able to use your site. More specifically, an error could mean your Ruby code threw an exception, it could mean some configuration is preventing your app from working correctly, or it could mean that your servers have completely blown up and there is no app available for your users to talk to. With that in mind… how will you know? That&rsquo;s the first and most important rule of running applications in production. You need to know in advance how you&rsquo;re going to find out if something is broken.</p>
<p>It&rsquo;s not popular deployment advice, but the most important way to build confidence and in your application is to test it. Test it manually, test it automatically, have other people try it and report back. Each of those things will save you more time debugging than you can even imagine right now.</p>
<p>Next, one of the most popular ways to increase confidence before a deploy is a second environment where you can deploy your code to test it before it is live on the main site. The test area is usually called &ldquo;staging&rdquo;, and the main area is usually called &ldquo;production&rdquo;. Rails has built-in support for both of those, as well as &ldquo;development&rdquo;, which runs by default on your local development machine.</p>
<p>Use the staging environment as a place where you can put code that you aren&rsquo;t sure about, and try the new version there. After you have confidence (there it is again) in your new changes, you can &ldquo;promote&rdquo; them to production. Some platforms even let you bump staging to production without having to wait for a full new production deploy.</p>
<p>After you&rsquo;re releasing code with manual and automatic tests to staging and then production, the next most common mitigation strategies are exception reporting and uptime monitoring. Exception reporting does exactly what it sounds like, and sends a report to you when there is an exception in your application.</p>
<p>Personally, I tend to use honeybadger.io to set up exception tracking and uptime monitoring. It&rsquo;s free for a single user, which is great for my personal project, and I am apple to connect it to Slack so I get notifications if there are Ruby or Javascript errors, or if the site stops being reachable from the internet. Other popular options for tracking exceptions include Sentry, Airbrake, Bugsnag, and more that you can easily find with a search.</p>
<p>For anything that&rsquo;s less significant than an exception, or to help you investigate when an exception does occur, you&rsquo;ll also want to think about what usually gets called &ldquo;instrumentation&rdquo; or &ldquo;observability&rdquo;. The most basic level here is just seeing the logs from your application, and most hosting will provide a way for you to do that.</p>
<p>The more advanced levels of observability include setting up additional tracking in your application, both inside the Rails framework and inside your own code. This tracking can take the form of metrics, which are numbers counting when and how many times some particular thing has happened. Monitoring that focuses primarily on metrics includes using the Prometheus open source tool, or DataDog, AppSignal, NewRelic, and others like them.</p>
<p>Monitoring can also take the form of traces, where your application keeps track of how long each part of a response takes, letting you see each controller, parameter, database query, http request, and anything else that happened. If you&rsquo;ve ever heard of OpenTelemetry, this is what that is about. Honeycomb.io is the pioneer of this style of monitoring, but others like Sentry and DataDog have also added the ability to collect and review traces.</p>
<p>I don&rsquo;t always find Honeycomb&rsquo;s &ldquo;only events and traces&rdquo; style easier to use than simply reading logs, but it regularly helps me solve debugging mysteries that I would not have been able to figure out from solely reading logs, so I&rsquo;m very glad to have it as an option.</p>
<p>Lastly on the topic of errors in production, the open-ended question to ask yourself is &ldquo;how will people tell me about problems?&rdquo;. Do you have a contact form? A support email? A social media profile? Make sure there&rsquo;s some way to hear from your users, because there will always be something that doesn&rsquo;t set off your monitoring but still needs to be fixed… if you can hear about it.</p>
<h3 id="data">Data</h3>
<p>Next up is Data. Data includes not just the stuff that users have uploaded and put directly into your database, it also includes uploaded files, and anything else that you might want while recovering from a catastrophic server failure. Server failures range from the very mundane server part died to the very surprising truck rammed into the data center&rsquo;s power transformer, but they all have the same end state: your server is gone.</p>
<p>Now that your server is gone, you need to set up something again. This is the touchiest part of any production service. Do you have a copy of the latest code? A recent database backup? Uploaded files saved somewhere you can still get to them?</p>
<p>Start with the code. Make sure that you deploy to production by pushing to a code host and deploying from there. Most often that means GitHub and a deploy from GitHub Actions, but the important part is that any code that goes live needs to be easy to find even if your laptop vanishes.</p>
<p>Next, handle the database. Use a system that automatically takes daily snapshots of your database, or build something yourself that creates daily copies. For my side projects, I have set up a daily GitHub Action that connects to my database, pulls out a copy, and uploads it as an artifact. Actions artifacts have no size limit, and just time out after 90 days, which is perfect for daily backups.</p>
<p>Test your backups. More than one startup has completely failed and shut down when they discovered that their backups weren&rsquo;t actually running, or the results were corrupted, only after it was too late. Have more than one backup, preferably one per day, and test your backups system regularly. That&rsquo;s the second, and most important, rule of running applications in production.</p>
<p>Once you have your database(s) taken care of, it&rsquo;s time to look at your user uploaded or created files. Using a file service like S3, B2, R2, Google Cloud Storage, or Tigris can be a huge advantage here. Those services all provide a basic promise that your files will be copied to at least 3 places at all times. While it&rsquo;s possible to run your own copy of Minio and treat it like S3, if the filesystem backing your Minio server crashes, those files are gone. I don&rsquo;t recommend doing that unless you&rsquo;re sure those files don&rsquo;t really matter.</p>
<p>Lastly for data, you now need to think about how you are going to deal with all of the existing production data, now constantly growing, that you can&rsquo;t just erase. In local development, if your schema gets into a weird state, it&rsquo;s easy to just create a new database and import the schema again. Now that you have a live, production database, you can&rsquo;t do that.</p>
<p>Adding, removing, and renaming database columns are all now potentially actions that could break your application. To avoid full site downtime, you need to deploy code that can handle both the old schema and the new schema, then run the schema migration, then remove the handler code and deploy again. I recommend adding the <a href="https://github.com/ankane/strong_migrations"><code>strong_migrations</code></a> gem as soon as you have a live production site, to add some guardrails that will make it harder to accidentally break either the site or your users&rsquo; data.</p>
<h3 id="speed">Speed</h3>
<p>Now let&rsquo;s talk about speed. Speed from a Rails app? It&rsquo;s possible! In my experience, the biggest reasons that Rails apps get slow are external API calls, responses with no pagination, and accidental N+1 database queries. Rails may not be the most performant framework in the world, but it can be perfectly usable as long as you avoid those traps.</p>
<p>Why does the speed of your application matter? As mentioned at the start, it comes down to confidence. If users can see what is happening quickly, they trust your app to be working correctly. If your application matter takes so long that users lose interest or get distracted, that confidence is gone. Whatever did happen with that thing… hmm, I dunno.</p>
<p>Use the monitoring and observability tools we talked about before to keep an eye on how long your application is taking to do things. Don&rsquo;t use the average, because averages don&rsquo;t reflect any actual user experience. Instead, track a percentile that reflects how many users you can tolerate having a bad experience.</p>
<p>If you have 10 users per day, maybe the 90th percentile performance is what to watch because only 1 person will have a worse experience. If you have 10 million users per day, you might want to track the 99.99th percentile, because that&rsquo;s still 1000 people every day having a worse experience than that number.</p>
<p>When you find something that&rsquo;s slow, your main tools to fight against it are monitoring and profiling. Monitoring can tell you which users experienced the slowness, and what they were trying to do. It might even be able to tell you which SQL queries or HTTP calls made things so slow. Meanwhile, profiling can tell you what code is being run inside a given action, and help you optimize it. The <code>rack-prof</code> gem is the most well-known profiler, but there are multiple options available.</p>
<h3 id="security">Security</h3>
<p>Now that you know how and why to keep your app fast, let&rsquo;s talk about security. There are two resources your production application has that you now need to defend: user data, and servers on the internet. One class of attacks will try to break in to your application to steal user data, like emails, phone numbers, whatever you have stored. The other type of attack is about hijacking your server to do something for the attacker, like send spam or show ads or mine cryptocurrency.</p>
<p>The best defense for user data is to not store it at all. If you don&rsquo;t absolutely need it, don&rsquo;t collect it, and then no one can steal it. If you need to collect user information, make sure you are only collecting what you need. If your users are supplying personal information, especially sensitive financial or identity information, make sure the data is encrypted inside the database so a simple database leak won&rsquo;t reveal it. It&rsquo;s not hard to encrypt a single model column anymore, so be sure to do it!</p>
<p>The best defense against attackers who want to hijack your servers is to update your libraries. Use a tool like Dependabot or Renovate to apply security fixes, so your application isn&rsquo;t vulnerable to known hacks. Set up a monitoring alert on your CPU and disk usage. Set up a billing alert for if your usage goes above your usual amount, so you can investigate.</p>
<p>Ultimately, it&rsquo;s always worth it to have a security policy, even if it&rsquo;s very simple. Provide contact information so problems can be brought to your attention. You don&rsquo;t need to have a security team and a HackerOne program, but you do need to have a basic awareness of what could go wrong and how you can approach handling it. Knowing you have backups that can&rsquo;t be deleted by someone hacking into your server is a huge advantage here.</p>
<h3 id="lead-time">Lead Time</h3>
<p>Finally, lead time. Lead time is one of the for metrics tracked by the DORA program&rsquo;s research into software development teams. The book <em>Accelerate</em>, written by the founders of the DORA research program, goes into much more depth than I can fit here, so I&rsquo;m going to briefly summarize just this one aspect.</p>
<p>Lead time is the amount of time it takes to go from code being committed to that same code being live in production. If you can make a change and get it deployed into production in just a few minutes, you will have a completely different experience developing your application than if it takes hours or days. The DORA research found that small lead times is one of the biggest predictors of software projects being successful.</p>
<p>The overall, main finding of the DORA research project is that conventional wisdom of going slowly and carefully is exactly backwards—the teams and projects with the least errors and rollbacks are the teams that deploy the fastest and the most frequently. If you&rsquo;re already operating a larger or slower application in production, the biggest advice that I have is to reduce the size of each change, and make rolling out each change as fast as you possibly can.</p>
<p>The beginning of a project is by far the easiest time to set up the standard automations that you plan to use going forward. I strongly recommend setting up completely automatic testing and deployment. With GitHub Actions and modern hosting platforms, you can expect new commits to be tested and fully deployed to production within 5 minutes. That&rsquo;s a fantastic starting point to use as a base for future expansion, as your application and team grows.</p>
<h3 id="strategy">Strategy</h3>
<p>Now that I&rsquo;ve filled your heads with more ideas than you can probably remember, let&rsquo;s quickly review and then briefly discuss an overall strategy that you can use whenever you are trying to operate a production application. The big concerns after you deploy your app are: Errors, Data, Speed, Security, and Lead Time. Use those categories to review proposed changes, and look for issues you might have missed. Will the proposed change impact your ability to monitor errors? Back up user data? Respond to requests quickly? Keep your application secure? Ship changes quickly?</p>
<p>By keeping those categories of concern in mind, you are already better prepared to operate a production deployment than the vast majority of Rails developers. If the thought of working with those concerns is interesting or exciting to you, you probably have a bright future in DevOps or SRE. If the thought of working with those concerns fills you with unspeakable dread, you probably want to stick to bigger teams where someone else is available to handle operations and deployment.</p>
<h3 id="self-promotion">Self-promotion</h3>
<p>If your team could use a boost while handling everything we&rsquo;ve talked about today, you can get that support. I work for <a href="https://spinel.coop">Spinel Cooperative</a>, with developers from the core teams of Rails, Hotwire, RubyGems, and more. We would love to give your team all the advantages of 20 years spent building, deploying, and operating Ruby and Rails. Come say hi after the talk, or drop us an email at <a href="mailto:hello@spinel.coop">hello@spinel.coop</a>.</p>
<h3 id="conclusion">Conclusion</h3>
<p>In the end, will listening to all the advice in this talk mean your app never goes down? Unfortunately, probably not. Downtime is something that comes for us all, and usually includes some aspect that is completely unexpected or outside of our control. Instead, this advice will make you prepared. You&rsquo;ll be prepared to recover from downtime, and prepared to add more protections in the future as you learn the particular issues your application needs to defend against.</p>
<p>Ultimately, that&rsquo;s the best any of us can do, and I wish you all good luck with your future deployments.</p>
]]></content></entry><entry><title type="html">We want to move Ruby forward</title><link href="https://andre.arko.net/2025/10/26/we-want-to-move-ruby-forward/"/><updated>2025-10-26T12:21:18+09:00</updated><id>http://andre.arko.net/2025/10/26/we-want-to-move-ruby-forward/</id><content type="html"><![CDATA[<p>On September 9, without warning, Ruby Central <a href="https://pup-e.com/blog/goodbye-rubygems/">kicked out the maintainers</a> who have cared for Bundler and RubyGems for over a decade. Ruby Central made these changes against the <a href="https://github.com/ruby/rubygems/blob/master/doc/rubygems/POLICIES.md#committer-access">established project policies</a>, while ignoring all <a href="https://gist.github.com/simi/349d881d16d3d86947945615a47c60ca">objections from the maintainers&rsquo; team</a>. At the time, <a href="https://rubycentral.org/news/strengthening-the-stewardship-of-rubygems-and-bundler/">Ruby Central claimed</a> these changes were “temporary&quot;. However,</p>
<ul>
<li>None of the “temporary” changes made by Ruby Central have been undone, more than six weeks later.</li>
<li>Ruby Central still has not communicated with the removed maintainers about restoring any permissions.</li>
<li>Ruby Central still has not offered “operator agreements” or “contributor agreements” to any of the removed maintainers.</li>
<li>The <a href="https://andre.arko.net/2025/09/25/bundler-belongs-to-the-ruby-community/merger-agreement.pdf">Ruby Together merger agreement</a> plainly states that it is the maintainers who will decide what is best for their projects, not Ruby Central.</li>
<li>Last week, Matz stepped in to assume control of RubyGems and Bundler himself. <a href="https://www.ruby-lang.org/en/news/2025/10/17/rubygems-repository-transition/">His announcement</a> states that the Ruby core team will assume control and responsibility for the primary RubyGems and Bundler GitHub repository.</li>
<li>Ruby Central did not communicate with any removed maintainers before transferring control of the <a href="https://github.com/rubygems/rubygems/">rubygems/rubygems</a> GitHub repo to the Ruby core team.</li>
<li>On October 24th, Shan publicly confirmed <a href="https://youtu.be/nKpo68g9dEk?list=PLdqi4WM39BUiorBaKf4KfhejVDm0Uu0ew&amp;t=823">she does not believe the maintainers need to be told why they were removed</a>.</li>
</ul>
<p>While we know that Ruby Central had no right to act the way they did, it is nevertheless clear to us that the Ruby community will be better off if the codebase, maintenance, and legal rights to RubyGems and Bundler are all together in the same place.</p>
<p>To bring this about, <strong>we are prepared to transfer our interests in RubyGems and Bundler to Matz</strong>, end the dispute over the GitHub enterprise account, 2 GitHub organizations, and 70 repositories, and hand over all rights in the Bundler logo and Bundler name, including the trademark applications in the US, EU, and Japan.</p>
<p>Once we have entered into a legal agreement to settle any legal claims with Ruby Central and transfer all rights to Matz, the former maintainers will step back entirely from the RubyGems and Bundler projects, leaving them fully and completely to Matz, and by extension to the entire Ruby community.</p>
<p>Although Ruby Central’s actions were not legitimate, our commitment to the Ruby community remains strong. We’re choosing to focus our energy on projects to improve Ruby for everyone, including <a href="https://rv.dev/">rv</a>, <a href="https://github.com/RubyElders/ruby-butler">Ruby Butler</a>, <a href="https://github.com/duckinator/jim">jim</a>, and <a href="https://gem.coop/">gem.coop</a>.</p>
<p>Signed,<br>
The former maintainers: <a href="https://github.com/indirect">André</a>, <a href="https://github.com/deivid-rodriguez">David</a>, <a href="https://github.com/duckinator">Ellen</a>, <a href="https://github.com/simi">Josef</a>, <a href="https://github.com/martinemde">Martin</a>, and <a href="https://github.com/segiddins">Samuel</a></p>
]]></content></entry><entry><title type="html">&lt;code>jj&lt;/code> part 4: configuration</title><link href="https://andre.arko.net/2025/10/15/jj-part-4-configuration/"/><updated>2025-10-15T19:52:56-09:00</updated><id>http://andre.arko.net/2025/10/15/jj-part-4-configuration/</id><content type="html"><![CDATA[<p>Previously in this series: <a href="/2025/10/12/jj-part-3-workflows/"><code>jj</code> part 3: workflows</a></p>
<p>Just like git, jj offers tiers of configuration that layer on top of one another. Every setting can be set for a single repo, for the current user, or globally for the entire system. Just like git, jj offers the ability to create aliases, either as shortcuts or by building up existing commands and options into new completely new commands.</p>
<p>Completely unlike git, jj also allows configuring revset aliases and default templates, extending or replacing built-in functionality. Let&rsquo;s look at the ways it&rsquo;s possible to customize jj via configurations. We&rsquo;ll cover basic config, custom revsets, custom templates, and custom command aliases.</p>
<h3 id="config-basics">config basics</h3>
<p>Let&rsquo;s start with some configuration basics. You can globally configure jj to change your name and email based on a path prefix, so you don’t have to remember to set your work email separately in each work repo anymore.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[[<span style="color:#a6e22e">--scope</span>]]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">--when</span>.<span style="color:#a6e22e">repositories</span> = [<span style="color:#e6db74">&#34;~/work&#34;</span>]
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">--scope</span>.<span style="color:#a6e22e">user</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">email</span> = <span style="color:#e6db74">&#34;me@work.domain&#34;</span>
</span></span></code></pre></div><p>Or perhaps you want jj to wait for your editor if you are writing a commit message, but you don&rsquo;t want jj to wait for your editor to exist if you are editing your jj configuration file. You can ensure that using a scope.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">editor</span> = <span style="color:#e6db74">&#34;code -w&#34;</span>
</span></span><span style="display:flex;"><span>[[<span style="color:#a6e22e">--scope</span>]]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">--when</span>.<span style="color:#a6e22e">commands</span> = [<span style="color:#e6db74">&#34;config&#34;</span>]
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">--scope</span>.<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">editor</span> = <span style="color:#e6db74">&#34;code&#34;</span>
</span></span></code></pre></div><p>I also highly recommend trying out multiple options for formatting your diffs, so you can find the one that is most helpful to you. A very popular diff formatter is <code>difftastic</code>, which provides syntax aware diffs for many languages. I personally use <code>delta</code>, and the configuration to format diffs with delta looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[[<span style="color:#a6e22e">--scope</span>]]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">--when</span>.<span style="color:#a6e22e">commands</span> = [<span style="color:#e6db74">&#34;diff&#34;</span>, <span style="color:#e6db74">&#34;show&#34;</span>]
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">--scope</span>.<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pager</span> = <span style="color:#e6db74">&#34;delta&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">diff-formatter</span> = <span style="color:#e6db74">&#34;:git&#34;</span>
</span></span></code></pre></div><p>Another very impactful configuration is which tool jj uses to handle interactive diff editing, such as in the <code>jj split</code> or <code>jj squash -i</code> commands. While the default terminal UI is pretty good, make sure to also try out Meld, an open source GUI.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">diff-editor</span> = <span style="color:#e6db74">&#34;meld&#34;</span> <span style="color:#75715e"># or vimdiff, vscode, etc</span>
</span></span></code></pre></div><p>In addition to changing the diff editor, you can also change the merge editor, which is the program that is used to resolve conflicts. Meld can again be a good option, as well as any of several other merging tools.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">merge-editor</span> = <span style="color:#e6db74">&#34;meld&#34;</span> <span style="color:#75715e"># or vimdiff, vscode, mergiraf etc</span>
</span></span></code></pre></div><p>Tools like <a href="https://mergiraf.org/">mergiraf</a> provide a way to attempt syntax-aware automated conflict resolution before handing off any remaining conflicts to a human to resolve. That approach can dramatically reduce the amount of time you spend manually handling conflicts.</p>
<p>You might even want to try FileMerge, the macOS developer tools built-in merge tool. It supports both interactive diff editing and conflict resolution.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">merge-tools</span>.<span style="color:#a6e22e">filemerge</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">program</span> = <span style="color:#e6db74">&#34;open&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">edit-args</span> = [<span style="color:#e6db74">&#34;-a&#34;</span>, <span style="color:#e6db74">&#34;FileMerge&#34;</span>, <span style="color:#e6db74">&#34;-n&#34;</span>, <span style="color:#e6db74">&#34;-W&#34;</span>, <span style="color:#e6db74">&#34;--args&#34;</span>,
</span></span><span style="display:flex;"><span>             <span style="color:#e6db74">&#34;-left&#34;</span>, <span style="color:#e6db74">&#34;$left&#34;</span>, <span style="color:#e6db74">&#34;-right&#34;</span>, <span style="color:#e6db74">&#34;$right&#34;</span>,
</span></span><span style="display:flex;"><span>             <span style="color:#e6db74">&#34;-merge&#34;</span>, <span style="color:#e6db74">&#34;$output&#34;</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">merge-args</span> = [<span style="color:#e6db74">&#34;-a&#34;</span>, <span style="color:#e6db74">&#34;FileMerge&#34;</span>, <span style="color:#e6db74">&#34;-n&#34;</span>, <span style="color:#e6db74">&#34;-W&#34;</span>, <span style="color:#e6db74">&#34;--args&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;-left&#34;</span>, <span style="color:#e6db74">&#34;$left&#34;</span>, <span style="color:#e6db74">&#34;-right&#34;</span>, <span style="color:#e6db74">&#34;$right&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;-ancestor&#34;</span>, <span style="color:#e6db74">&#34;$base&#34;</span>, <span style="color:#e6db74">&#34;-merge&#34;</span>, <span style="color:#e6db74">&#34;$output&#34;</span>,]
</span></span></code></pre></div><p>Just two more configurations before we move on to templates. First, the default subcommand, which controls what gets run if you just type <code>jj</code> and hit return. The default is to run <code>jj log</code>, but my own personal obsessive twitch is to run <code>jj status</code> constantly, and so I have changed my default subcommand to <code>status</code>, like so:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">default-command</span> = [<span style="color:#e6db74">&#34;status&#34;</span>]
</span></span></code></pre></div><p>The last significant configuration is the default revset used by <code>jj log</code>. Depending on your work patterns, the multi-page history of commits in your current repo might not be helpful to you. In that case, you can change the default revset shown by the log command to one that’s more helpful. My own default revset shows only one change from my origin. If I want to see more than the newest change from my origin I use <code>jj ll</code> to get the longer log, using the original default revset. I&rsquo;ll show that off later.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">revsets</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">log</span> = <span style="color:#e6db74">&#34;(trunk()..@):: | (trunk()..@)-&#34;</span>
</span></span></code></pre></div><h3 id="templates">templates</h3>
<p>While the <a href="https://jj-vcs.github.io/jj/latest/templates/">jj template docs</a> are a great reference, they don’t do very much to show off what’s possible by using templates, so we’ll show some examples.</p>
<p>The first and most obvious template is the <code>jj log</code> template, which controls how each change is rendered in the log output. The default template is named <code>builtin_log_compact</code>, and jj comes with a few pre-built template options for the log view, like <code>builtin_log_detailed</code> and <code>builtin_log_oneline</code>. You can see them all by running <code>jj log -T</code>.</p>
<p>Use <code>jj log -T NAME</code> to try them out and see how they look. If you want to experiment with your own custom log formats, you can provide a template string instead of the name of an existing template. Here’s an example inline template that prints out just the short change ID, a newline, and then the change description:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>jj log -T <span style="color:#e6db74">&#39;change_id.short() ++ &#34;\n&#34; ++ description&#39;</span>
</span></span></code></pre></div><p>Try out the various <a href="https://jj-vcs.github.io/jj/latest/templates/">documented template properties</a> yourself! Once you’re happy with a template that you’ve tested, you can add it to your config with a name, and then use it by name.</p>
<p>Here’s a more complicated example, adapted from @marchyman in the jj Discord, with several of the elements that we’ve discussed so far. This example changes the default command, adding extra options. It also uses a named revset alias, and a named template alias.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f92672">[</span>ui<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>default-command <span style="color:#f92672">=</span> <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;log&#34;</span>, <span style="color:#e6db74">&#34;-T&#34;</span>, <span style="color:#e6db74">&#34;log_with_files&#34;</span>, <span style="color:#e6db74">&#34;--limit&#34;</span> <span style="color:#e6db74">&#34;7&#34;</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>revset-aliases<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;recent_work&#39;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;ancestors(visible_heads(), 3) &amp; mutable()&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>template-aliases<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>log_with_files <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">if(root,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  format_root_commit(self),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  label(if(current_working_copy, &#34;working_copy&#34;),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    concat(
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      format_short_commit_header(self) ++ &#34;\n&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      separate(&#34; &#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        if(empty, label(&#34;empty&#34;, &#34;(empty)&#34;)),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        if(description,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          description.first_line(),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          label(if(empty, &#34;empty&#34;), description_placeholder),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        ),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      ) ++ &#34;\n&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      if(self.contained_in(&#34;recent_work&#34;), diff.summary()),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    ),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  )
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;</span>
</span></span></code></pre></div><p>The named template recreates the regular template from <code>log</code>, and uses a revset filter to include the list of changed files in changes that are both mutable (that is, not yet pushed) and also within 3 commits of the end of a branch. By showing up to 7 changes, but the file list for up to 3 mutable changes, the log output becomes more useful, reminding you what files have been changed in the most recent commits that you might want to push.</p>
<p>One more template you might want to adjust is the default description, shown when running <code>jj commit</code> or <code>jj desc</code> for a change that does not yet have a description. If you don’t use a VCS GUI, it can be helpful to see the diff of what is being committed at the same time as you write the commit message. In git, that meant running <code>git commit --verbose</code>, but in jj that means adjusting the default description. The <a href="https://jj-vcs.github.io/jj/latest/config/#default-description">jj config docs</a> provide an example template that will replicate that effect, and show you the diff while you write the message.</p>
<h3 id="revset-aliases">revset aliases</h3>
<p>Building on the earlier section where we talked about <a href="#"><code>jj log</code></a>, creating your own revset aliases is a powerful way to construct views tailored to your personal needs.</p>
<p>A built-in revset alias we can use to illustrate this is <code>immutable()</code>. In the same way that git requires <code>--force</code> to push over an existing remote commit, jj requires <code>--ignore-immutable</code> to edit a commit matched by <code>immutable()</code>.</p>
<p>(Incidentally, I believe this arrangement is also an example of the way jj’s design is an improvement on git. Instead of deciding to overwrite published commits during a push, you are forced to decide much earlier, during the edit itself, if you are okay with changing a published commit. Anyway, back to revset aliases.)</p>
<p>The default <code>immutable_heads()</code> revset is <code>present(trunk()) | tags() | untracked_remote_bookmarks()</code>, which composes four other revsets together. Let’s look at each one. The <code>trunk()</code> revset is simply the primary branch, whether it is named <code>main</code> or <code>master</code>, wrapped in <code>present()</code> to remove it if none of those branches exist. The <code>tags()</code> revset is every change that has been given a tag. The <code>untracked_remote_bookmarks()</code> revset is exactly what it sounds like: any branch provided by the remote that you have not manually opted in to tracking locally (which is what you would do if you are working on the branch). All three revsets are combined into one overall list with the <code>|</code> operator. Those heads are then used to construct the full list of immutable commits, which is every ancestor of those heads.</p>
<p>Now that you know how that works, we can change it. For example, perhaps you want the same immutability rules that <code>git</code> provides, where commits are immutable once they have been pushed to any remote at all. In that case, you could add this to your config file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">revset-aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;immutable_heads()&#34;</span> = <span style="color:#e6db74">&#34;present(trunk()) | tags() | remote_bookmarks()&#34;</span>
</span></span></code></pre></div><p>With that configuration, jj will extrapolate that it cannot change any commits on the primary branch, all the commits leading up to a tag, and all commits leading up to a named branch in the remote. If you use this revset, jj will stop you from changing commits once you have pushed them to a branch, since you told it to make those immutable.</p>
<p>With this power at your disposal, you can change the default revset shown when you run <code>jj log</code>, or you can create your own named revsets for your own purposes. You can see <a href="https://github.com/indirect/dotfiles/blob/main/private_dot_config/private_jj/config.toml#L107">my revset aliases</a> in my dotfiles, and read more about the default aliases in the jj docs.</p>
<h3 id="command-aliases">command aliases</h3>
<p>Okay. Now we can talk about command aliases. First up, the venerable <code>tug</code>. In the simplest possible form, it takes the closest bookmark, and moves that bookmark to <code>@-</code>, the parent of the current commit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">tug</span> = [<span style="color:#e6db74">&#34;bookmark&#34;</span>, <span style="color:#e6db74">&#34;move&#34;</span>, <span style="color:#e6db74">&#34;--from&#34;</span>, <span style="color:#e6db74">&#34;heads(::@- &amp; bookmarks())&#34;</span>, <span style="color:#e6db74">&#34;--to&#34;</span>, <span style="color:#e6db74">&#34;@-&#34;</span>]
</span></span></code></pre></div><p>What if you want it to be smarter, though? It could find the closest bookmark, and then move it to the closest <em>pushable</em> commit, whether that commit was <code>@</code>, or <code>@-</code>, or <code>@---</code>. For that, you can create a revset for <code>closest_pushable</code>, and then tug from the closest bookmark to the closest pushable, like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">revset-aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;closest_pushable(to)&#39;</span> = <span style="color:#e6db74">&#39;heads(::to &amp; mutable() &amp; ~description(exact:&#34;&#34;) &amp; (~empty() | merges()))&#39;</span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">tug</span> = <span style="color:#e6db74">&#39;bookmark move --from &#34;heads(::@ &amp; bookmarks())&#34; --to &#34;closest_pushable(@)&#34;&#39;</span>
</span></span></code></pre></div><p>Now your bookmark jumps up to the change that you can actually push, by excluding immutable, empty, or descriptionless commits.</p>
<p>What if you wanted to allow tug to take arguments, for those times when two bookmarks are on the same change, or when you actually want to tug a different bookmark than the closest one? That’s also pretty easy, by adding a second variant of the tug command that takes an argument:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">tug</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;sh&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">if [ &#34;</span><span style="color:#a6e22e">x</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#34; = &#34;</span><span style="color:#a6e22e">x</span><span style="color:#e6db74">&#34; ]; then
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  jj bookmark move --from &#34;</span><span style="color:#a6e22e">closest_bookmark</span><span style="color:#960050;background-color:#1e0010">(@)</span><span style="color:#e6db74">&#34; --to &#34;</span><span style="color:#a6e22e">closest_pushable</span><span style="color:#960050;background-color:#1e0010">(@)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">else
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  jj bookmark move --to &#34;</span><span style="color:#a6e22e">closest_pushable</span><span style="color:#960050;background-color:#1e0010">(@)</span><span style="color:#e6db74">&#34; &#34;</span><span style="color:#960050;background-color:#1e0010">$@</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">fi
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>]
</span></span></code></pre></div><p>This version of tug works just like the previous one if no argument is given. But if you do pass an argument, it will move the bookmark with the name that you passed instead of the closest one.</p>
<p>How about if you’ve just pushed to GitHub, and you want to create a pull request from that pushed bookmark? The <code>gh pr create</code> command isn’t smart enough to figure that out automatically, but you can tell it which bookmark to use:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">pr</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">gh pr create --head $(jj log -r &#39;closest_bookmark(@)&#39; -T &#39;bookmarks&#39; --no-graph | cut -d &#39; &#39; -f 1)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>]
</span></span></code></pre></div><p>Just grab the list of bookmarks attached to the closest bookmark, take the first one, pass it to <code>gh pr create</code>, and you’re all set.</p>
<p>What if you just want single commands that let you work against a git remote, with defaults tuned for automatic tugging, pushing, and tracking? I’ve also got you covered.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">init</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj git init --colocate
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># only track origin branches, not upstream or others
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj bookmark track &#39;glob:*@origin&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>]
</span></span></code></pre></div><p>Use <code>jj init</code> to colocate jj into this git repo, and then track any branches from upstream, like you would get from a git clone.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">pull</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Find the closest bookmark
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">closest=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#39;closest_bookmark(@)&#39;</span> <span style="color:#960050;background-color:#1e0010">\</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">-n</span> <span style="color:#ae81ff">1</span> <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;bookmarks&#39;</span> <span style="color:#a6e22e">--no-graph</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#a6e22e">cut</span> <span style="color:#a6e22e">-d</span> <span style="color:#e6db74">&#39; &#39;</span> <span style="color:#a6e22e">-f</span> <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Remove the trailing * from the name if there is one
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">closest=&#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span><span style="color:#960050;background-color:#1e0010">%\\*</span>}<span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Now fetch from the git remote
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj git fetch
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># If the closest bookmark still exists, rebase on it (else trunk)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj log -n 1 -r &#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span>}<span style="color:#e6db74">&#34; 2&gt;&amp;1 &gt; /dev/null \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &amp;&amp; jj rebase -d &#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span>}<span style="color:#e6db74">&#34; || jj rebase -d &#39;trunk()&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Show the new state of things after the pull
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj log -r &#39;stack()&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>]
</span></span></code></pre></div><p>Then, you can <code>jj pull</code> to find the closest bookmark to <code>@</code>, do a git fetch, rebase your current local commits on top of whatever just got pulled, and then show your new stack. When you’re done, just <code>jj push</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">push</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Check to see if we can tug a bookmark to @
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">tuggable=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#39;closest_bookmark(@)..closest_pushable(@)&#39;</span> <span style="color:#960050;background-color:#1e0010">\</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;&#34;n&#34;&#39;</span> <span style="color:#a6e22e">--no-graph</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># If we can, tug that bookmark as close to @ as possible
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">[[ -n &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">tuggable</span><span style="color:#e6db74">&#34; ]] &amp;&amp; jj tug
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Now find the closest thing that we can push to `origin`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">pushable=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#39;remote_bookmarks(remote=origin)..@&#39;</span> <span style="color:#960050;background-color:#1e0010">\</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;bookmarks&#39;</span> <span style="color:#a6e22e">--no-graph</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># If we have something to push, run `jj git push`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">[[ -n &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">pushable</span><span style="color:#e6db74">&#34; ]] &amp;&amp; jj git push || echo &#34;</span><span style="color:#a6e22e">Nothing</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">push</span>.<span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Now that we have pushed, find the closest bookmark and remove *
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">closest=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#39;closest_bookmark(@)&#39;</span> <span style="color:#a6e22e">-n</span> <span style="color:#ae81ff">1</span> <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;bookmarks&#39;</span> <span style="color:#960050;background-color:#1e0010">\</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">--no-graph</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#a6e22e">cut</span> <span style="color:#a6e22e">-d</span> <span style="color:#e6db74">&#39; &#39;</span> <span style="color:#a6e22e">-f</span> <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">closest=&#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span><span style="color:#960050;background-color:#1e0010">%\\*</span>}<span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Check to see if that bookmark is already tracking origin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">tracked=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">bookmark</span> <span style="color:#a6e22e">list</span> <span style="color:#a6e22e">-r</span> <span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span>} <span style="color:#a6e22e">-t</span> <span style="color:#960050;background-color:#1e0010">\</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;if(remote == &#34;origin&#34;, name)&#39;</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># If that bookmark isn&#39;t tracking origin, start to track origin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">[[ &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">tracked</span><span style="color:#e6db74">&#34; == &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">closest</span><span style="color:#e6db74">&#34; ]] \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  || jj bookmark track &#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span>}<span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">origin</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>]
</span></span></code></pre></div><p>This push handles looking for a tuggable bookmark, tugging it, doing a git push, and making sure that you’re tracking the origin copy of whatever you just pushed, in case you created a new branch.</p>
<h3 id="further-reading">further reading</h3>
<p>For another perspective on jj configuration, partly overlapping with this post, check out my JJ Con talk, <a href="/2025/09/28/stupid-jj-tricks/">stupid jj tricks</a>.</p>
<p>You can also try reading some jj config files directly, like <a href="https://github.com/indirect/dotfiles/blob/main/private_dot_config/private_jj/config.toml">my jj config</a>, or <a href="https://gist.github.com/thoughtpolice/8f2fd36ae17cd11b8e7bd93a70e31ad6">thoughtpolice&rsquo;s jj config</a>, or <a href="https://gist.github.com/pksunkara/622bc04242d402c4e43c7328234fd01c">pksunkara&rsquo;s jj config</a>.</p>
<h3 id="next-time">next time</h3>
<p>Hopefully this tour through jj configuration options has revealed some ways that jj can be used to do more than was possible with only git. Next time, we&rsquo;ll focus on the ways that jj goes beyond git, offering things that were impractical or even impossible before.</p>
]]></content></entry><entry><title type="html">&lt;code>jj&lt;/code> part 3: workflows</title><link href="https://andre.arko.net/2025/10/12/jj-part-3-workflows/"/><updated>2025-10-12T10:52:56+09:00</updated><id>http://andre.arko.net/2025/10/12/jj-part-3-workflows/</id><content type="html"><![CDATA[<p>Previously on this blog: <a href="/2025/10/02/jj-part-2-commands/"><code>jj</code> part 2: commands</a></p>
<p>Now that you hopefully have an idea of how to operate jj, let’s look at the commands you need to get work done in jj. One great aspect of jj layering on top of git repos is that the git repo is still there underneath, and you can use any git command exactly like you usually would if there’s anything missing from your jj workflows.</p>
<h3 id="submit-a-pull-request">submit a pull request</h3>
<p>The flow to create and send a PR will probably look pretty familiar: use <code>jj git clone</code> to get a copy of the repo, make your changes, use <code>jj commit</code> to create your new commits. When you’re ready, use <code>jj bookmark set NAME</code> to give your changes a name and <code>jj git push</code> to create a new branch on the remote. Use GitHub.com or <code>gh pr create --head NAME</code> to open the PR.</p>
<p>If you amend the commits in your PR, you can force-push the new commits with <code>jj git push</code>. If you add new changes on top, you’ll need to <code>jj bookmark set NAME</code> to update the bookmark to the latest change before you <code>jj git push</code> again.</p>
<p>That’s the whole flow! Congratulations on migrating from git to jj for your everyday work.</p>
<p>If using <code>bookmark set</code> all the time gets tedious, there’s a community alias named <code>jj tug</code> that finds the closest bookmark and moves it to the closest pushable change. I personally wrote an alias for myself named <code>jj push</code> that I use to handle pushing new changes to existing remote branches. We’ll talk about those aliases in the next major section, which is about configuring jj.</p>
<h3 id="work-on-multiple-prs-at-once">work on multiple PRs at once</h3>
<p>One situation I often find myself in is working on two (or even more) pull requests at the same time. With the powerful commit-editing primitives provided by jj, there are at least two (and probably more) ways to structure this kind of parallel work.</p>
<p>The first option is what I think of as merge-based: create a merge commit that unifies the tips of your two or more branches using <code>jj new -d A -d B</code>, do your work, and create new commits with <code>jj split</code> or <code>jj commit</code>. Then,  rebase those commits using <code>jj rebase -r @- --insert-before A</code> or the like, moving the new commits backwards into one of the PR branches. This is the same as the “megamerge” strategy described above, but it works just as well with two branches.</p>
<p>The second option is to liberally rebase every branch on top of each other, creating a completely linear history where PR #4 contains PR #3, which also contains PR #2, which also contains PR #1. Since jj uses change IDs to keep track of changes as their commits are amended or rebased, you can rebase the entire chain on top of new commits to <code>main</code>. Your bookmarks will stay in the same place, and you can <code>jj git push</code> to update each remote branch. Any time one of the PRs lands, you rebase your full chain on top of the new <code>main</code> and resume work where you left off.</p>
<p>If you want to work on multiple branches at once, you will probably also find the articles <a href="https://ofcr.se/jujutsu-merge-workflow">Jujutsu Merge Workflow</a> and <a href="https://v5.chriskrycho.com/journal/jujutsu-megamerges-and-jj-absorb/">Jujutsu Megamerges and <code>jj absorb</code></a> interesting.</p>
<h3 id="further-workflow-reading">further workflow reading</h3>
<p>There are many new workflows that jj users have already developed, and this brief overview is just the tip of the iceberg. The jj docs include a section on <a href="https://jj-vcs.github.io/jj/latest/github/">using jj with GitHub or GitLab</a>, and there are some great reflections on different workflows in the blog posts  <a href="https://kubamartin.com/posts/introduction-to-the-jujutsu-vcs/">Jujutsu VCS Introduction and Patterns</a>, <a href="https://pksunkara.com/thoughts/git-experts-should-try-jujutsu/">Git experts should try Jujutsu</a>, and <a href="https://zerowidth.com/2025/jj-tips-and-tricks/">jj tips and tricks</a>.</p>
<h3 id="next-time">next time</h3>
<p>Next up, we&rsquo;re going to talk about configuring jj. Want to use a diff tool? A merge tool? Add your own commands? Optimize your day to day work? We&rsquo;ve got options.</p>
<p>Continue with <a href="/2025/10/15/jj-part-4-configuration/"><code>jj</code> part 4: configuration</a>.</p>
]]></content></entry><entry><title type="html">Announcing &lt;code>rv&lt;/code> 0.2</title><link href="https://andre.arko.net/2025/10/11/announcing-rv-0.2/"/><updated>2025-10-11T01:48:20-07:00</updated><id>http://andre.arko.net/2025/10/11/announcing-rv-0.2/</id><content type="html"><![CDATA[<p>With the help of many new contributors, and after many late nights wrestling with make, we are happy to (slightly belatedly) announce <a href="https://github.com/spinel-coop/rv/releases/tag/v0.2.0">the 0.2 release of rv</a>!</p>
<p>This version dramatically expands support for Rubies, shells, and architectures.</p>
<p>Rubies: we have added Ruby 3.3, as well as re-compiled all Ruby 3.3 and 3.4 versions with YJIT. On Linux, YJIT increases our glibc minimum version to 2.35 or higher. That means most distro releases from 2022 or later should work, but please let us know if you run into any problems.</p>
<p>Shells: we have added support for bash, fish, and nushell in addition to zsh.</p>
<p>Architectures: we have added Ruby compiled for macOS on x86, in addition to Apple Silicon, and added Ruby compiled for Linux on ARM, in addition to x86.</p>
<p>Special thanks to newest member of the maintainers&rsquo; team <a href="https://github.com/adamchalmers">@adamchalmers</a> for improving code and tests, adding code coverage and fuzzing, heroic amounts of issue triage, and nushell support. Additional thanks are due to all the new contributors in version 0.2, including <a href="https://github.com/Thomascountz">@Thomascountz</a>, <a href="https://github.com/lgarron">@lgarron</a>, <a href="https://github.com/coezbek">@coezbek</a>, and <a href="https://github.com/renatolond">@renatolond</a>.</p>
<p>To upgrade, run <code>brew install rv</code>, or check the <a href="https://github.com/spinel-coop/rv/releases/tag/v0.2.0">release notes</a> for other options.</p>
]]></content></entry><entry><title type="html">The RubyGems “security incident”</title><link href="https://andre.arko.net/2025/10/09/the-rubygems-security-incident/"/><updated>2025-10-09T19:45:15-07:00</updated><id>http://andre.arko.net/2025/10/09/the-rubygems-security-incident/</id><content type="html"><![CDATA[<p>Ruby Central posted an extremely concerning &ldquo;<a href="https://rubycentral.org/news/rubygems-org-aws-root-access-event-september-2025/">Incident Response Timeline</a>&rdquo; today, in which they make a number of exaggerated or purely misleading claims. Here&rsquo;s my effort to set the record straight.</p>
<p>First, and most importantly: <strong>I was a primary operator of RubyGems.org, securely and successfully, for over ten years. Ruby Central does not accuse me of any harms or damages in their post, in fact stating “we have no evidence to indicate that any RubyGems.org data was copied or retained by unauthorized parties, including Mr. Arko.”</strong></p>
<p>The actions I took during a time of great confusion and uncertainty (created by Ruby Central!) were careful, specific, and aimed to defend both Ruby Central the organization and RubyGems.org the service from potential threats.</p>
<p>The majority of the team, including developers in the middle of paid full-time work for Ruby Central, had just had all of their permissions on GitHub revoked. And then restored six days later. And then revoked again the next day. Even after the second mass-deletion of team permissions, Marty Haught sent an email to the team within minutes, at 12:47pm PDT, saying he was (direct quote) &ldquo;terribly sorry&rdquo; and &ldquo;I messed up&rdquo;. <small><strong>Update</strong>: Added email timestamp.</small></p>
<p>The erratic and contradictory communication supplied by Marty Haught, and the complete silence from Shan and the board, made it impossible to tell exactly who had been authorized to take what actions. As this situation occurred, I was the primary on-call. My contractual, paid responsibility to Ruby Central was to defend the RubyGems.org service against potential threats. </p>
<p>Marty&rsquo;s final email clearly stated &ldquo;I&rsquo;ll follow up more on this and engage with the governance rfc in good faith.&rdquo;. Just a few minutes after that email, at 1:01pm PDT, Marty also posted <a href="https://github.com/rubygems/rfcs/pull/61#issuecomment-3309461815">a public GitHub comment</a>, where he agreed to participate in the proposed governance process and stated &ldquo;I&rsquo;m committed to find the right governance model that works for us all. More to come.&rdquo; <small style="display: inline;"><strong>Update</strong>: screenshot of comment removed and replaced with link, since the comment appears to still be visible (at least to logged out users) on GitHub.</small></p>
<p>Given Marty&rsquo;s claims, the sudden permission deletions made no sense. Worried about the possibility of hacked accounts or some sort of social engineering, I took action as the primary on-call engineer to lock down the AWS account and prevent any actions by possible attackers. I did not change the email addresses on any accounts, leaving them all owned by a team-shared email at rubycentral.org, to ensure the organization retained overall control of the accounts, even if individuals were somehow taking unauthorized actions.</p>
<p>Within a couple of days, Ruby Central made an (unsigned) public statement, and various board members agreed to talk directly to maintainers. At that point, I realized that what I thought might have been a malicious takeover was both legitimate and deliberate, and Marty would never &ldquo;fix the permissions structure&rdquo;, or &ldquo;follow up more&rdquo; as he said.</p>
<p>Once I understood the situation, I backed off to let Ruby Central take care of their &ldquo;security audit&rdquo;. I left all accounts in a state where they could recover access. I did not alter, or try to alter, anything in the Ruby Central systems or GitHub repository after that. I was confident, at the time, that Ruby Central&rsquo;s security experts would quickly remove all outside access.</p>
<p>My confidence was sorely misplaced.</p>
<p>Almost two weeks later, someone asked if I still had access and I discovered (to my great alarm), that Ruby Central&rsquo;s &ldquo;security audit&rdquo; had failed. Ruby Central also had not removed me as an &ldquo;owner&rdquo; of the Ruby Central GitHub Organization. They also had not rotated any of the credentials shared across the operational team using the RubyGems 1Password account.</p>
<p>I believe Ruby Central confused themselves into thinking the &ldquo;Ruby Central&rdquo; 1Password account was used by operators, and they did revoke my access there. However, that 1Password account was not used by the open source team of RubyGems.org service operators. Instead, we used the &ldquo;RubyGems&rdquo; 1Password account, which was full of operational credentials. Ruby Central did not remove me from the &ldquo;RubyGems&rdquo; 1Password account, even as of today.</p>
<p>Aware that I needed to disclose this surprising access, but also aware that it was impossible for anyone except former operators to exploit this security failure, I immediately wrote an email to Ruby Central to disclose the problem.</p>
<p>Here is a copy of my disclosure email, in full.</p>
<pre tabindex="0"><code>From: André Arko &lt;andre@arko.net&gt;
Subject: Re: RubyGems.org access
Date: September 30, 2025 at 10:23:12 AM PDT
To: Marty Haught &lt;marty@rubycentral.org&gt;

Hi Marty,

It has come to my attention that despite the statements in [your] email, I have had uninterrupted access to RubyGems.org production environments from September 18 until today, September 30, via the root credentials of the Ruby Central AWS account, as well as continued and ongoing access to the full feed of production alerts and logs in DataDog.

It seems that the only permissions I have had removed are from the GitHub organization named &#34;rubygems&#34;, which as you know is unrelated to the RubyGems.org production access you mention in your email.

I have also noticed I am still, as of September 30, the owner of the GitHub organizations named &#34;rubycentral&#34; and &#34;rubytogether&#34;.

I am unable to transfer the HelpScout or PagerDuty accounts, as you have disabled my andre@rubygems.org Google account.

Please advise as to your desired resolution of this situation.

Thank you,
André Arko
</code></pre><p>Ruby Central did not reply to this email for over three days.</p>
<p>When they finally did reply, they seem to have developed some sort of theory that I was interested in &ldquo;access to PII&rdquo;, which is entirely false. <strong>I have no interest in any PII, commercially or otherwise</strong>. As my private email published by Ruby Central demonstrates, my entire proposal was based solely on company-level information, with no information about individuals included in any way. Here&rsquo;s their response, over three days later.</p>
<pre tabindex="0"><code>From: Marty Haught &lt;marty@rubycentral.org&gt;
Subject: Re: RubyGems.org access
Date: October 3, 2025 at 6:54:01 PM MDT
To: André Arko &lt;andre@arko.net&gt;

Hi André,

Please confirm that you cannot access the Ruby Central AWS root account credentials, either through the console or by access keys.

In addition, please confirm whether you are in possession of any RubyGems.org production data,  including, but not limited to, server logs, access logs, PII, or other organizational data.

Thank you,
Marty
</code></pre><p>In addition to ignoring the (huge) question of how Ruby Central failed to secure their AWS Root credentials for almost two weeks, and <strong>appearing to only be aware of it because I reported it to them</strong>, their reply also failed to ask whether any other shared credentials might still be valid. There were more.</p>
<pre tabindex="0"><code>From: André Arko &lt;andre@arko.net&gt;
Subject: Re: RubyGems.org access
Date: October 5, 2025 at 11:59:35 AM PDT
To: Marty Haught &lt;marty@rubycentral.org&gt;

Hi Marty,

Thanks for letting me know you got my email disclosing my unintended access. I’m concerned that security must not be a very high priority for Ruby Central since no one acknowledged my disclosure for more than three days, but I appreciate the confirmation.

As far as I can tell, I can no longer access the Ruby Central AWS root account either through the console or via access keys.

I confirm I did not download or save any production data after your email of September 18, including server logs, access logs, PII, or other organizational data.

However, while checking AWS credentials in order to write this email, I discovered that several other service credentials have not been rotated, and are still valid for production AWS access. That means both myself and the other former operators all still have access to AWS via those previously-shared credentials.

I would appreciate it if you could answer the request from my first email, and reply with your desired resolution for this remaining unintended production access, as well as the GitHub organization ownership.

Thanks,
André
</code></pre><p>Unbeknownst to me, while I was answering Marty&rsquo;s email in good faith, Ruby Central&rsquo;s attorney was sending my lawyer a letter alleging I had committed a federal crime, on the theory that I had &ldquo;hacked&rdquo; Ruby Central&rsquo;s AWS account. On the contrary, my actions were taken in defense of the service that Ruby Central was paying me to support and defend.</p>
<p>With my side of the story told, I&rsquo;ll leave it to you to decide whether you think it&rsquo;s true that &ldquo;Ruby Central remains committed to transparent, responsible stewardship of the RubyGems infrastructure and to maintaining the security and trust that the Ruby ecosystem depends on.&rdquo;</p>
]]></content></entry><entry><title type="html">Announcing gem.coop, a community gem server</title><link href="https://andre.arko.net/2025/10/05/announcing-gem-coop/"/><updated>2025-10-05T19:00:00-08:00</updated><id>http://andre.arko.net/2025/10/05/announcing-gem-coop/</id><content type="html"><![CDATA[<p>The team behind the last ten years of rubygems.org, including @deivid-rodriguez, @duckinator, @martinemde, @segiddins, @simi, and myself, is very pleased to announce a new gem server for the Ruby community: <a href="https://gem.coop">gem.coop</a>.</p>
<p>The new server’s governance policies are being prepared in coordination with Mike McQuaid of Homebrew, and will be released later this week.</p>
<p>The current versions of RubyGems and Bundler work with this new server already, and any Ruby developer is welcome to switch to using this new server immediately.</p>
<p>We have exciting plans to add new features and functionality in the coming days. <a href="https://gem.coop">Join us!</a></p>
]]></content></entry><entry><title type="html">&lt;code>jj&lt;/code> part 2: commands &amp; revsets</title><link href="https://andre.arko.net/2025/10/02/jj-part-2-commands/"/><updated>2025-10-02T10:52:56+09:00</updated><id>http://andre.arko.net/2025/10/02/jj-part-2-commands/</id><content type="html"><![CDATA[<p>Previously on this blog: <a href="/2025/09/28/jj-part-1-what-is-it/"><code>jj</code> part 1: what is it</a></p>
<p>Now, let’s take a look at the most common jj commands, with a special focus on the way arguments are generally consistent and switches don’t hide totally different additional commands.</p>
<h3 id="jj-log">jj log</h3>
<p>The log command is the biggest consumer of revsets, which are passed using <code>-r</code> or <code>--revisions</code>. With <code>@</code>, which is the jj version of <code>HEAD</code>, you can build a revset for exactly the commits you want to see. The git operator <code>..</code> is supported, allowing you to log commits after A and up to B with <code>-r A..B</code>, but that’s just the start. Here’s a quick list of some useful revsets to give you the flavor:</p>
<ul>
<li><code>@-</code> the parent of the current commit</li>
<li><code>kv+</code> the first child of the change named <code>kv</code></li>
<li><code>..A &amp; ..B</code> changes in the intersection of <code>A</code> and <code>B</code>’s ancestors</li>
<li><code>~description(glob:&quot;wip:\*&quot;)</code> changes whose message does <em>not</em> start with <code>wip:</code>, because tilde negates a revset</li>
<li><code>heads(::@ &amp; mutable() &amp; ~description(exact:&quot;&quot;) &amp; (~empty() | merges()))</code> the closest “pushable” change, meaning the nearest ancestor of <code>@</code> that is mutable (by default mutable means “not in the main/trunk branch”), that has some description set, and that either has some changes or is a merge commit. (Some jj merge commits can be empty, if there were no conflicts.)</li>
</ul>
<p>Using the jj config file, you can give any revset an alias, and then use that alias. I use <code>closest_pushable(@)</code> quite a bit, especially when naming branches and pushing.</p>
<p>For a full review of everything that’s possible with revsets, check out <a href="https://jj-vcs.github.io/jj/latest/revsets/">the revset documentation</a> and the blog post <a href="https://willhbr.net/2024/08/18/understanding-revsets-for-a-better-jj-log-output/">Understanding Revsets for a Better JJ Log Output</a>.</p>
<h3 id="jj-commit--desc--new--edit--split">jj commit / desc / new / edit / split</h3>
<p>The functionality of <code>git commit</code> is broken up into four separate jj commands. You use <code>new</code> to create a new empty child change, defaulting to <code>@</code>, and edit it. The <code>desc</code> command lets you set the description (or message) on a given change. The <code>commit</code> command works like git, but is effectively the same as <code>jj desc &amp;&amp; jj new</code>. You use <code>edit</code> to re-open an existing change for amending, and <code>split</code> to interactively select a diff to break out into a second change. These are all common git workflows, done by using flags or multiple git commands, made direct and straightforward single commands in jj.</p>
<h3 id="jj-restore--abandon">jj restore / abandon</h3>
<p>What if using <code>checkout</code> and <code>reset</code> to roll back either files or full commits had clearer names?</p>
<p>If you want to get back a file from a previous change, you can use <code>restore</code>. Specify which change you want to bring back, and also provide a file name or glob to limit the restoration to specific files.</p>
<p>Where you might have previously used <code>git reset</code> or <code>git checkout</code> to manipulate which commits are included in the current branch, you can now use <code>abandon</code> to remove entire changes from your history. Without any arguments it will remove <code>@</code>, the working commit, which is similar to <code>git reset --hard</code>. With arguments, <code>abandon</code> will remove all changes in the given revset from the local history.</p>
<h3 id="jj-bookmark-list--set--track">jj bookmark list / set / track</h3>
<p>Bookmarks are jj’s alternative to named git branches, and can be set up to automatically track a branch in a git remote. While compatibility with git branches is nice, names aren’t required by jj’s model. You can push your current unnamed change instantly with <code>jj git push --change @</code>, and jj will use the change ID (which stays the same across amends and rebases) as the git branch name. Now you don’t have to think of a good name for your branch before you can work on it (or push it!).</p>
<p>For more detail comparing and contrasting bookmarks to branches, I recommend the post <a href="https://neugierig.org/software/blog/2025/08/jj-bookmarks.html">Understanding Jujutsu bookmarks</a>.</p>
<h3 id="jj-git-push--fetch">jj git push / fetch</h3>
<p>It does what you would expect based on git, but the defaults are different than you might expect. Unless you configure the <code>git.fetch</code> and <code>git.push</code> settings, jj will only push to or fetch from <code>origin</code>. To operate on another remote, pass <code>--remote NAME</code>. To operate on all remotes, use <code>glob:*</code> as the remote name.</p>
<h3 id="jj-rebase--squash">jj rebase / squash</h3>
<p>The rebase command works like you would expect, but better. You can rebase a  single change to a different place with <code>jj rebase -r id --insert-before A</code>, or rebase a change and all it’s descendants with <code>jj rebase -s id --insert-after B</code>. You can even rebase an entire branch automatically with <code>jj rebase -b @ --destination C</code>, moving every ancestor of <code>@</code> that is not an ancestor of <code>C</code> into a new chain of commits descending from <code>C</code>. I did all of these constantly in git, and it’s much more involved.</p>
<p>The squash command is just a clear, single command for the common git operation where you move a diff into a commit or move a diff out of a commit, by change ID and/or filename.</p>
<h3 id="jj-merge-doesnt-exist">jj merge (doesn’t exist)</h3>
<p>The git rebase and merge commands (also including apply-patch, cherry-pick, and others) are all a bit special because they can create conflicts that have to be resolved before git will allow the commit to be… committed. This is the other half of the magic of jj: your new commit just holds any conflicts inside it. It’s impossible to lose work in a merge disaster because everything is always committed. You can resolve conflicts immediately, after other merges, or never! The results are always immediately stored, no matter how complete or incomplete your resolution is at the time.</p>
<p>Thanks to this feature, you don’t need a dedicated merge command—any new change can have however many parents you want, regardless of conflicts. It’s just as valid to <code>jj new A B C D E</code> as it is to <code>jj new A</code>. One pattern that is common in jj but was miserable in git is to create a “megamerge” combining all your current work branches. All editing happens on top of the megamerge, and you move individual changes backwards into a specific branch as you decide where to put them. Compared to git, it feels like magic.</p>
<h3 id="commands-beyond-git">commands beyond git</h3>
<p>There are many jj commands that have no analogous git command. Some real standouts include <code>jj absorb</code>, <code>jj parallelize</code>, and <code>jj undo</code>. We’ll talk more about those commands in a future post about jj beyond git.</p>
<h3 id="further-command-reading">further command reading</h3>
<p>The previously mentioned <a href="https://justinpombrio.net/src/jj-cheat-sheet.pdf">jj cheat sheet PDF</a> has a second page, containing a quick summary of each command, what it does, and the arguments it accepts.</p>
<h3 id="next-time">next time</h3>
<p>Now that we have talked about commands, next up is workflows! How can you use jj to work on a pull request? How can you work on multiple branches or PRs at the same time?</p>
<p>Continue with <a href="/2025/10/12/jj-part-3-workflows/"><code>jj</code> part 3: workflows</a>.</p>
<p>The full series also includes: <a href="/2025/10/15/jj-part-4-configuration/">part 4: configuration</a></p>
]]></content></entry><entry><title type="html">&lt;code>rv&lt;/code>, a Ruby manager for the future</title><link href="https://andre.arko.net/2025/09/30/rv-a-ruby-manager-for-the-future/"/><updated>2025-09-30T12:25:36-07:00</updated><id>http://andre.arko.net/2025/09/30/rv-a-ruby-manager-for-the-future/</id><content type="html"><![CDATA[<p><small>This post was originally given as a talk at the <a href="">SF Ruby Meetup</a>. The <a href="https://speakerdeck.com/indirect/rv-a-ruby-manager-for-the-future">slides</a> are also available.</small></p>
<script defer class="speakerdeck-embed" data-id="4591b4b2d21d42c399dea04572cc8cff" data-ratio="1.7777777777777777" src="//speakerdeck.com/assets/embed.js"></script>
<p>For the last ten years or so of working on Bundler, I’ve had a wish rattling around: I want a bigger, better dependency manager. It doesn’t just manage your gems, it manages your ruby versions, too. It doesn’t just manage your ruby versions, it installs pre-compiled rubies so you don’t have to wait for ruby to compile from source over and over. And more than all of that, it makes it completely trivial to run any script or tool written in ruby, even if that script or tool needs a different ruby and gems than your application does.</p>
<p>For the entire ten years of daydreaming, I’ve been hoping someone else would build it and I could just use it. Then I discovered that someone <em>did</em> build it… but for Python. It’s called <a href="https://docs.astral.sh/uv/"><code>uv</code></a>, and almost exactly one year ago <a href="https://astral.sh/blog/uv-unified-python-packaging">version 0.3 shipped</a> with all the features I had wished for, and even more that I hadn’t thought to wish for.</p>
<p>At this point, I’ve been using <code>uv</code> for almost a year and every time I use a project written in Python, the experience is delightful. Not only can you run a command directly out of any package that isn’t even installed, you can run a command that requires a python you don’t have installed, and <code>uv</code> takes care of installing the right python, installing the right packages, and running your command, in just a second or two.</p>
<p>Whether you want to run a CLI tool, a webapp, or a random script, <code>uv</code> always ensures the environment is correct as part of running the command. No more installing a new package version only to realize later you broke something old, no more setting up dependencies manually only to have the script running inside cron silently break later.</p>
<p>Earlier this year, my long time consulting job disappeared and I found myself looking for something to replace it. One of my ideas was to start a company inspired by <a href="https://geomys.org">Geomys</a> in the Go language, offering expert advice from open source maintainers, but the idea felt weak to me without a “spotlight” project to show off our expertise.</p>
<p>In July of this year, I finally realized that these two ideas could go together extremely well—the company can show our expertise by building this developer tool, and clients paying for our advice to solve their problems can ensure we are able to support and expand the tool.</p>
<p>I talked to some Ruby friends about the idea, and it resonated with them, so we started working on both the company and the open source project. Today, Spinel Cooperative has a website at <a href="https://spinel.coop">spinel.coop</a>, and <code>rv</code> has a website at <a href="https://rv.dev">rv.dev</a>. The team has expanded, and includes notable RubyGems and Bundler contributors <a href="https://segiddins.me">Samuel Giddins</a> and <a href="https://github.com/deivid-rodriguez">David Rodriguez</a>, notable Rails contributors <a href="https://kaspth.com">Kasper Timm Hanson</a> and <a href="https://sls.name">Sam Stephenson</a>, who is also the original creator of <a href="https://rbenv.org">rbenv</a> and ruby-build.</p>
<p>Our goal is a completely new kind of management tool, where you don’t need to install rvm and then some ruby and then update rubygems and bundler and then bundle install your gems—you just run your command, and everything is handled. Not a version manager, or a dependency manager, but both of those things and much more.</p>
<p>With that vision in place, we were now faced with a very practical question. What can we build that would be useful right away? We landed on precompiled rubies for development work as the most useful place to start, and got to work.</p>
<p>We&rsquo;re using Rust to build <code>rv</code>, for two reasons. The obvious reason is that Rust produces very fast results, which is also why our biggest inspiration <code>uv</code> is written in Rust. The less obvious reason is based on years of trying to onboard new contributors to Bundler and RubyGems—it turns out if you are a Ruby developer, you unfortunately don&rsquo;t (yet) know the subset of Ruby that we are forced to use for Bundler and RubyGems.</p>
<p>There are two major things that basically every Ruby program does that you can&rsquo;t do if you are managing gems. First, you can&rsquo;t use any gems. If you want to use code that&rsquo;s inside a gem, you need to copy that code wholesale into Bundler or RubyGems, and then you need to constantly update it anytime that gem has any changes. Second, you can&rsquo;t use anything with native extensions, ever. JSON gem? Psych gem for YAML? Completely impossible, because Bundler and RubyGems need to be installable even if there is no compiler present.</p>
<p>So with those constraints in mind, and with a clear goal in mind of a tool so fast you normally can&rsquo;t even tell it&rsquo;s running, we settled on Rust, and started building a CLI. I&rsquo;ve used Rust for smaller personal projects in the past, but never created a full CLI tool. I am happy to report that the <code>clap</code> library for creating CLIs in Rust is great, and recommend it to anyone who might be interested.</p>
<p>The next piece that we needed was precompiled Rubies. There are a few big projects out there compiling Ruby in advance, mostly for use on servers. The <code>setup-ruby</code> GitHub action and the official Ruby docker images are both based on the <code>ruby-build</code> project originally started as part of <code>rbenv</code>.</p>
<p>Unfortunately, those existing precompiled Ruby versions aren&rsquo;t usable for our needs because they aren&rsquo;t <strong>statically compiled</strong> and because they aren&rsquo;t <strong>relocatable</strong>. Statically compiled (as opposed to dynamically compiled) means that Ruby copies the code from a shared library into its own binary.</p>
<p>Show of hands&hellip; have any of you ever had trouble compiling Ruby because of OpenSSL? Okay, put your hands down. Now, how many of you have had an already-installed Ruby suddenly stop working because of OpenSSL, and you had to install it again? Good news, <code>rv</code> fixes both of those problems by putting the OpenSSL inside the Ruby, so they can never get separated.</p>
<p>There is a tradeoff here—if there is a critical security flaw in OpenSSL, we will need to compile Ruby again to include the critical security update. The first reason we are okay with this tradeoff is that OpenSSL doesn&rsquo;t have huge security issues very often. The second reason we are okay with this is that your production servers are probably using the official Ruby docker images and not Ruby installed by <code>rv</code>, so it&rsquo;s even less of a concern.</p>
<p>In the end, the closest existing system we were able to build on top of was Homebrew&rsquo;s <code>portable-ruby</code> project. That&rsquo;s the system Homebrew uses to build the Ruby install that Homebrew itself runs on. The Homebrew team built some excellent infrastructure for building a statically linked Ruby, and even added the changes needed to make sure that Ruby could be relocated.</p>
<p>Since Homebrew needs to be able to install into <code>/usr/local</code> on x86, but <code>/opt/homebrew</code> on Apple Silicon, and into any user&rsquo;s home directory for Linuxbrew, they need to be able to take a single precompiled Ruby and put it in any location on disk. That&rsquo;s another one of the requirements that isn&rsquo;t met by the <code>setup-ruby</code> or Docker image Rubies—if you move them to another directory, they stop working.</p>
<p>Using Homebrew&rsquo;s <code>portable-ruby</code> as a base, we were able to start with macOS ARM and Ubuntu x86, add Ubuntu on ARM, and then build every version in the Ruby 3.4.x series. After a few weeks of work, we had some initial functionality working. <code>rv</code> could switch between installed Ruby versions in zsh, but most importantly <strong>it could install precompiled Ruby 3.4.x on macOS and Ubuntu in one second flat</strong>. Yes, you heard that right. <code>rv ruby install 3.4.5</code>. Wait 1 second. Done. You can run Ruby commands now.</p>
<p>With that proof of concept complete, we announced version 0.1. People got very excited! It was fantastic to see how many people were excited by the vision for a new, fast tool for Ruby development.</p>
<p>It&rsquo;s been a few weeks since that 0.1 release, and we have been hard at work. We&rsquo;ve expanded the team to include community contributors, merged pull requests, and compiled more Rubies than ever.</p>
<p>When 0.2 is released, in the very near future, it will include support for not just zsh, but bash, fish, and nushell. We have also added support for macOS on x86, meaning we now fully support x86 and ARM on both macOS and Linux. Finally, the precompiled Ruby versions available will expand to include all of Ruby 3.3 as well as 3.4. Just to top all of that off, every version of Ruby will have YJIT built in.</p>
<p>Our short-term plans include finishing support for Ruby 3.2, adding rubies that work with musl libc on Linux, and testing on more Linux distributions. Our longer-term plans including improving the way gems are compiled, so that installing your entire application and all of its gems can happen in just a few seconds.</p>
<p>We want to live in a future where anyone can run a Ruby command, or tool, or application in seconds (or less!). We&rsquo;re going to build that future, for ourselves and for everyone else.</p>
]]></content></entry><entry><title type="html">stupid jj tricks</title><link href="https://andre.arko.net/2025/09/28/stupid-jj-tricks/"/><updated>2025-09-28T11:00:00-08:00</updated><id>http://andre.arko.net/2025/09/28/stupid-jj-tricks/</id><content type="html"><![CDATA[<p><small>This post was originally given as a talk for <a href="https://github.com/jj-vcs/jj/wiki/JJ-Con-2025">JJ Con</a>. The <a href="https://speakerdeck.com/indirect/stupid-jj-tricks">slides</a> and <a href="https://www.youtube.com/watch?v=ZnTNFIMjDwg">video</a> are also available.</small></p>
<script defer class="speakerdeck-embed" data-id="f204b260f0ba4c6186ba335b01dbe28d" data-ratio="1.7777777777777777" src="//speakerdeck.com/assets/embed.js"></script>
<p><b>WARNING: This content was written for (and presented at) the inagural JJ Con, a conference for <code>jj</code> enthusiasts and contributors. If you&rsquo;re new to using <code>jj</code>, I strongly recommend you read my other posts about <code>jj</code> first: <a href="/2025/09/28/jj-part-1-what-is-it/">part 1</a>, <a href="/2025/10/02/jj-part-2-commands/">part 2</a>, <a href="/2025/10/12/jj-part-3-workflows/">part 3</a>, <a href="/2025/10/15/jj-part-4-configuration/">part 4</a>.</b></p>
<p>Welcome to “stupid jj tricks”. Today, I’ll be taking you on a tour through many different jj configurations that I have collected while scouring the internet. Some of what I’ll show is original research or construction created by me personally, but a lot of these things are sourced from blog post, gists, GitHub issues, Reddit posts, Discord messages, and more.</p>
<p>To kick things off, let me introduce myself. My name is André Arko, and I’m probably best known for spending the last 15 years maintaining the Ruby language dependency manager, Bundler. In the <code>jj</code> world, though, my claim to fame is completely different: Steve Klabnik once lived in my apartment for about a year, so I’m definitely an authority on everything about <code>jj</code>. Thanks in advance for putting into the official tutorial that whatever I say here is now authoritative and how things should be done by everyone using <code>jj</code>, Steve.</p>
<h3 id="general-configuration">general configuration</h3>
<p>The first jj tricks that I’d like to quickly cover are some of the most basic, just to make sure that we’re all on the same page before we move on to more complicated stuff.</p>
<p>To start with, did you know that you can globally configure jj to change your name and email based on a path prefix? You don’t have to remember to set your work email separately in each work repo anymore.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[[<span style="color:#a6e22e">--scope</span>]]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">--when</span>.<span style="color:#a6e22e">repositories</span> = [<span style="color:#e6db74">&#34;~/work&#34;</span>]
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">--scope</span>.<span style="color:#a6e22e">user</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">email</span> = <span style="color:#e6db74">&#34;me@work.domain&#34;</span>
</span></span></code></pre></div><p>I also highly recommend trying out multiple options for formatting your diffs, so you can find the one that is most helpful to you. A very popular diff formatter is <code>difftastic</code>, which provides syntax aware diffs for many languages. I personally use <code>delta</code>, and the configuration to format diffs with delta looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[[<span style="color:#a6e22e">--scope</span>]]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">--when</span>.<span style="color:#a6e22e">commands</span> = [<span style="color:#e6db74">&#34;diff&#34;</span>, <span style="color:#e6db74">&#34;show&#34;</span>]
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">--scope</span>.<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pager</span> = <span style="color:#e6db74">&#34;delta&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">diff-formatter</span> = <span style="color:#e6db74">&#34;:git&#34;</span>
</span></span></code></pre></div><p>Another very impactful configuration is which tool jj uses to handle interactive diff editing, such as in the <code>jj split</code> or <code>jj squash -i</code> commands. While the default terminal UI is pretty good, make sure to also try out Meld, an open source GUI.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">diff-editor</span> = <span style="color:#e6db74">&#34;meld&#34;</span> <span style="color:#75715e"># or vimdiff, vscode, etc</span>
</span></span></code></pre></div><p>In addition to changing the diff editor, you can also change the merge editor, which is the program that is used to resolve conflicts. Meld can again be a good option, as well as any of several other merging tools.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">merge-editor</span> = <span style="color:#e6db74">&#34;meld&#34;</span> <span style="color:#75715e"># or vimdiff, vscode, mergiraf etc</span>
</span></span></code></pre></div><p>Tools like mergiraf provide a way to attempt syntax-aware automated conflict resolution before handing off any remaining conflicts to a human to resolve. That approach can dramatically reduce the amount of time you spend manually handling conflicts.</p>
<p>You might even want to try FileMerge, the macOS developer tools built-in merge tool. It supports both interactive diff editing and conflict resolution.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">merge-tools</span>.<span style="color:#a6e22e">filemerge</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">program</span> = <span style="color:#e6db74">&#34;open&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">edit-args</span> = [<span style="color:#e6db74">&#34;-a&#34;</span>, <span style="color:#e6db74">&#34;FileMerge&#34;</span>, <span style="color:#e6db74">&#34;-n&#34;</span>, <span style="color:#e6db74">&#34;-W&#34;</span>, <span style="color:#e6db74">&#34;--args&#34;</span>,
</span></span><span style="display:flex;"><span>             <span style="color:#e6db74">&#34;-left&#34;</span>, <span style="color:#e6db74">&#34;$left&#34;</span>, <span style="color:#e6db74">&#34;-right&#34;</span>, <span style="color:#e6db74">&#34;$right&#34;</span>,
</span></span><span style="display:flex;"><span>             <span style="color:#e6db74">&#34;-merge&#34;</span>, <span style="color:#e6db74">&#34;$output&#34;</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">merge-args</span> = [<span style="color:#e6db74">&#34;-a&#34;</span>, <span style="color:#e6db74">&#34;FileMerge&#34;</span>, <span style="color:#e6db74">&#34;-n&#34;</span>, <span style="color:#e6db74">&#34;-W&#34;</span>, <span style="color:#e6db74">&#34;--args&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;-left&#34;</span>, <span style="color:#e6db74">&#34;$left&#34;</span>, <span style="color:#e6db74">&#34;-right&#34;</span>, <span style="color:#e6db74">&#34;$right&#34;</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#e6db74">&#34;-ancestor&#34;</span>, <span style="color:#e6db74">&#34;$base&#34;</span>, <span style="color:#e6db74">&#34;-merge&#34;</span>, <span style="color:#e6db74">&#34;$output&#34;</span>,]
</span></span></code></pre></div><p>Just two more configurations before we move on to templates. First, the default subcommand, which controls what gets run if you just type <code>jj</code> and hit return. The default is to run <code>jj log</code>, but my own personal obsessive twitch is to run <code>jj status</code> constantly, and so I have changed my default subcommand to <code>status</code>, like so:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">ui</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">default-command</span> = [<span style="color:#e6db74">&#34;status&#34;</span>]
</span></span></code></pre></div><p>The last significant configuration is the default revset used by <code>jj log</code>. Depending on your work patterns, the multi-page history of commits in your current repo might not be helpful to you. In that case, you can change the default revset shown by the log command to one that’s more helpful. My own default revset shows only one change from my origin. If I want to see more than the newest change from my origin I use <code>jj ll</code> to get the longer log, using the original default revset. I&rsquo;ll show that off later.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">revsets</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">log</span> = <span style="color:#e6db74">&#34;(trunk()..@):: | (trunk()..@)-&#34;</span>
</span></span></code></pre></div><h3 id="templates">templates</h3>
<p>Okay, enough of plain configuration. Now let’s talk about templates! Templates make it possible to do many, many things with jj that were not originally planned or built in, and I think that’s beautiful.</p>
<p>First, if you haven’t tried this yet, please do yourself a favor and go try every builtin jj template style for the <code>log</code> command. You can list them all with <code>jj log -T</code>, and you can try them each out with <code>jj log -T NAME</code>. If you find a builtin log style that you especially like, maybe you should set it as your default template style and skip the rest of this section. For the rest of you sickos, let’s see some more options.</p>
<p>The first thing that I want to show you all is the draft commit description. When you run <code>jj commit</code>, this is the template that gets generated and sent to your editor for you to complete. Since I am the kind of person who always sets git commit to verbose mode, I wanted to keep being able to see the diff of what I was committing in my editor when using jj. Here’s what that looks like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">templates</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">draft_commit_description</span> = <span style="color:#e6db74">&#39;&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  concat(
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    coalesce(description, default_commit_description, &#34;\n&#34;),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    surround(
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      &#34;\nJJ: This commit contains the following changes:\n&#34;, &#34;&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      indent(&#34;JJ:     &#34;, diff.stat(72)),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    ),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;\nJJ: ignore-rest\n&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    diff.git(),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  )
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;&#39;&#39;</span>
</span></span></code></pre></div><p>If you’re not already familiar with the jj template functions, this uses <code>concat</code> to combine strings, <code>coalesce</code> to choose the first value that isn’t empty, <code>surround</code> to add before+after if the middle isn’t empty, and <code>indent</code> to make sure the diff status is fully aligned. With this template, you get a preview of the diff you are committing directly inside your editor, underneath the commit message you are writing.</p>
<p>Now let’s look at the overridable subtemplates. The default templates are made of many repeated pieces, including IDs, timestamps, ascii art symbols to show the commit graph visually, and more. Each of those pieces can be overrides, giving you custom formats without having to change the default template that you use.</p>
<p>For example, if you are a UTC sicko, you can change all timestamps to render in UTC like <code>2025-02-17 21:23:47.000 +00:00</code>, with this configuration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">template-aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;format_timestamp(timestamp)&#34;</span> = <span style="color:#e6db74">&#34;timestamp.utc()&#34;</span>
</span></span></code></pre></div><p>Or alternatively, you can force all timestamps to print out in full, like <code>2025-02-13 01:53:08.000 -08:00</code> (which is similar to the default, but includes the time zone) by returning just the timestamp itself:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">template-aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;format_timestamp(timestamp)&#34;</span> = <span style="color:#e6db74">&#34;timestamp&#34;</span>
</span></span></code></pre></div><p>And finally you can set all timestamps to show a “relative” distance, like <code>7 months ago</code>, rather than a direct timestamp:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">template-aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;format_timestamp(timestamp)&#34;</span> = <span style="color:#e6db74">&#34;timestamp.ago()&#34;</span>
</span></span></code></pre></div><p>Another interesting example of a template fragment is supplied by <code>@scott2000</code> on GitHub, who changes the node icon specifically to show which commits might be pushed on the next <code>jj git push</code> command.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">templates</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">log_node</span> = <span style="color:#e6db74">&#39;&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">if(self &amp;&amp; !current_working_copy &amp;&amp; !immutable &amp;&amp; !conflict &amp;&amp; in_branch(self),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#34;◇&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  builtin_log_node
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">template-aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;in_branch(commit)&#34;</span> = <span style="color:#e6db74">&#39;commit.contained_in(&#34;immutable_heads()..bookmarks()&#34;)&#39;</span>
</span></span></code></pre></div><p>This override of the <code>log_node</code> template returns a hollow diamond if the change meets some pushable criteria, and otherwise returns the <code>builtin_log_node</code>, which is the regular icon.</p>
<p>It’s not a fragment, but I once spent a good two hours trying to figure out how to get a template to render just a commit message body, without the “title” line at the top. Searching through all of the built-in jj templates finally revealed the secret to me, which is a template function named <code>remove_prefix()</code>. With that knowledge, it becomes possible to write a template that returns only the body of a commit message:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">description_body</span> = <span style="color:#e6db74">&#39;description.remove_prefix(description.first_line()).trim_start()&#39;</span>
</span></span></code></pre></div><p>We first extract the title line, remove that from the front, and then trim any whitespace from the start of the string, leaving just the description body.</p>
<p>Finally, I’d like to briefly look at the possibility of machine-readable templates. Attempting to produce JSON from a jj template string can be somewhat fraught, since it’s hard to tell if there are quotes or newlines inside any particular value that would need to be escaped for a JSON object to be valid when it is printed. Fortunately, about 6 months ago, jj merged an <code>escape_json()</code> function, which makes it possible to generate valid JSON with a little bit of template trickery. For example, we could create a <code>log</code> output of a JSON stream document including one JSON object per commit, with a template like this one:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">log_json_stream</span> = <span style="color:#e6db74">&#39;&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#34;{&#34; ++ 
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;change_id&#34;.escape_json() ++ &#34;: &#34; ++ stringify(change_id).escape_json() ++ &#34;, &#34; ++ 
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;author&#34;.escape_json() ++ &#34;: &#34; ++ stringify(author).escape_json() ++
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#34;}\n&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;&#39;&#39;</span>
</span></span></code></pre></div><p>This template produces valid JSON that can then be read and processed by other tools, looks like this.</p>
<p><img src="json.png" alt="json output from jj log, parsed and formatted by jq"></p>
<p><strong>Update:</strong> there is now a <code>json()</code> template function, which makes it much simpler to output valid JSON, like so: <code>jj log --no-graph -T 'json(self) ++ &quot;\n&quot;'</code>.</p>
<p>Templates have vast possibilities that have not yet been touched on, and I encourage you to investigate and experiment yourself.</p>
<h3 id="revsets">revsets</h3>
<p>Now let’s look at some revsets. The biggest source of revset aliases that I have seen online is from @thoughtpolice’s jjconfig gist, but I will consolidate across several different config files here to demonstrate some options.</p>
<p>The first group of revsets roughly corresponds to “who made it”, and composes well with other revsets in the future. For example, it’s common to see a <code>user(x)</code> type alias, and a <code>mine()</code> type alias to let the current user easily identify any commits that they were either author or committer on, even if they used multiple different email addresses.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#e6db74">&#39;user(x)&#39;</span> = <span style="color:#e6db74">&#39;author(x) | committer(x)&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;mine()&#39;</span> = <span style="color:#e6db74">&#39;user(&#34;me@personal.domain&#34;) | user(&#34;me@domain&#34;)&#39;</span>
</span></span></code></pre></div><p>Another group uses description prefixes to identify commits that have some property, like WIP or “private”. It’s then possible to use these in other revsets to exclude these commits, or even to configure jj to refuse to push them.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#e6db74">&#39;wip()&#39;</span> = <span style="color:#e6db74">&#39;description(glob:&#34;wip:*&#34;)&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;private()&#39;</span> = <span style="color:#e6db74">&#39;description(glob:&#34;private:*&#34;)&#39;</span>
</span></span></code></pre></div><p>Thoughtpolice seems to have invented the idea of a <code>stack</code>, which is a group of commits on top of some parent:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#75715e"># stack(x, n) is the set of mutable commits reachable from &#39;x&#39;,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># with &#39;n&#39; parents. &#39;n&#39; is often useful to customize the display</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># and return set for certain operations. &#39;x&#39; can be used to target</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># the set of &#39;roots&#39; to traverse, e.g. @ is the current stack.</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;stack()&#39;</span> = <span style="color:#e6db74">&#39;stack(@)&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;stack(x)&#39;</span> = <span style="color:#e6db74">&#39;stack(x, 2)&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;stack(x, n)&#39;</span> = <span style="color:#e6db74">&#39;ancestors(reachable(x, mutable()), n)&#39;</span>
</span></span></code></pre></div><p>Building on top of the stack, it’s possible to construct a set of commits that are “open”, meaning any stack reachable from the current commit or other commits authored by the user. By setting the stack value to 1, nothing from trunk or other remote commits is included, so every open commit is mutable, and could be changed or pushed.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#e6db74">&#39;open()&#39;</span> = <span style="color:#e6db74">&#39;stack(mine() | @, 1)&#39;</span>
</span></span></code></pre></div><p>Finally, building on top of the open revset, it’s possible to define a “ready” revset that is every open change that isn’t a child of wip or private change:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#e6db74">&#39;ready()&#39;</span> = <span style="color:#e6db74">&#39;open() ~ descendants(wip() | private())&#39;</span>
</span></span></code></pre></div><p>It’s also possible to create a revset of “interesting” commits by using the opposite kind of logic, as in this chain of revsets composed by <code>@sunshowers</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#e6db74">&#39;uninterested()&#39;</span> = <span style="color:#e6db74">&#39;::remote_bookmarks() | tags()&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;interested()&#39;</span> = <span style="color:#e6db74">&#39;mine() ~ uninterested()&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;open()&#39;</span> = <span style="color:#e6db74">&#39;&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    ancestors(interested(), 3)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      | tracked_remote_bookmarks()
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      | ancestors(@, 3)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;&#39;&#39;</span>
</span></span></code></pre></div><p>You take remote commits and tags, then subtract those from our own commits, and then show anything that is either local-only, tracking the remote, or close to the current commit.</p>
<h3 id="commands">commands</h3>
<p>Now let’s talk about jj commands. You probably think I mean creating jj commands by writing our own aliases, but I don’t! That’s the next section. This section is about the jj commands that it took me weeks or months to realize existed, and understand how powerful they are.</p>
<p>First up: <code>jj absorb</code>. When I first read about absorb, I thought it was the exact inverse of squash, allowing you to choose a diff that you would bring into the current commit rather than eject out of the current commit. That is wildly wrong, and so I want to make sure that no one else falls victim to this misconception. The absorb command iterates over every diff in the current commit, finds the previous commit that changed those lines, and squashes just that section of the diff back to that commit. So if you make changes in four places, impacting four previous commits, you can <code>jj absorb</code> to squash all four sections back into all four commits with no further input whatsoever.</p>
<p>Then, <code>jj parallelize</code>. If you’re taking advantage of jj’s amazing ability to not need branches, and just making commits and squashing bits around as needed until you have each diff combined into one change per thing you need to submit… you can break out the entire chain of separate changes into one commit on top of trunk for each one by just running <code>jj parallelize 'trunk()..@'</code> and letting jj do all the work for you.</p>
<p>Last command, and most recent one: <code>jj fix</code>. You can use fix to run a linter or formatter on every commit in your history before you push, making sure both that you won’t have any failures and that you won’t have any conflicts if you try to reorder any of the commits later.</p>
<p>To configure the fix command, add a tool and a glob in your config file, like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">fix</span>.<span style="color:#a6e22e">tools</span>.<span style="color:#a6e22e">black</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">command</span> = [<span style="color:#e6db74">&#34;/usr/bin/black&#34;</span>, <span style="color:#e6db74">&#34;-&#34;</span>, <span style="color:#e6db74">&#34;--stdin-filename=$path&#34;</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">patterns</span> = [<span style="color:#e6db74">&#34;glob:&#39;**/*.py&#39;&#34;</span>]
</span></span></code></pre></div><p>Now you can just <code>jj fix</code> and know that all of your commits are possible to reorder without causing linter fix conflicts. It’s great.</p>
<h3 id="aliases">aliases</h3>
<p>Okay. Now we can talk about command aliases. First up, the venerable <code>tug</code>. In the simplest possible form, it takes the closest bookmark, and moves that bookmark to <code>@-</code>, the parent of the current commit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">tug</span> = [<span style="color:#e6db74">&#34;bookmark&#34;</span>, <span style="color:#e6db74">&#34;move&#34;</span>, <span style="color:#e6db74">&#34;--from&#34;</span>, <span style="color:#e6db74">&#34;heads(::@- &amp; bookmarks())&#34;</span>, <span style="color:#e6db74">&#34;--to&#34;</span>, <span style="color:#e6db74">&#34;@-&#34;</span>]
</span></span></code></pre></div><p>What if you want it to be smarter, though? It could find the closest bookmark, and then move it to the closest <em>pushable</em> commit, whether that commit was <code>@</code>, or <code>@-</code>, or <code>@---</code>. For that, you can create a revset for <code>closest_pushable</code>, and then tug from the closest bookmark to the closest pushable, like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">revset-aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;closest_pushable(to)&#39;</span> = <span style="color:#e6db74">&#39;heads(::to &amp; mutable() &amp; ~description(exact:&#34;&#34;) &amp; (~empty() | merges()))&#39;</span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">tug</span> = <span style="color:#e6db74">&#39;bookmark move --from &#34;heads(::@ &amp; bookmarks())&#34; --to &#34;closest_pushable(@)&#34;&#39;</span>
</span></span></code></pre></div><p>Now your bookmark jumps up to the change that you can actually push, by excluding immutable, empty, or descriptionless commits.</p>
<p>What if you wanted to allow tug to take arguments, for those times when two bookmarks are on the same change, or when you actually want to tug a different bookmark than the closest one? That’s also pretty easy, by adding a second variant of the tug command that takes an argument:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">tug</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;sh&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">if [ &#34;</span><span style="color:#a6e22e">x</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#34; = &#34;</span><span style="color:#a6e22e">x</span><span style="color:#e6db74">&#34; ]; then
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  jj bookmark move --from &#34;</span><span style="color:#a6e22e">closest_bookmark</span><span style="color:#960050;background-color:#1e0010">(@)</span><span style="color:#e6db74">&#34; --to &#34;</span><span style="color:#a6e22e">closest_pushable</span><span style="color:#960050;background-color:#1e0010">(@)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">else
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  jj bookmark move --to &#34;</span><span style="color:#a6e22e">closest_pushable</span><span style="color:#960050;background-color:#1e0010">(@)</span><span style="color:#e6db74">&#34; &#34;</span><span style="color:#960050;background-color:#1e0010">$@</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">fi
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>]
</span></span></code></pre></div><p>This version of tug works just like the previous one if no argument is given. But if you do pass an argument, it will move the bookmark with the name that you passed instead of the closest one.</p>
<p>How about if you’ve just pushed to GitHub, and you want to create a pull request from that pushed bookmark? The <code>gh pr create</code> command isn’t smart enough to figure that out automatically, but you can tell it which bookmark to use:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">pr</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">gh pr create --head $(jj log -r &#39;closest_bookmark(@)&#39; -T &#39;bookmarks&#39; --no-graph | cut -d &#39; &#39; -f 1)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>]
</span></span></code></pre></div><p>Just grab the list of bookmarks attached to the closest bookmark, take the first one, pass it to <code>gh pr create</code>, and you’re all set.</p>
<p>What if you just want single commands that let you work against a git remote, with defaults tuned for automatic tugging, pushing, and tracking? I’ve also got you covered.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">init</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj git init --colocate
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># only track origin branches, not upstream or others
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj bookmark track &#39;glob:*@origin&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>]
</span></span></code></pre></div><p>Use <code>jj init</code> to colocate jj into this git repo, and then track any branches from upstream, like you would get from a git clone.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">pull</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">closest=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#39;closest_bookmark(@)&#39;</span> <span style="color:#a6e22e">-n</span> <span style="color:#ae81ff">1</span> <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;bookmarks&#39;</span> <span style="color:#a6e22e">--no-graph</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#a6e22e">cut</span> <span style="color:#a6e22e">-d</span> <span style="color:#e6db74">&#39; &#39;</span> <span style="color:#a6e22e">-f</span> <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">closest=&#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span><span style="color:#960050;background-color:#1e0010">%\\*</span>}<span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj git fetch
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj log -n 1 -r &#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span>}<span style="color:#e6db74">&#34; 2&gt;&amp;1 &gt; /dev/null &amp;&amp; jj rebase -d &#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span>}<span style="color:#e6db74">&#34; || jj rebase -d &#39;trunk()&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">jj log -r &#39;stack()&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>]
</span></span></code></pre></div><p>Then, you can <code>jj pull</code> to find the closest bookmark to <code>@</code>, do a git fetch, rebase your current local commits on top of whatever just got pulled, and then show your new stack. When you’re done, just <code>jj push</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">push</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;bash&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">tuggable=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#39;closest_bookmark(@)..closest_pushable(@)&#39;</span> <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;&#34;n&#34;&#39;</span> <span style="color:#a6e22e">--no-graph</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">[[ -n &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">tuggable</span><span style="color:#e6db74">&#34; ]] &amp;&amp; jj tug
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">pushable=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#39;remote_bookmarks(remote=origin)..@&#39;</span> <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;bookmarks&#39;</span> <span style="color:#a6e22e">--no-graph</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">[[ -n &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">pushable</span><span style="color:#e6db74">&#34; ]] &amp;&amp; jj git push || echo &#34;</span><span style="color:#a6e22e">Nothing</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">push</span>.<span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">closest=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#39;closest_bookmark(@)&#39;</span> <span style="color:#a6e22e">-n</span> <span style="color:#ae81ff">1</span> <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;bookmarks&#39;</span> <span style="color:#a6e22e">--no-graph</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#a6e22e">cut</span> <span style="color:#a6e22e">-d</span> <span style="color:#e6db74">&#39; &#39;</span> <span style="color:#a6e22e">-f</span> <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">closest=&#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span><span style="color:#960050;background-color:#1e0010">%\\*</span>}<span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">tracked=&#34;</span><span style="color:#960050;background-color:#1e0010">$(</span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">bookmark</span> <span style="color:#a6e22e">list</span> <span style="color:#a6e22e">-r</span> <span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span>} <span style="color:#a6e22e">-t</span> <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;if(remote == &#34;origin&#34;, name)&#39;</span><span style="color:#960050;background-color:#1e0010">)</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">[[ &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">tracked</span><span style="color:#e6db74">&#34; == &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#a6e22e">closest</span><span style="color:#e6db74">&#34; ]] || jj bookmark track &#34;</span><span style="color:#960050;background-color:#1e0010">$</span>{<span style="color:#a6e22e">closest</span>}<span style="color:#960050;background-color:#1e0010">@</span><span style="color:#a6e22e">origin</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>]
</span></span></code></pre></div><p>This push handles looking for a tuggable bookmark, tugging it, doing a git push, and making sure that you’re tracking the origin copy of whatever you just pushed, in case you created a new branch.</p>
<h3 id="combo-tricks">combo tricks</h3>
<p>Last, but definitely most stupid, I want to show off a few combo tricks that manage to deliver some things I think are genuinely useful, but in a sort of cursed way.</p>
<p>First, we have counting commits. In git, you can pass an option to log that simply returns a number rather than a log output. Since jj doesn’t have anything like that, I was forced to build my own when I wanted my shell prompt to show how many commits beyond trunk I had committed locally. In the end, I landed on a template consisting of a single character per commit, which I then counted with <code>wc</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">jj</span> <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">--no-graph</span> <span style="color:#a6e22e">-r</span> <span style="color:#e6db74">&#34;main..@ &amp; (~empty() | merges())&#34;</span> <span style="color:#a6e22e">-T</span> <span style="color:#e6db74">&#39;&#34;n&#34;&#39;</span> <span style="color:#ae81ff">2</span><span style="color:#960050;background-color:#1e0010">&gt;</span> <span style="color:#960050;background-color:#1e0010">/</span><span style="color:#a6e22e">dev</span><span style="color:#960050;background-color:#1e0010">/</span><span style="color:#a6e22e">null</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#a6e22e">wc</span> <span style="color:#a6e22e">-c</span> <span style="color:#960050;background-color:#1e0010">|</span> <span style="color:#a6e22e">tr</span> <span style="color:#a6e22e">-d</span> <span style="color:#e6db74">&#39; &#39;</span>
</span></span></code></pre></div><p>That’s <a href="https://github.com/jj-vcs/jj/discussions/6683">the best anyone on GitHub could come up with, too</a>. See? I warned you it was stupid.</p>
<p>Next, via <code>@marchyman</code> on Discord, I present: <code>jj log</code> except for the closest three commits it also shows <code>jj status</code> at the same time.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">ll</span> = [<span style="color:#e6db74">&#34;log&#34;</span>, <span style="color:#e6db74">&#34;-T&#34;</span>, <span style="color:#e6db74">&#34;log_with_files&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">revset-aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;recent_work&#39;</span> = <span style="color:#e6db74">&#39;ancestors(visible_heads(), 3) &amp; mutable()&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">template-aliases</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">log_with_files</span> = <span style="color:#e6db74">&#39;&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">if(root,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  format_root_commit(self),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  label(if(current_working_copy, &#34;working_copy&#34;),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    concat(
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      format_short_commit_header(self) ++ &#34;\n&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      separate(&#34; &#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        if(empty, label(&#34;empty&#34;, &#34;(empty)&#34;)),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        if(description,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          description.first_line(),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          label(if(empty, &#34;empty&#34;), description_placeholder),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        ),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      ) ++ &#34;\n&#34;,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      if(self.contained_in(&#34;recent_work&#34;), diff.summary()),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    ),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  )
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#39;</span>
</span></span></code></pre></div><p>Simply create a new template that copies the regular log template, while inserting a single conditional line that adds <code>diff.summary()</code> if the current commit is inside your new revset that covers the newest 3 commits. Easy. And now you know how to create the <code>jj ll</code> alias I promised to explain earlier.</p>
<p>Last, but definitely most stupid, I have ported my previous melding of <code>git branch</code> and <code>fzf</code> over to <code>jj</code>, as the subcommand <code>fuzzy_bookmark</code>, which I alias to <code>jj z</code> because it’s inspired by <code>zoxide</code>, the shell cd fuzzy matcher with the command <code>z</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-Toml" data-lang="Toml"><span style="display:flex;"><span><span style="color:#a6e22e">z</span> = [<span style="color:#e6db74">&#34;fuzzy_bookmark&#34;</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">za</span> = [<span style="color:#e6db74">&#34;bookmark&#34;</span>, <span style="color:#e6db74">&#34;list&#34;</span>, <span style="color:#e6db74">&#34;-a&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">fuzzy_bookmark</span> = [<span style="color:#e6db74">&#34;util&#34;</span>, <span style="color:#e6db74">&#34;exec&#34;</span>, <span style="color:#e6db74">&#34;--&#34;</span>, <span style="color:#e6db74">&#34;sh&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span>, <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">if [ &#34;</span><span style="color:#a6e22e">x</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#34; = &#34;</span><span style="color:#a6e22e">x</span><span style="color:#e6db74">&#34; ]; then
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  jj bookmark list
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">else
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  jj bookmark list -a -T &#39;separate(&#34;</span><span style="color:#960050;background-color:#1e0010">@</span><span style="color:#e6db74">&#34;, name, remote) ++ &#34;</span><span style="color:#960050;background-color:#1e0010">\</span><span style="color:#a6e22e">n</span><span style="color:#e6db74">&#34;&#39; 2&gt; /dev/null | sort | uniq | fzf -f &#34;</span><span style="color:#960050;background-color:#1e0010">$</span><span style="color:#ae81ff">1</span><span style="color:#e6db74">&#34; | head -n1 | xargs jj new
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">fi
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>]
</span></span></code></pre></div><p>This means you can <code>jj z</code> to see a list of local bookmarks, or <code>jj za</code> to see a list of all bookmarks including remote branches. Then, you can <code>jj z some</code> to do a fuzzy match on <code>something</code>, and execute <code>jj new something</code>. Jump to work on top of any named commit trivially by typing a few characters from its name.</p>
<h3 id="shell-prompt-tricks">shell prompt tricks</h3>
<p>I would love to also talk about all the stupid shell prompt tricks that I was forced to develop while setting up a zsh prompt that includes lots of useful jj information without slowing down prompt rendering, but I’m already out of time. Instead, I will refer you to my <a href="https://andre.arko.net/2025/06/20/a-jj-prompt-for-powerlevel10k/">blog post about a jj prompt for powerlevel10k</a>, and you can spend another 30 minutes going down that rabbit hole whenever you want.</p>
<h3 id="acknowledgements">acknowledgements</h3>
<p>Finally, I want to thank some people. Most of all, I want to thank everyone who has worked on creating jj, because it is so good.</p>
<p>I also want to thank everyone who has posted their configurations online, inspiring this talk. All the people whose names I was able to find in my notes include @martinvonz, @thoughtpolice, @pksunkara, @scott2000, @avamsi, @simonmichael, and @sunshowers. If I missed you, I am very sorry, and I am still very grateful that you posted your configuration.</p>
<p>Last, I need to thank @steveklabnik and @endsofthreads for being jj-pilled enough that I finally tried it out and ended up here as a result.</p>
<p>Thank you so much, to all of you.</p>
]]></content></entry><entry><title type="html">&lt;code>jj&lt;/code> part 1: what is it</title><link href="https://andre.arko.net/2025/09/28/jj-part-1-what-is-it/"/><updated>2025-09-28T14:07:48+09:00</updated><id>http://andre.arko.net/2025/09/28/jj-part-1-what-is-it/</id><content type="html"><![CDATA[<p>I’ve been working on a blog post about migrating to jj for two months now. Rather than finish my ultimate opus and smother all of you in eight thousand words, I finally realized I could ship incrementally and post as I finish each section. Here’s part 1: what is jj and how do I start using it?</p>
<h3 id="pls-i-just-want-to-use-jj-with-github">pls, I just want to use <code>jj</code> with GitHub</h3>
<p>Sure, you can do that. Convert an existing git repo with <code>jj git init --colocate</code> or clone a repo with <code>jj git clone</code>. Work in the repo like usual, but with no <code>add</code> needed, changes are staged automatically.</p>
<p>Commit with <code>jj commit</code>, mark what you want to push with <code>jj bookmark set NAME</code>, and then push it with <code>jj git push</code>. If you make any additional changes to that branch, update the branch tip by running <code>jj bookmark set NAME</code> again before each push.</p>
<p>Get changes from the remote with with <code>jj git fetch</code>. Set up a local copy of a remote branch with <code>jj bookmark track NAME@REMOTE</code>. Check out a branch with <code>jj new NAME</code>, and then loop back up to the start of the previous paragraph for commit and push. If you just want a slightly more detailed version of that, try <a href="https://matthewkmayer.github.io/blag/public/post/jj-for-my-workflow/">jj for my workflow</a>. That’s probably all you  need to get started, so good luck and have fun!</p>
<h3 id="jj-concepts"><code>jj</code> concepts</h3>
<p>Still here? Cool, let’s talk about how jj is different from git. There’s <a href="https://jj-vcs.github.io/jj/v0.13.0/git-comparison/">a list of differences from git</a> in the jj docs, but more than specific differences, I found it helpful to think of jj as like git, but every change in the repo creates a commit.</p>
<p>Edit a file? There’s a commit before the edit and after the edit. Run a jj command? There’s a commit before the command and after the command. Some really interesting effects fall out of storing every action as a commit, like no more staging, trivial undo, committed conflicts , and change IDs.</p>
<p>When edits are always immediately committed, you don’t need a staging area, or to manually move files into the staging area. It’s just a commit, and you can edit it by editing the files on disk directly.</p>
<p>Any jj command you run can be fully rewound, because any command creates a new operation commit in the op log. No matter how many commits you just revised in that rebase, you can perfectly restore their previous state by running  <code>jj undo</code>.</p>
<p>Any merge conflict is stored in the commit itself. A rebase conflict doesn’t stop the rebase—your rebase is already done, and now has some commits with conflicts inside them. Conflicts are simply commits with conflict markers, and you can fix them whenever you want. You can even rebase a branch full of conflicts without resolving them! They’re just commits. (Albeit with conflict markers inside them.)</p>
<p>Ironically, every action being a commit also leads away from commits: how do you talk about a commit both before and after you amended it? You add change IDs. Changes give you a single identifier for your intention, even as you need many commits to track how you amended, rebased, and then merged those changes.</p>
<p>Once you’ve internalized a model where every state is a commit, and change IDs stick around through amending commits, you can do some wild shenanigans that used to be quite hard with git. Five separate PRs open but you want to work with all of them at once? Easy. Have one commit that needs to be split into five different new commits across five branches? Also easy.</p>
<p>One other genius concept jj offers is <strong>revsets</strong>. In essence, revsets are a query language for selecting changes, based on name, message, metadata, parents, children, or several other options. Being able to select lists of changes easily is a huge improvement, especially for commands like log or rebase.</p>
<h3 id="further-reading">further reading</h3>
<p>For more about jj’s design, concepts, and why they are interesting, check out the blog posts <a href="https://reasonablypolymorphic.com/blog/jj-strategy/">jj strategy</a>, <a href="https://zerowidth.com/2025/what-ive-learned-from-jj/">What I’ve Learned From JJ</a>, <a href="https://v5.chriskrycho.com/essays/jj-init/">jj init</a>, and <a href="https://www.felesatra.moe/blog/2024/12/23/jj-is-great-for-the-wrong-reason">jj is great for the wrong reason</a>. For a quick reference you can refer to later, there’s a single page summary in the <a href="https://justinpombrio.net/src/jj-cheat-sheet.pdf">jj cheat sheet PDF</a>.</p>
<h3 id="next-time">next time</h3>
<p>Keep an eye out for the next part of this series in the next few days. We’ll talk about commands in jj, and exactly how they are both different and better than git commands.</p>
<p>Continue with <a href="/2025/10/02/jj-part-2-commands/"><code>jj</code> part 2: commands &amp; revsets</a>.</p>
<p>The full series also includes: <a href="/2025/10/12/jj-part-3-workflows/">part 3: workflows</a>, <a href="/2025/10/15/jj-part-4-configuration/">part 4: configuration</a></p>
]]></content></entry><entry><title type="html">Bundler belongs to the Ruby community</title><link href="https://andre.arko.net/2025/09/25/bundler-belongs-to-the-ruby-community/"/><updated>2025-09-25T14:16:51+09:00</updated><id>http://andre.arko.net/2025/09/25/bundler-belongs-to-the-ruby-community/</id><content type="html"><![CDATA[<p>I’ve spent 15 years of my life working on Bundler. When I introduce myself, people say &ldquo;oh, the Bundler guy?&rdquo;, and I am forced to agree.</p>
<p>I didn’t come up with the original idea for Bundler (that was Yehuda). I also didn’t work on the first six months worth of prototypes. That was all Carl and Yehuda together, back when &ldquo;Carlhuda&rdquo; was a super-prolific author of Ruby libraries, including most of the work to modularize Rails for version 3.</p>
<p>I joined the team at a pivotal moment, in February 2010, as the 0.9 prototype was starting to be re-written yet another time into the shape that would finally be released as 1.0. By the time Carl, Yehuda, and I released version 1.0 together in August 2010, we had fully established the structure and commands that Bundler 2.7.2 still uses today.</p>
<p>I gave my first conference talk about Bundler at Red Dirt Ruby in May 2010. Because they would be too busy with Rails 3 talks, Yehuda and Carl asked me to give the first RailsConf talk about Bundler, in June 2010.</p>
<p>As Carl and Yehuda drifted off to other projects, in 2011 and 2012 respectively, I took on a larger role, co-maintaining the project with Terence Lee, then on the Ruby platform team at Heroku. We shipped (and, embarrassingly, broke) many versions of Bundler on our way to the 1.1 release and its major speed improvements. We also gave several conference talks together, sharing what we had learned about Bundler, about gems, and about maintaining open source.</p>
<p>In 2013, I managed to convince the owner of <code>bundler.io</code> to sell me his domain, and rebuilt the website to host a separate copy of the documentation for every version of Bundler, ensuring even users on old versions could still access accurate documentation.</p>
<p>By the end of 2013, Terence had drifted away from the project as well, and I realized that everyone using Ruby was now one bus (or one lottery ticket) away from Bundler having no significant maintainers. During 2014, I made sure to settle any remaining ownership issues, including purchasing the rights to the Bundler logo, and began investigating various funding ideas. I tried specialized consulting, corporate sponsorships, and asking Ruby Central about sponsoring Bundler and RubyGems development work. Ruby Central declined, citing their desire to stay focused on conferences, but suggested that if I wanted to pursue something myself they would be happy to collaborate.</p>
<p>In 2015, I founded Ruby Together specifically to raise funds to pay the existing maintainers team of Bundler, RubyGems, and RubyGems.org. Over time, we were able to raise enough money to quietly but scrappily keep the entire RubyGems ecosystem maintained and functional. Ruby Together did not ever, at any point, demand any form of governance or control over the existing open source projects. Maintainers did their thing in the RubyGems and Bundler GitHub orgs, while Ruby Together staff and board members did their thing in the rubytogether GitHub org.</p>
<p>By 2021, when Ruby Central and Ruby Together were both interested in merging together, funds were harder to find. Ruby Together had a membership program. Ruby Central wanted a to have a membership program. The confusing split between “Ruby Central owns the AWS account, but Ruby Together pays all the devs” continued to be a problem.</p>
<p>We prepared <a href="merger-agreement.pdf">a merger agreement</a> (which you can read in full at the link), stating that Ruby Central’s new goal after the merger would be “paying maintainers to do the programming”. The agreement also states that Ruby Central will follow Ruby Together’s <a href="https://github.com/rubycentral/board/blob/38904a634d5993e244911087efdaa54811d2d516/VISION_MISSON_VALUES.md">Vision, Mission, and Values</a>, a document that is hosted in the rubycentral GitHub organization today. That document includes a very specific list of goals, including:</p>
<ul>
<li>Project users and maintainers are empowered to decide what’s best for their projects</li>
<li>Ruby open source developers are paid for their work</li>
<li>Give control to the community</li>
<li>Be accountable and transparent to the community</li>
<li>Establish a collaborative, positive space for projects</li>
<li>Have a clear and transparent funding process</li>
</ul>
<p>You can read much more in both the merger agreement and in the Mission, Vision, and Values document, but the fundamental goal for both the non-profit and the open source projects is clear: this is all for the Ruby community. Without the community, there is no point to this work, and there is no way it could ever have been done in the first place. Without the 354 individuals who contributed to Bundler and to RubyGems, I could never have become &ldquo;the Bundler guy&rdquo; in the first place.</p>
<p>In the last few weeks, Ruby Central has <a href="https://joel.drapper.me/p/rubygems-takeover/">suddenly asserted</a> that they alone own Bundler. That simply isn’t true. In order to defend the reputation of the team of maintainers who have given so much time and energy to the project, I have registered my existing trademark on the Bundler project.</p>
<p>Trademarks do not affect copyright, which stays with the original contributors unchanged. Trademarks do not affect license terms, which stay MIT and unchanged. Trademarks only impact one thing: who is allowed say that what they make is named &ldquo;Bundler&rdquo;. Ruby Central is welcome to the code, just like everyone else. They are not welcome to the project name that the Bundler maintainers have painstakingly created over the last 15 years.</p>
<p>While the trademark has been registered under my name as an individual, I will not keep it for myself, because the idea of Bundler belongs to the Ruby community. Once there is a Ruby organization that is accountable to the maintainers, and accountable to the community, with openly and democratically elected board members, I commit to transfer my trademark to that organization.</p>
<p>I will not license the trademark, and will instead transfer ownership entirely. Bundler should belong to the community, and I want to make sure that is true for as long as Bundler exists.</p>
]]></content></entry><entry><title type="html">Adventures in CPU contention</title><link href="https://andre.arko.net/2025/09/23/adventures-in-cpu-contention/"/><updated>2025-09-23T10:00:00-07:00</updated><id>http://andre.arko.net/2025/09/23/adventures-in-cpu-contention/</id><content type="html"><![CDATA[<p>Recently on this blog, I wrote about <a href="/2025/08/18/in-memory-filesystems-in-rust/">in-memory filesystems in Rust</a>, and concluded that I wasn&rsquo;t able to detect a difference between any form of in-memory filesystem and using a regular SSD on macOS. I also asked anyone who found a counterexample to please let me know.</p>
<p>Last week, <a href="https://davidbarsky.com">David Barsky</a> of <a href="https://ersc.io">ERSC</a> sent me an extremely compelling counter-example, and I spent several days running benchmarks to understand it better.</p>
<p>The top level summary is that the test suite for the <a href="http://github.com/jj-vcs/jj/">jj VCS</a> exhibits an absolutely huge difference between running on an SSD and running against a ramdisk. In my first reproduction attempt, I found the SSD took 239 seconds, while the ramdisk took just 37 seconds. That&rsquo;s bananas! How was that even possible?</p>
<p>What I discovered will amaze, distress, and astound you. Probably.</p>
<p>First, the context. The jj project recently shipped a change to <a href="https://github.com/jj-vcs/jj/pull/7375">always use <code>fdatasync()</code></a> when persisting a temporary file. My understanding is that this change was made to prevent certain kinds of bad data being written.</p>
<p>After adding more calls to <code>fdatasync()</code>, which is a variation of <code>fsync()</code>, contributors to jj noticed that the tests ran about the same speed on linux, but dramatically slower on macOS. This eventually produced a pull request <a href="https://github.com/jj-vcs/jj/pull/7493">suggesting a ramdisk for tests on macOS</a>, noting that it was much faster.</p>
<p>This situation intrigued me—how much faster was it? And why? At the highest level, there is an explanation that makes sense to me: testing <code>fdatasync()</code> causes a huge difference between physical disks and RAM, because it breaks through the filesystem cache in memory, and forces slow disk writes before returning.</p>
<p>But then I actually tested the suggested change on two different machines, and what I was seeing didn’t make any sense. I had access to two different Macs for testing: one M4 Max with 16 CPU cores, and one M3 Ultra with 32 CPU cores.</p>
<p>When I tried SSD vs RAM on 16 cores, the difference was huge. When I tried SSD vs RAM on 32 cores, the difference was… much smaller. That’s confusing. The <code>cargo nextest run</code> command will (by default) run one test binary on every core available on the machine. Why would running the tests on more cores make the tests slower?</p>
<p>Since it seemed like I was getting inconsistent results, I eventually used <code>hyperfine</code> to systematically run the entire test suite 10 times using 1 core, 2 cores, 3 cores, 4 cores, etc, all the way up to the full 32 cores in my M3 Ultra testing box.</p>
<p>The results I saw for using the SSD made sense, mostly. Adding more cores made the tests run faster… up to about 4 cores. Cores 5 to 32, on the other hand, don’t seem to do anything at all. From the outside, that makes it look like the APFS filesystem on the SSD has some kind of mutex or lock that really only allows 4 cores to actually run at the same time. Running similar tests on the M4 Max produced similar results—APFS on SSD seems to test faster up to about 4 cores, and then get stuck there no matter how many more cores you add.</p>
<p>Where things started to get weird was using tmpfs on a ramdisk. On the M4 Max things went roughly how you might expect, with each additional core decreasing the overall runtime but with diminishing returns. The full test suite on one core takes about 327 seconds, and with 16 cores takes about 37 seconds. 15 cores is just a hair slower at 38, and so on.</p>
<p>On the M3 Ultra, though, using a ramdisk and testing from 1 to 32 cores produced <em>worse</em> results for every core added beyond the 12th. I’ve <a href="https://gist.github.com/indirect/c3d911b093ecab55dc96ebaaef7b1adb">created a gist with raw benchmark output</a>, but you can see the summary chart below.</p>
<p><img src="jj-test-bench.png" alt="a chart showing SSD and ramdisk test suite times"></p>
<p>Whatever is going on with the jj test suite and the ramdisk creates so much contention that 32 cores will all run full out at 100% while taking 3x longer than 12 cores running at 100%.</p>
<p>That’s pretty wild! In the end, it doesn’t seem to be a story about in-memory filesystems exactly. Instead, it’s about some kind livelock contention between the running cores and some shared, limited resource. I’m not sure if that resource is memory itself, some shared CPU cache, the IO bus, or what. But it sure is dramatic.</p>
]]></content></entry><entry><title type="html">Goodbye, RubyGems</title><link href="https://andre.arko.net/2025/09/19/goodbye-rubygems/"/><updated>2025-09-19T00:54:56-07:00</updated><id>http://andre.arko.net/2025/09/19/goodbye-rubygems/</id><content type="html"><![CDATA[<p>As chronicled by my teammate Ellen, <a href="https://pup-e.com/goodbye-rubygems.pdf">the RubyGems team is no more</a>. I wish the best of luck to everyone taking on the herculean task of keeping package management functional and working for the entire Ruby community.</p>
<p>In the meantime, I&rsquo;m looking forward to spending my new free time focusing on projects that I&rsquo;m truly excited about, like <a href="https://rv.dev"><code>rv</code></a>. We&rsquo;d love to have your help as we work to build next-generation tools for Ruby.</p>
]]></content></entry></feed>