<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://tekin.co.uk/atom.xml" rel="self" type="application/atom+xml" /><link href="https://tekin.co.uk/" rel="alternate" type="text/html" /><updated>2026-07-28T21:15:11+00:00</updated><id>https://tekin.co.uk/atom.xml</id><title type="html">tekin.co.uk</title><subtitle>This is the personal blog of Tekin Süleyman, Manchester UK based Ruby on Rails developer available for hire.</subtitle><author><name>tekin</name></author><entry><title type="html">Overriding Rails’ default validation error message format</title><link href="https://tekin.co.uk/2026/07/overriding-the-default-rails-error-message-format" rel="alternate" type="text/html" title="Overriding Rails’ default validation error message format" /><published>2026-07-25T00:00:00+00:00</published><updated>2026-07-25T00:00:00+00:00</updated><id>https://tekin.co.uk/2026/07/overriding-the-default-rails-error-message-format</id><content type="html" xml:base="https://tekin.co.uk/2026/07/overriding-the-default-rails-error-message-format"><![CDATA[<p><em>This is a followup to my recent <a href="/2026/07/10-things-you-might-not-know-about-rails-i18n">lightning talk</a> and
<a href="/2026/07/ten-cool-things-you-might-not-know-about-rails-i18n">writeup</a> about i18n in Rails and how it can be useful
even when we’re not translating our applications.</em></p>

<p>In the talk and writeup I describe how we can <a href="/2026/07/ten-cool-things-you-might-not-know-about-rails-i18n#thing-3-overriding-the-error-message-format-itself">change the Active Record error message
format</a>
from the default of <code class="language-plaintext highlighter-rouge">"%{attribute} %{message}"</code> and drop the attribute prefix, giving us more flexibility in how we
phrase our error messages. The downside to this is that it results in the <a href="https://github.com/rails/rails/blob/main/activemodel/lib/active_model/locale/en.yml">default Rails error
messages</a> being output as
incomplete sentences. It’s since occurred to me that it’s possible to avoid this by replicating the default
validation error messages in our app’s locale file with the attribute prefix <em>as part of the message</em>, rather than the
format!</p>

<p>So we end up with a locale file that looks something like this:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml
en:
  errors:
    # Our overridden message format without the attribute prefix
    format: "%{message}"

    # The Rails default messages, but with the attribute prefix included
    messages:
      inclusion: "%{attribute} is not included in the list"
      exclusion: "%{attribute} is reserved"
      invalid: "%{attribute} is invalid"
      confirmation: "%{attribute} doesn't match %{attribute}"
      accepted: "%{attribute} must be accepted"
      empty: "%{attribute} can't be empty"
      blank: "%{attribute} can't be blank"
      present: "%{attribute} must be blank"
      too_long:
        one: "%{attribute} is too long (maximum is 1 character)"
        other: "%{attribute} is too long (maximum is %{count} characters)"
      password_too_long: "%{attribute} is too long"
      too_short:
        one: "%{attribute} is too short (minimum is 1 character)"
        other: "%{attribute} is too short (minimum is %{count} characters)"
      wrong_length:
        one: "%{attribute} is the wrong length (should be 1 character)"
        other: "%{attribute} is the wrong length (should be %{count} characters)"
      not_a_number: "%{attribute} is not a number"
      not_an_integer: "%{attribute} must be an integer"
      greater_than: "%{attribute} must be greater than %{count}"
      greater_than_or_equal_to: "%{attribute} must be greater than or equal to %{count}"
      equal_to: "%{attribute} must be equal to %{count}"
      less_than: "%{attribute} must be less than %{count}"
      less_than_or_equal_to: "%{attribute} must be less than or equal to %{count}"
      other_than: "%{attribute} must be other than %{count}"
      in: "%{attribute} must be in %{count}"
      odd: "%{attribute} must be odd"
      even: "%{attribute} must be even"

      # Our bespoke error message definitions
      models:
        album:
          title:
            blank: "Enter a title for the album"
</code></pre>

<p>With that in place, we’re free to define bespoke validation errors for specific attributes and models without the
attribute prefix, whilst also preserving the default error messages as a fallback.</p>]]></content><author><name>Tekin Süleyman</name></author><category term="Ruby &amp; Rails" /><summary type="html"><![CDATA[A follow-up to my recent Rails i18n post that describes a way to override the validation error message format without leaving the default error messages as incomplete sentences.]]></summary></entry><entry><title type="html">10 Things You Might Not Know About Rails i18n</title><link href="https://tekin.co.uk/2026/07/ten-cool-things-you-might-not-know-about-rails-i18n" rel="alternate" type="text/html" title="10 Things You Might Not Know About Rails i18n" /><published>2026-07-24T00:00:00+00:00</published><updated>2026-07-24T00:00:00+00:00</updated><id>https://tekin.co.uk/2026/07/ten-cool-things-you-might-not-know-about-rails-i18n</id><content type="html" xml:base="https://tekin.co.uk/2026/07/ten-cool-things-you-might-not-know-about-rails-i18n"><![CDATA[<p><em>This is mostly a transcript of my <a href="/2026/07/10-things-you-might-not-know-about-rails-i18n">recent lightning talk on i18n in
Rails</a>, go watch that if you prefer your content in video form.
Otherwise read on to find out why I believe understanding i18n in Rails can be useful to you, a Rails developer, even if
you’re not translating your applications into other languages.</em></p>

<p>I’m going to share ten things I think are interesting in i18n, and explain how we can take advantage of them in our
single language app code. But first a brief primer on i18n…</p>

<h2 id="what-actually-is-i18n">What actually is i18n?</h2>

<p>i18n is shorthand for <em>Internationalisation</em> When we’re talking about i18n, it’s useful to understand the difference
between <em>Internationalisation</em> and <em>Localisation</em>:</p>

<ul>
  <li><strong>Internationalisation</strong> — the process of abstracting content and other locale-specific things away from application
code itself</li>
  <li><strong>Localisation</strong> — the process of adapting software to support different languages and regions (maybe using i18n)</li>
</ul>

<p>The take away here is that although i18n is primarily a framework for adapting applications to support other languages,
the core idea of separating content from applications code has useful properties that we can take advantage of it to
improve our Rails applications, even if they only support a single language.</p>

<h2 id="i18n-in-rails">i18n in Rails</h2>

<p>Rails ships with the <a href="https://github.com/ruby-i18n/i18n">i18n gem</a>, and there are two main components:</p>

<ul>
  <li>locale files where textual content is organised and stored per-language (normally as YAML)</li>
  <li>two helper methods:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">I18n.translate</code> for outputting text content from the locale files</li>
      <li><code class="language-plaintext highlighter-rouge">I18n.localize</code> for localising dates and times</li>
    </ul>
  </li>
</ul>

<p>Rails aliases these helpers in views and helper modules to the convenient shorthands of <code class="language-plaintext highlighter-rouge">t</code> and <code class="language-plaintext highlighter-rouge">l</code> respectively.</p>

<p>So the most basic usage of i18n would look something like this:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml
en:
  home_title: Hello world
  home_body: How are you doing?
</code></pre>

<pre><code class="language-erb">
&lt;!-- app/views/home/index.html.erb —&gt;

&lt;h1&gt;&lt;%= t 'home_title' %&gt;&lt;/h1&gt;
&lt;p&gt;&lt;%= t 'home_body' %&gt;&lt;/p&gt;

&lt;time datetime=&quot;&lt;%= @published_at.iso8601 %&gt;&quot;&gt;
  &lt;%= l @published_at %&gt;
&lt;/time&gt;
</code></pre>

<p>Primer over! On to the ten things…</p>

<h2 id="thing-1-you-are-already-using-it">Thing 1: You are already using it</h2>

<p>Even if you’re not localising your app you’re almost certainly still using i18n. That’s because it’s baked into the very
fabric of the framework.</p>

<p>For example Active Record uses i18n under the hood to generate error messages for its validations. It does so using a
YAML file that <a href="https://github.com/rails/rails/blob/main/activemodel/lib/active_model/locale/en.yml">ship with Active Model</a>:</p>

<pre><code class="language-yaml">
# rails/activemodel/lib/active_model/locale/en.yml
en:
  errors:
    # The default format to use in full error messages.
    format: "%{attribute} %{message}"

    messages:
      model_invalid: "Validation failed: %{errors}"
      inclusion: "is not included in the list"
      exclusion: "is reserved"
      invalid: "is invalid"
      confirmation: "doesn't match %{attribute}"
      accepted: "must be accepted"
      empty: "can't be empty"
      blank: "can't be blank"
      ...
</code></pre>

<p>Whenever you see something like “Title cannot be blank”, this is the i18n plumbing that was used to construct it.</p>

<h2 id="thing-2-default-error-message-can-be-overridden">Thing 2: Default error message can be overridden</h2>

<p>Rails i18n makes it easy to override these default errors messages. Just replicate the same key structure in your
application’s locale file with your chosen phrasing:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml
en:
  errors:
    messages:
      # Overrides the default Rails message for presence validations
      blank: "must be provided"
</code></pre>

<p>Rails will check your application’s locale file first, falling back to the defaults only if the message isn’t defined
there.</p>

<h3 id="model-and-attribute-specific-overrides">Model and attribute specific overrides</h3>

<p>As well as overriding this global phrasing, it’s also possible to override the error messages for a given attribute
name:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml
en:
  errors:
    attributes:
       title: # phrasing will apply to any attribute called "title”
           blank: "must be provided”
</code></pre>

<p>Or you can get even more granular and override specific attributes on a given model. For example if we have the
following model and validation:</p>

<pre><code class="language-ruby">
class Album &lt; ApplicationRecord
  validates :category, presence: true
end
</code></pre>

<p>It’s possible to set a bespoke error message for this specific model and attribute:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml
en:
  activerecord:
    errors:
      models:
        album: # model name
          category: # attribute name
            blank: "must be selected"
</code></pre>

<h2 id="thing-3-overriding-the-error-message-format-itself">Thing 3: Overriding the error message format itself</h2>

<p>As well as overriding individual validation messages globally and for specific attributes, it’s also possible to change
the actual format of error messages. By default, errors are constructed with the familiar format of
<code class="language-plaintext highlighter-rouge">"%{attribute} %{message}"</code>. This is defined in the same YAML that ships with Active Model alongside the default error
messages:</p>

<pre><code class="language-yaml">
# rails/activemodel/lib/active_model/locale/en.yml
en:
  errors:
    # The default format to use in full error messages.
    format: "%{attribute} %{message}"
</code></pre>

<p>This format works as a reasonable default to ship with the framework as it gives us mostly coherent error messages out
of the box without us having to do anything, but it lacks fidelity and can result in some clunky and unfriendly sounding
messages. For example, take this validation:</p>

<pre><code class="language-ruby">
class Album
  validates :number_of_tracks, comparison: { greater_than: 0 }
end
</code></pre>

<p>If we go with the default format we end up with <code class="language-plaintext highlighter-rouge">"Number of tracks must be greater than 0"</code>. Which is fine, but we
can definitely do better.</p>

<p>My personal preference on the apps I work with is to remove the attribute prefix entirely:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml
en:
  errors:
    # Remove the attribute name from the default error message format:
    format: "%{message}"
</code></pre>

<p>This frees me from having to make my error messages work with attribute’s name upfront, allowing me to write more
user-friendly and coherent error messages:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml
  activerecord:
    errors:
      models:
        album:
          number_of_tracks:
            greater_than: "You must upload at least one track"
</code></pre>

<p>Now changing the default format does come at a cost: without the attribute name prefix, the default messages that ship
with Rails no longer form complete sentences. This forces me to write bespoke error messages for each validation I add
to my app:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml
en:
  activerecord:
    errors:
      models:
        album:
          title:
            blank: "Enter a title for the album"
</code></pre>

<p>Personally I’m happy to pay this cost for the benefit of improving the user experience. So whilst you may not want to do
this on an existing app with thousands of error messages to backfill, it might be something you consider next time you
run <code class="language-plaintext highlighter-rouge">rails new</code> and are starting from a clean slate.</p>

<p><em><strong>Update</strong>: There’s actually a fairly neat way to avoid this cost, check out my <a href="/2026/07/overriding-the-default-rails-error-message-format">follow-up post on
how</a>.</em></p>

<h2 id="thing-4-using-i18-for-your-custom-validations">Thing 4: Using i18 for your custom validations</h2>

<p>As well using i18n to manage and override the error messages for the built-in validations, you can also use it to
manage the messages for your custom validations.</p>

<p>Let’s look at a custom model validation:</p>

<pre><code class="language-ruby">
class DirectDebitMandate &lt; ApplicationRecord
  validate :bank_account_must_pass_modulus_check

  private

  def bank_account_must_pass_modulus_check
    if modulus_check_fail?
      errors.add(:bank_account_number, "The account number you entered is not valid")
    end
  end
end
</code></pre>

<p>Here the error message is inlined as part of the validation code. Instead of doing this, we can pass in a symbol
identifying the error, and define the corresponding message in the locale file using the same key structure as the
built-in validations:</p>

<pre><code class="language-ruby">
class DirectDebitMandate &lt; ApplicationRecord
  validate :bank_account_must_pass_modulus_check

  private

  def bank_account_must_pass_modulus_check
    if modulus_check_fail?
      errors.add(:bank_account_number, :failed_modulus)
    end
  end
end
</code></pre>

<pre><code class="language-yaml">
# app/config/locales/en.yml
en:
  activerecord:
    errors:
      models:
        direct_debit_mandate:
          bank_account_number:
            failed_modulus: "The account number you entered is not valid"
</code></pre>

<p>This results in more concise and compact model code, but also equally as nice is that it removes a presentational
concern — the specific phrasing of the message — our the business logic.</p>

<p>It’s also possible to interpolate both the attribute name and the value of the attribute directly into the error
message, which is useful for custom validations that are used across multiple attributes and/or models:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml
en:
  activerecord:
    errors:
      messages:
        failed_modulus: "The %{attribute} '%{value}' is not valid"
</code></pre>

<h2 id="thing-5-form-labels">Thing 5: Form labels</h2>

<p>Another place where Rails makes use of i18n under the hood is the form helpers: specifically, the label helper methods.
By default the label helper methods humanize the attribute name to arrive at the label text:</p>

<pre><code class="language-erb">
&lt;%= form.label :address_county %&gt; # label text will be "Address county"
</code></pre>

<p>One way to override the label text is to pass in a string to the helper call like so:</p>

<pre><code class="language-erb">
&lt;%= form.label :address_county, "County (optional)" %&gt;
</code></pre>

<p>I think a better way is to define label overrides in the application’s locale file:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml

helpers:
  label:
    order: # the model name
      address_county: 'County (optional)'
</code></pre>

<p>Not only does this keep the view code more concise and less noisy, but it has the added advantage of defining this label
override in one place: now any other forms that display the same label will automatically get the overridden text
without you having to manually copy the literal string to every template.</p>

<p>And as with form errors, label overrides can be defined per-model as well as globally:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml

helpers:
  label:
    address_county: 'County (optional)' # applies to all models with attributes called address_county
</code></pre>

<h2 id="thing-6-built-in-pluralisation">Thing 6: Built-in pluralisation</h2>

<p>i18n has sophisticated support for <a href="https://guides.rubyonrails.org/i18n.html#pluralization">pluralisation</a> that we can
leverage to simplify our app code. As an example, we can display the stock availability of a product by calling out to
i18n like so:</p>

<pre><code class="language-erb">
&lt;!-- app/views/albums/show.html.erb —&gt;

&lt;h1&gt;&lt;%= @album.name %&gt;&lt;/h1&gt;

&lt;div class="availability"&gt;
  &lt;%= t 'album.availability', count: @album.stock.count %&gt;
&lt;/div&gt;
</code></pre>

<p>And then define the phrasing based on the number of items in stock in our locale file:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml
en:
  album:
    availability:
      zero: "Out of stock"
      one: "Just one album left in stock!"
      other: "%{count} albums in stock"
</code></pre>

<p>Much neater than a bunch of conditional logic in a helper method!</p>

<h2 id="thing-7-action-mailer-support">Thing 7: Action Mailer support</h2>

<p>As well as being available in Rails controllers and views, i18n is also integrated into Action Mailer, allowing you to
organise content for mailer templates in local files. Mailer get one extra feature of controllers: the ability to
specify email subject lines in locale files:</p>

<pre><code class="language-yaml">
# app/config/locales/en.yml

en:
  membership_mailer: # mailer name
    submitted: # mailer action
      subject: "Welcome to to the union!"
    dropped:
      subject: "Want to complete your membership application?"
</code></pre>

<p>Again, this enables us to move another presentational concern out of our application code and put it somewhere more
appropriate.</p>

<p>There is also a meta benefit to defining all these bits of content (mailer subject lines, form labels, validation error
messages) in an application’s locale file: it puts them all in one centralised place, making them easier to change,
check for consistency and generally manage. It also makes the content more accessible to non-developers such as
designers and product folks.</p>

<h2 id="thing-8-time-and-date-formatting">Thing 8: Time and date formatting</h2>

<p>Right at the top I mentioned the <code class="language-plaintext highlighter-rouge">I18n.localize</code> method that is used to output localised times and dates. The way this
works is by defining our own time and date formats in our locale file using the same
<a href="https://docs.ruby-lang.org/en/master/language/strftime_formatting_rdoc.html">format specification</a> as Ruby’s
<code class="language-plaintext highlighter-rouge">strftime</code>:</p>

<pre><code class="language-yaml">
# config/locales/en.yml
en:
  date:
    # ...
    formats:
      short: "%d/%m/%Y"
</code></pre>

<p>Here we’ve defined a short date format that will be familiar to my UK-based audience: day/month/year. To render a date
using this format we simply pass in the format identifier to <code class="language-plaintext highlighter-rouge">I18n.localize</code> (or its <code class="language-plaintext highlighter-rouge">l</code> alias):</p>

<pre><code class="language-erb">
&lt;%= I18n.localize @album.released_on, format: short %&gt;
</code></pre>

<p>or with the <code class="language-plaintext highlighter-rouge">l</code> shorthand:</p>

<pre><code class="language-erb">
&lt;%= l @album.released_on, format: short %&gt;
</code></pre>

<h3 id="uk-vs-us-dates">UK vs US dates…</h3>

<p>Now my American readers may be confused by this short format, as across the pond the preferred way to display dates is
actually month/day/year. So whilst you may not need to localise your entire application, you might want to consider
localising your date rendering, especially if you have customers both sides of the Atlantic.</p>

<p>To do this you would define a US region specific English locale file:</p>

<pre><code class="language-yaml">
# config/locales/en-US.yml
en-US:
  date:
    # ...
    formats:
      short: "%m/%d/%Y"
</code></pre>

<p>Then update your controller code to automatically switch locale so the user gets dates formatted appropriately for them.</p>

<p>There are many ways to do actually do the locale switching, but at a high-level, you identify the user’s specific locale
and then make sure your controller actions are executed with that locale. Here’s an example of doing that by sniffing
the user’s locale from their browser headers:</p>

<pre><code class="language-ruby">
class ApplicationController &lt; ActionController::Base
  around_action :switch_locale

  private

  def switch_locale(&amp;action)
    browser_locale = extract_locale(request.env['HTTP_ACCEPT_LANGUAGE'])

    I18n.with_locale(browser_locale, &amp;action)
  end
end
</code></pre>

<p>(The exact mechanics of the <code class="language-plaintext highlighter-rouge">extract_locale</code> method is left as a separate exercise, but you could do worse than use the
<a href="https://github.com/iain/http_accept_language/">http_accept_language</a> library)</p>

<h2 id="thing-9-you-can-define-locale-files-in-ruby">Thing 9: You can define locale files in Ruby</h2>

<p>Little known fact: the i18n library supports locale files defined using Ruby! Now why would you do this, other than a
burning hatred for YAML? Well one reason is that Ruby-based locale files allows you to make use of procs to dynamically
generate content.</p>

<p>Below is an excerpt from a Ruby-based locale file from one of my applications. It uses a proc to format dates as
academic years, changing the output depending on which side of September the date is:</p>

<pre><code class="language-ruby">
# config/locales/en.rb

{
  en: {
    date: {
      formats: {
        academic_year: -&gt;(date, _) {
          year = date.year
          date.month &gt;= 9 ? "#{year}/#{year + 1}" : "#{year - 1}/#{year}"
        }
      }
    }
  }
}
</code></pre>

<p>Now this could just as easily be achieved using a standard Rails helper method. But for me I like the consistency and
clarity of using a single mechanism (<code class="language-plaintext highlighter-rouge">localize</code>) for formatting dates across my application code, rather than with a
mishmash of calls to bespoke helpers, <code class="language-plaintext highlighter-rouge">DateTime#to_fs</code>, <code class="language-plaintext highlighter-rouge">strftime</code>, etc.</p>

<h2 id="thing-10-">Thing 10: …</h2>

<p>This is the part of the lightning talk where I make a meta point about what I’ve covered. I won’t write that up here,
but if you’re interested go watch <a href="/2026/07/10-things-you-might-not-know-about-rails-i18n">the video</a> from minute 9.</p>]]></content><author><name>Tekin Süleyman</name></author><category term="Ruby &amp; Rails" /><category term="popular" /><summary type="html"><![CDATA[The i18n framework in Rails is full of useful abstractions that can help make your Rails code more maintainable, even when you're not actually translating it.]]></summary></entry><entry><title type="html">10 Things You Might Not Know About Rails i18n</title><link href="https://tekin.co.uk/2026/07/10-things-you-might-not-know-about-rails-i18n" rel="alternate" type="text/html" title="10 Things You Might Not Know About Rails i18n" /><published>2026-07-13T00:00:00+00:00</published><updated>2026-07-13T00:00:00+00:00</updated><id>https://tekin.co.uk/2026/07/10-things-you-might-not-know-about-rails-i18n</id><content type="html" xml:base="https://tekin.co.uk/2026/07/10-things-you-might-not-know-about-rails-i18n"><![CDATA[<meta property="og:video" content="https://www.youtube-nocookie.com/embed/X1u8ZWCSuA0?start=23" />

<div class="video-container">
  <iframe title="10 Things You Might Not Know About Rails i18n" width="560" height="315" src="https://www.youtube-nocookie.com/embed/X1u8ZWCSuA0?start=76" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
</div>

<p>A lightning talk exploring i18n in Rails and how it can be helpful to us even when we’re not translating our applications into other languages.</p>

<p>I gave this talk at <a href="https://brightonruby.com/2026/10-cool-things-internationalisation-tekin-suleyman/">Brighton Ruby in 2026</a>.</p>

<p><a href="https://speakerdeck.com/tekin/10-things-you-might-not-know-about-rails-i18n">Slides</a></p>]]></content><author><name>Tekin Süleyman</name></author><category term="Ruby &amp; Rails" /><category term="speaking" /><summary type="html"><![CDATA[A talk exploring i18n in Rails, and how it can be useful even when we're not translating our applications into other languages. I gave this talk at Brighton Ruby in 2026.]]></summary></entry><entry><title type="html">The Ruby community has a DHH problem</title><link href="https://tekin.co.uk/2025/09/the-ruby-community-has-a-dhh-problem" rel="alternate" type="text/html" title="The Ruby community has a DHH problem" /><published>2025-09-21T00:00:00+00:00</published><updated>2025-09-21T00:00:00+00:00</updated><id>https://tekin.co.uk/2025/09/the-ruby-community-has-a-dhh-problem</id><content type="html" xml:base="https://tekin.co.uk/2025/09/the-ruby-community-has-a-dhh-problem"><![CDATA[<p><a href="https://davidcel.is">David Celis</a> recently published a <a href="https://davidcel.is/articles/rails-needs-new-governance">thoughtful
piece</a> on Rails governance in response to the latest troubling
<a href="https://world.hey.com/dhh/as-i-remember-london-e7d38e64">blog post</a> from DHH, the creator of
Rails. Like David, I’ve also been troubled by DHH’s recent output and the harm it is causing to the Ruby community.</p>

<p>I think it’s worth taking a moment to analyse DHH’s post in more detail and make it clear exactly why it’s so
problematic.</p>

<p>In his post, DHH complains that London is no longer a city he wants to live in because it is now only a third “native
Brit”. His use of “native Brit” is as a proxy for “White British”. The implication is clear: if you are not White, you
are not British.</p>

<p><em>(Update: for the avoidance of doubt that David is talking about whiteness here, when he makes his claim that London is
only a third “native Brit” he references <a href="https://en.wikipedia.org/wiki/Ethnic_groups_in_London">this Wikipedia post on London ethnicity
statistics</a> where the most recent census data shows 36.8% of
individuals identifying as White British, excluding all those that identify as either Asian or Black British from his
made up  “native Brit” classification.)</em></p>

<p>In the same post he praises Tommy Robinson (actual name Stephen Christopher Yaxley-Lennon), a right-wing agitator with
several <a href="https://en.wikipedia.org/wiki/Tommy_Robinson#Criminal_offences">convictions</a> for violent offences and a long
history of association with far-right groups such as the <a href="https://en.wikipedia.org/wiki/English_Defence_League">English Defence
League</a> and the <a href="https://en.wikipedia.org/wiki/British_National_Party">British Nationalist
Party</a>. He then goes on to describe those that attended last
weekend’s far-right rally in London as “perfectly normal, peaceful Brits” protesting against the
“demographic nightmare” that has enveloped London, despite the <a href="https://www.bbc.co.uk/news/articles/cwydezxl0xlo">violence and disorder they caused</a>.</p>

<p>To all of that he adds a dash of Islamophobia, citing “Pakistani rape gangs” as one of the reasons for the unrest,
repeating a weaponised trope borne from a long since <a href="https://policinginsight.com/feature/analysis/when-bad-evidence-is-worse-than-no-evidence-quilliams-grooming-gangs-report-and-its-legacy/">discredited
report</a>
from the Quilliam Foundation, an organisation with ties to both the the <a href="https://medium.com/insurge-intelligence/the-quilliam-foundation-is-financed-by-tea-party-conservatives-investigated-by-sam-harris-1e43d54f0bee">US Tea
Party</a>,
and <a href="https://www.theguardian.com/uk-news/2013/oct/12/tommy-robinson-quilliam-foundation-questions-motivation">Tommy
Robinson</a> himself. A
trope that exists despite the fact that the <a href="https://novaramedia.com/2019/06/12/child-sexual-exploitation-asian-grooming-gangs-and-the-left/">overwhelming majority of convicted child sex offenders are white
men</a>, with Asian men
in fact under-represented.</p>

<p>According to DHH, there is nothing racist or xenophobic in saying “Britain primarily a united kingdom for the Brits”,
which again let’s be absolutely clear; by “Brits” he means White people.</p>

<h2 id="im-tired">I’m tired</h2>

<p>As a non-white British citizen born and raised in London, I can’t explain just how painful it is to hear this sort of
toxic rhetoric being promoted by one of the most prominent and visible leaders of the Ruby community. I experienced my
fair share of racism growing up
in London in the eighties and nineties from the very people that DHH is lionising in his post. White men who questioned my right to
exist. Places and spaces where I didn’t feel welcome or safe because of my ethnic origin. And despite the recent rise
in far-right politics, for the most part things are much better today. Anyone who has spent even the smallest amount of time
in the great city of London knows that it is precisely its rich multiculturalism that makes it great. The migration issue in
Britain is <a href="https://bsky.app/profile/robjimfleming.bsky.social/post/3lzbp5gcbsc2m">complicated</a> and
<a href="https://migrationobservatory.ox.ac.uk/resources/briefings/uk-public-opinion-toward-immigration-overall-attitudes-and-level-of-concern/">multi-layered</a>
and DHH wades into it with all the subtlety and nuance of a bull in china shop, sharing the same <a href="https://www.youtube.com/watch?v=tKEsyIuTrO8">old and tired xenophobic tropes</a>.</p>

<p>Unfortunately it isn’t just migrants and non-white people that have come under fire in DHH’s recent writing and social
media posts. There have been multiple examples of <a href="https://world.hey.com/dhh/bad-therapy-08849dc9">anti-trans rhetoric</a>.
There was the post where he described an ad featuring a plus-sized Black women as <a href="https://world.hey.com/dhh/the-beauty-of-ideals-b3dccf72">“grotesque”</a>
and <a href="https://world.hey.com/dhh/you-expect-principles-but-should-wish-for-none-531988ec">celebrated</a> the ads being
replaced with ones featuring “blond babies” (what is it about the baby’s blondness that is relevant I wonder?). There
have been the posts veering into puritanical
<a href="https://world.hey.com/dhh/the-parental-dead-end-of-consent-morality-e4e8a8ee">Pronatalism</a>,
another favourite subject of the nationalist right. Whether it’s his direct intention or not, there has been a
consistent theme of othering and stigmatising in his recent writing. The message is clear: if you are trans, a migrant,
black, overweight, childless, have <a href="https://world.hey.com/dhh/cold-reading-an-adhd-affliction-44163793">ADHD</a>, then in
DHH’s view you are somehow inferior. Instead of raising up and supporting the marginalised and vulnerable, DHH choses
to exclude and punch down. This is not something the Ruby community can continue to simply ignore.</p>

<h2 id="i-️-ruby">I ❤️ Ruby</h2>

<p>I love the Ruby community dearly. It’s a community that has given me much joy, and a successful career doing work I
love. It’s also a community I’ve done my fair share to help grow and prosper. Through running my <a href="https://nwrug.org">local Ruby
meetup</a>, <a href="https://tekin.co.uk/speaking/">speaking at conferences</a>, organising <a href="https://railsgirls.com/manchester">Rails Girls
workshops</a>, helping multiple people enter the industry through mentorship, and contributing to
open source, including <a href="https://contributors.rubyonrails.org/contributors/tekin-suleyman/commits">Rails itself</a>. And in
the over fifteen years I’ve been part of the community I’ve met many kind, thoughtful and compassionate people, few of
whom I would ever imagine sharing DHH’s increasingly reactionary and toxic views. But from the safety of his blue-tick
echo chamber, and despite many in the community voicing their concern and disgust, DHH is able to spin a narrative that
any opposition to his views are nothing more than noise from <a href="https://nitter.net/dhh/status/1969362639500324985#m">“radical, violence-condoning nut jobs”</a>.</p>

<h2 id="power-and-the-complicity-of-silence">Power and the complicity of silence</h2>

<p>My sense is that since the great <a href="https://www.theverge.com/2021/4/27/22406673/basecamp-political-speech-policy-controversy">Basecamp
implosion</a>, rather than
reflect on how he and Jason may have been at fault for what happened, DHH found
solace in ideas that allowed him to frame himself as victim. DEI and “wokeness” were the actual problem, not him.
The recent state of US politics has only further entrenched him in these ideas and emboldening him to fully mask-off.
All that is to say I don’t imagine DHH is likely to change. Too much of his self image is now tied up in the misguided
moral high ground of these right-wing ideals. So what can be done?</p>

<p>DHH holds an incredible amount of power. He has the trademarks to Rails, and sits as the chair of the Rails
Foundation. He is also a board member at Shopify, a company that contributes a huge amount to the Ruby ecosystem, both
financially, and through the incredible work of the Ruby and Rails infrastructure team. Of the twelve Rails Core
members, almost half are directly employed by either Basecamp or Shopify. DHH and Basecamp drives much of the
direction for new features that make it into Rails. Unfortunately DHH isn’t going anywhere.</p>

<p>At the same time, if we want the Ruby community to grown and thrive and for the next generation of Rubyists to
feel welcome, we cannot afford to let the damage he is causing by sharing his harmful and toxic ideas go unchecked. We
need many more voices to speak up and make it abundantly clear that his views are not widely held. Most importantly,
DHH needs to hear directly from the other leaders in our community that his behaviour is not OK and is causing
serious harm. That means the <a href="https://rubycentral.org/about/#directors">leadership at Ruby Central</a>.
That means the members of <a href="https://rubyonrails.org/community#core">Rails Core</a>. That means the many podcasters and
conference organisers that have a platform and voice that the community listens to. That means Matz himself.</p>

<p>Silence and appeasement is only emboldening DHH to ratchet up the toxicity and allowing the damage to continue.
Perhaps if enough of those in positions of power call him out on his bullshit he’ll at at least follow his own
advice and <a href="https://world.hey.com/dhh/make-politics-private-again-9b47aaaf">keep his politics to himself</a>. More
importantly though, it will send a clear message to the wider Ruby community (and those who may be considering joining
it) that the majority does not stand with DHH and his toxic views.</p>

<h2 id="update-9-october-2025">Update (9 October 2025)</h2>

<p>Since I wrote this post others have also written on the topic. Here’s a selection in case you need further convincing:</p>

<ul>
  <li><a href="https://jakelazaroff.com/words/dhh-is-way-worse-than-i-thought/">DHH Is Way Worse Than I Thought</a></li>
  <li><a href="https://christianheilmann.com/2025/09/25/as-i-remember-london/">As I remember London</a></li>
  <li><a href="https://paulbjensen.co.uk/2025/09/17/on-dhhs-as-i-remember-london.html">On DHH’s “As I Remember London”</a></li>
  <li><a href="https://victorwynne.com/dhh/">Ruby deserves better leadership than DHH</a></li>
  <li><a href="https://johan.hal.se/wrote/2025/09/26/david-please-stop-posting/">David, please stop posting</a></li>
</ul>

<p>And of course if you haven’t watched it already, you really should watch <a href="https://tomstu.art/the-dhh-problem">Tom’s five minute talk from
2014</a>. As relevant today as it was then.</p>]]></content><author><name>Tekin Süleyman</name></author><category term="Ruby &amp; Rails" /><category term="popular" /><summary type="html"><![CDATA[The Ruby community can no longer afford to stand silent in the face of DHH and his toxic ideas.]]></summary></entry><entry><title type="html">Yearnotes 2024</title><link href="https://tekin.co.uk/2025/01/yearnotes-2024" rel="alternate" type="text/html" title="Yearnotes 2024" /><published>2025-01-06T00:00:00+00:00</published><updated>2025-01-06T00:00:00+00:00</updated><id>https://tekin.co.uk/2025/01/yearnotes-2024</id><content type="html" xml:base="https://tekin.co.uk/2025/01/yearnotes-2024"><![CDATA[<p>After a four year absence it’s time to get back on the yearnotes train!</p>

<h2 id="work">Work</h2>

<h3 id="join-together">Join Together</h3>

<p>2024 was a good year for <a href="https://jointogether.online">Join Together</a>. Our main focus was a chunky project for the <a href="https://neu.org.uk">National Education Union</a> (NEU). As well as replacing their online join process, we also built them a bespoke service for upgrading existing student members to full membership after graduation.</p>

<p>The NEU are one of the largest (and more tech-savvy) unions in the UK, so it was a pretty big deal for us to win the project. And despite a fair amount of complexity and esoteric requirements, we totally nailed it, leaving the folks at the NEU over the moon with what we delivered.</p>

<p>We also shipped projects and updates of varying sizes for a good number of our existing union clients. It’s become clear that updates and ongoing development with existing clients will make up a healthy chunk of our revenue, which is helpful, as bringing new unions onboard can be a long and arduous process (it took a whole year from first contact to signed contract with NEU).</p>

<h3 id="side-gig">Side gig</h3>

<p>By the tail end of the year Join Together’s projects were wrapping up and a previous client of mine got in touch asking if I’d be available for a short contract. They needed urgent help shipping some major updates in a short space of time. With no new Join Together projects starting until the new year I was able to take them up on their offer, and spent the last couple months of 2024 working on their mission. It turned into quite an intense project, working mostly solo to make significant changes to their user modelling (primarily separating admin-related code/authentication from their generic <code class="language-plaintext highlighter-rouge">User</code> model<sup id="user-model-src"><a href="#user-model">(1)</a></sup>). In the end it all went smoothly, which was very pleasing, and I was wrapped up in time for Christmas.</p>

<p>I imagine this won’t be the last time I take on short contracts outside of my work on Join Together. Although we’re busy when we have projects on, the in-between time tends to be quiet as the platform mostly takes care of itself. As long as my <a href="https://jointogether.online/team/">partners in unionisation</a> are not also on outside contracts, and can take care of urgent issues, it’ll be a good way to keep busy and the bank balance healthy.</p>

<h3 id="maintainable-ruby-podcast">Maintainable Ruby Podcast</h3>

<p>This summer I made an appearance on the <a href="https://maintainable.fm/episodes/tekin-suleyman-balancing-complexity-and-team-size">Maintainable Software podcast</a>. I really enjoyed chatting with Robby and sharing my reckons about software, teams and some of the things I believe helps keep software maintainable. Some people have said nice things about the episode, so that’s good.</p>

<h3 id="conferences">Conferences</h3>

<p>I was sad to miss <a href="https://haggisruby.co.uk">Haggis Ruby</a> (I got Covid), hopefully they’ll be back in 2025. I did make it to Brighton Ruby tho (ten for ten!), which is always one of the highlights of the year. Andy puts on an incredible conference (and has great taste in cocktail flights). The talks are generally good, but for me it’s about spending time and catching up with Ruby friends, old and new, in lovely Brighton. Have you got your tickets for <a href="https://ti.to/goodscary/brightonruby-2025">Brighton Ruby 2025</a> yet?</p>

<p>I did submit one <a href="https://speakerline.io/proposals/7761">talk proposal</a> this year, to both Brighton Ruby and RailsConf, but was unsuccessful. It’s for a talk on i18n in Rails. My thesis is that everyone writing Rails code should be making more use of i18n, even when the app isn’t actually being localised into other languages, and that following the less well known Rails’ conventions for i18n can lead to simpler, clearer and more maintainable code. I’d still like to develop this talk at some point, perhaps to share with <a href="https://nwrug.org">NWRUG</a>.</p>

<h2 id="life">Life</h2>

<p>Welcome to the world baby Ela! That’s four nieces now, my siblings sticking to what they know.</p>

<figure>
  <img class="post-image" src="/images/20250106/baby-ela.jpg" alt="A picture of a baby looking at the camera" />
  <figcaption>My niece Ela Mulvey, born July 2024</figcaption>
</figure>

<h3 id="travels">Travels</h3>

<p>Myself and Lauren had an Easter break in Malaga. What we hadn’t realised was that Easter is a big deal in that part of Spain. Our arrival coincided perfectly with Holly Week (Semana Santa), when various brotherhoods parade through the streets wearing unfortunately-appropriated pointy hoods and carrying massive Jesus-themed floats. It’s quite a thing to behold.</p>

<p><img class="post-image" src="/images/20250106/jesus-float.jpg" alt="A gold float with Jesus on a donkey being carried by dozens of robed men through the streets of Malaga" /></p>

<p>In August we went on a trip to Copenhagen by train, stopping off in Brussels and Hamburg on the way. Earlier in the year I’d managed to snag a table at <a href="https://noma.dk/nomathreepointzero/">Noma</a> for their final Vegetable Season before they close as a permanent restaurant. An eye-watering amount of money for a meal, but as an experience it was one I won’t forget in a hurry and one of my main highlights of the year.</p>

<figure>
  <img class="post-image" src="/images/20250106/noma-food.jpg" alt="A grid of photos showing various dishes from the set menu at Noma, Copenhagen" />
  <figcaption>The entirely-vegetarian food from Noma’s Vegetable Season menu</figcaption>
</figure>

<p>Copenhagen itself is a beautiful city and checks many of my travel boxes: great food and coffee everywhere; lots of interesting art and culture; plenty of things to do. The main highlights included: Swimming in the pristine harbour; Wandering around Freetown Christiana; The Design Musuem; and <a href="https://louisiana.dk/en/">Louisiana</a>, a beautiful modern art gallery just outside Copenhagen with notable work by Franz Gerscht, William Kentridge, Louise Bourgeois and Yayoi Kusama.</p>

<p><img class="post-image" src="/images/20250106/yayoi-room.jpg" alt="A photo from inside Yoyoi Kusama’s Gleaming Lights of the Souls" /></p>

<p>We were only there for a day, but Hamburg is also somewhere I’d love to go back to and spend more time exploring.</p>

<figure>
  <img class="post-image" src="/images/20250106/hamburg-friends.jpg" alt="Myself, Lauren and our friends in Hamburg" />
  <figcaption>Hanging out with pals in Hamburg</figcaption>
</figure>

<h3 id="walking-club">Walking club</h3>

<p>Although once again we failed to make it to Snowdon (due to untimely illness), it was another solid showing for our Walking Club this year with big walks every month except August. The highlight for me was an epic hike up Scafell Pike on a glorious sunny day in June. Get out in the wilds people, it’s good for the soul.</p>

<p><img class="post-image" src="/images/20250106/walking-club.jpg" alt="View into a valley from near the peak of Scafell Pike" /></p>

<h3 id="djing">DJing</h3>

<p>I started DJing regularly again towards the end of the year with a bimonthly residency at <a href="https://odioba.com">Ōdiōba</a>, a lovely little listening bar in Stockport. It’s been great fun playing records out again, and it’s got me reconnecting with my record collection. The sets will be making their way onto <a href="https://www.mixcloud.com/tekin/">my Mixcloud</a> if you’re interested (December’s should be up shortly). I also DJ’d the launch for my friend John’s new book <a href="https://the-modernist.org/collections/books/products/a-time-a-place">A Time ⋅ A Place</a>, which I would encourage you to buy as it’s great, but it’s also sold out, sorry.</p>

<h3 id="garden-makeover">Garden makeover</h3>

<p>Almost ten years after moving in it was finally time to sort out our garden. I’m prone to procrastination, especially when it comes to major house projects like this, as personal head space tends to be in short supply. Thankfully Lauren forced matters this spring by calling various trades people for quotes on paving, kicking off a short stint as a garden designer. It’s a small garden (not much bigger than a yard really) so designing it became a careful balancing act between space for planting whilst maintaining enough room for general hanging out when the Manchester weather permits. Here’s the obligatory before and after shots for you:</p>

<figure>
  <img class="post-image" src="/images/20250106/garden-before.jpg" alt="A lawn and various shrubs in an urban garden" />
  <figcaption>Garden before</figcaption>
</figure>

<figure>
  <img class="post-image" src="/images/20250106/garden-after.jpg" alt="A partially paved garden with triangular beds to the left and right, and garden furniture" />
  <figcaption>Garden after</figcaption>
</figure>

<p>I’m pretty pleased with how it turned out. It’s a much more useable space, and we were able to make the most of it this summer before winter kicked in. I’m looking forward to more planting and outdoor fun times in 2025.</p>

<h2 id="film-tv-and-theatre">Film, TV and Theatre</h2>

<p>Three films stuck out for me this year: <em><a href="https://www.youtube.com/watch?v=QzZBbX5A1FA">Perfect Days</a></em>, <em><a href="https://www.youtube.com/watch?v=r-vfg3KkV54">The Zone of Interest</a></em>, and <em><a href="https://www.youtube.com/watch?v=_RwLdIiZk_8">Soundtrack to a Coup D’etat</a></em>. Each was striking and moving in their own way, and I’d recommend all three as must-see films. Honourable mention to <em>Civil War</em>, <em>Dune 2</em>, <em>Kneecap</em>, <em>Longlegs</em> and <em>The Substance</em>.</p>

<p>On the TV front I don’t think it was a vintage year. Post strike/Covid/ZIRP slump? <em>Shōgun</em>, <em>Industry</em> and <em>The Penguin</em> stood out. <em>House of the Dragon</em>, <em>The Bear</em>, <em>Slow Horses</em>, <em>Black Doves</em>, <em>English Teacher</em> and <em>Agatha All Along</em> were all serviceable fun.  The show I enjoyed the most however was a reality TV show on Netflix: <em>Culinary Class Wars</em>, a thoroughly entertaining and over the top cross between MasterChef and Squid Games.</p>

<p>We got to the theatre a couple of times, and the most memorable thing for me was <em><a href="https://www.instagram.com/darknoonshow/">Dark Noon</a></em>, a fantastic and intense retelling of the birth of modern America by a predominantly black South African cast. If you live in Australia anywhere near Sydney (and happen to be reading this in January 2025) I would highly recommend you check it out at the <a href="https://www.sydneyfestival.org.au/events/dark-noon">Sydney Festival</a>.</p>

<h2 id="music">Music</h2>

<p>I thought it might have been a quiet one for new music this year, but looking back through the releases I picked up there are some standouts for sure. Two of my favourites come from friends of mine: <em><a href="https://wearerhythmsection.bandcamp.com/album/desire">Private Joy’s Desire! EP</a></em> and <em><a href="https://jamiefinlay.bandcamp.com/album/sun-dogs">Jamie Finlay’s Sun Dogs album</a></em>. Other releases that stood out were new albums from <em>Nala  Sinephro</em>, <em>Don Glori</em>, <em>Shabaka</em>, <em>Thandi Ntuli and Carlos Nino</em>, <em>Greg Foat</em> ,<em>Lady Wray</em>, <em>Sam Gandel &amp; Sam Wilkes</em>, <em>Allysha Joy</em>, <em>Bricknasty</em> and <em>Hiatus Kaiyote</em>.</p>

<p>Aside from new music, I’ve continued to dig out old gems in various record shops and fairs on my travels, including at <a href="https://www.recordplanet.nl/en">Record Planet</a> in Holland, one fo the biggest record fairs there is. Imagine two aircraft hangers full of stalls selling vinyl from all over the world. Two days digging through the crates was exhausting, but loads of fun, and I managed to come back with a modest haul of tasty records, including some I’ve been trying to hunt down for some time. It also happens to coincide with <a href="https://leguesswho.com">Le Guess Who?</a>, which is a fantastic music festival.</p>

<p>Speaking of festivals, <a href="https://weoutherefestival.com">We Out Here</a> was, as always, a wonderful weekend of sunshine, dancing and incredible music. I’m already looking forward to 2025.</p>

<p>Other live music that hit the spot this year included gigs by <em>Shabaka</em>, <em>Lady Wray</em>, <em>Bricknasty</em>. And not forgetting <a href="https://www.instagram.com/manchester_music_for_gaza/">Manchester Music for Gaza</a>, a moving day of music, talks and activities that raised over £10,000 for <a href="https://www.map.org.uk">Medical Aid for Palestinians</a>. Well done Ian, Yousef, Zoe, and all involved. Free Palestine! 🇵🇸</p>

<h2 id="games">Games</h2>

<p>I didn’t find a huge amount of time for games this year. The highlight was probably the <em>Resident Evil 4 remake</em>, which I thoroughly enjoyed: spooky, cinematic, and difficult in the right sort of way. <em>Animal Well</em> was also enjoyable, if a little baffling and opaque. I lost interest in uncovering all the eggs after getting to the end.</p>

<p>I also picked up <em>Cyberpunk 2077</em> after the PS5 update hoping to get lost in an epic adventure. And whilst the graphics and world building are very impressive, it took too much effort (and more than a few YouTube tutorials) to wrap my head around all the systems and mechanics enough to feel vaguely competent in the game. And by the time I was in my stride it felt like I’d already seen most of the content. With that said I apparently put over 70 hours into it, so I guess it was alright?</p>

<p>I’ve only got a little way into <em>Astrobot</em> and so far it’s a joyful and fun experience that manages to capture some of Nintendo’s magic sauce. I’m looking forward to spending more time with it in 2025.</p>

<p>Honourable mention to <em>Overcooked! All You Can Eat</em>, which several years later continues to eat up countless hours as myself and my friend Lucy wring every last drop of fun out of it trying to 4-star all the levels<sup id="all-the-levels-src"><a href="#all-the-levels">(2)</a></sup>. No co-operative game has come close to its combination of chaotic fun and charm. It’s a shame Ghost Town have given up on further expansions as we’d buy them in a heart beat. Here’s hoping their recently-announced <a href="https://youtu.be/T_i0xhr4utk">new co-operative game</a> is able to replicate some of the same magic.</p>

<h2 id="looking-ahead-to-2025">Looking ahead to 2025</h2>

<p>After a couple years of various random issues I’m aiming to get my health and fitness back on track, and generally have a more relaxed and mindful approach to life in 2025. More time for some creative projects. And of course Snowdon must finally be conquered!</p>

<h2 id="footnotes">Footnotes</h2>

<p><a id="user-model" href="#user-model-src">1</a>: Perhaps I should blog separately about the patterns and techniques I used to do this iteratively and without any downtime?</p>

<p><a id="all-the-levels" href="#all-the-levels-src">2</a>: Unfortunately this may actually be impossible with just the two of us as the target score on some levels appears calibrated for three or four players. Still we plough on…</p>]]></content><author><name>Tekin Süleyman</name></author><summary type="html"><![CDATA[Here's what happened in my 2024.]]></summary></entry><entry><title type="html">Different ways to use “–patch” in Git</title><link href="https://tekin.co.uk/2024/08/the-many-uses-for-git-patch" rel="alternate" type="text/html" title="Different ways to use “–patch” in Git" /><published>2024-08-23T00:00:00+00:00</published><updated>2024-08-23T00:00:00+00:00</updated><id>https://tekin.co.uk/2024/08/the-many-uses-for-git-patch</id><content type="html" xml:base="https://tekin.co.uk/2024/08/the-many-uses-for-git-patch"><![CDATA[<p>I’ve written previously about <a href="/2017/03/git-tips-you-possibly-did-not-know-you-needed#3-interactively-stage-changes-with---patch">using <code class="language-plaintext highlighter-rouge">--patch</code> to interactively stage changes</a>. But did you know that you can use <code class="language-plaintext highlighter-rouge">--patch</code> (aka <code class="language-plaintext highlighter-rouge">-p</code>) to similar effect with other Git commands? Let’s take a look…</p>

<h2 id="selectively-stashing-changes">Selectively stashing changes</h2>

<p><code class="language-plaintext highlighter-rouge">git stash</code> is great for temporarily stashing changes that you want to apply later, and handily it also supports selectively stashing changes with the <code class="language-plaintext highlighter-rouge">--patch</code> flag:</p>

<pre class="terminal"><code>  $ git stash --patch

  diff --git a/spec/sidekiq/upload_job_spec.rb b/spec/sidekiq/upload_job_spec.rb
  index b5f1d04e..e3b6227d 100644
  --- a/spec/sidekiq/upload_job_spec.rb
  +++ b/spec/sidekiq/upload_job_spec.rb
  <span class="light-blue">@@ -7,6 +7,7 @@</span> describe '#perform' do
       context 'the application has already been uploaded' do
         let(:membership_application) { create :forsa, :uploaded }

  <span class="diff-green">+      # Here is change one of two</span>
         it 'lets us know via Appsignal' do
           allow(Appsignal).to receive(:report_error)
           job.perform(membership_application.id)

  <span class="diff-blue">(1/2) Stash this hunk [y,n,q,a,d,j,J,g,/,e,p,?]?</span>
</code></pre>

<p>Bonus <code class="language-plaintext highlighter-rouge">git stash</code> tip: you can also selectively stash entire files using <code class="language-plaintext highlighter-rouge">--</code> to disambiguate the command from the paths you want stashing:</p>

<pre class="terminal"><code>  $ git stash -- path/to/file.rb</code></pre>

<h2 id="selective-discarding-changes-from-your-current-work-tree">Selective discarding changes from your current work tree</h2>

<p>You can use the <code class="language-plaintext highlighter-rouge">git restore</code> command to discard local changes and restore files to their last committed state. It can also be called with the <code class="language-plaintext highlighter-rouge">--patch</code> flag to interactively select specific hunks to discard:</p>

<pre class="terminal"><code>  $ git restore --patch

  diff --git a/spec/sidekiq/upload_job_spec.rb b/spec/sidekiq/upload_job_spec.rb
  index b5f1d04e..e3b6227d 100644
  --- a/spec/sidekiq/upload_job_spec.rb
  +++ b/spec/sidekiq/upload_job_spec.rb
  <span class="light-blue">@@ -7,6 +7,7 @@</span> describe '#perform' do
       context 'the application has already been uploaded' do
         let(:membership_application) { create :forsa, :uploaded }

  <span class="diff-green">+      # Here is change one of two</span>
         it 'lets us know via Appsignal' do
           allow(Appsignal).to receive(:report_error)
           job.perform(membership_application.id)

  <span class="diff-blue">(1/2) Discard this hunk from worktree [y,n,q,a,d,j,J,g,/,e,p,?]?</span>
</code></pre>

<p>Note the different phrasing of the prompt on the last line: Here we are choosing the changes we want to <em>discard</em>. Be careful, this is a destructive change, and because these are unstaged and uncommitted changes git won’t be able to help you recover the changes once they’ve been discarded!</p>

<h2 id="selectively-restoring-changes-from-another-branch-or-commit">Selectively restoring changes from another branch or commit</h2>

<p>The <code class="language-plaintext highlighter-rouge">git restore</code> command also lets you restore changes from another branch or commit by specifying the <code class="language-plaintext highlighter-rouge">--source=</code> flag. Combine this with the <code class="language-plaintext highlighter-rouge">--patch</code> flag and you can interactively choose the specific changes to restore to your current work tree:</p>

<pre class="terminal"><code>  $ git restore --source=branch-name --patch

  diff --git b/app/models/flow_definitions/neu.rb a/app/models/flow_definitions/neu.rb
  index cdabfe7a..28308244 100644
  --- b/app/models/flow_definitions/neu.rb
  +++ a/app/models/flow_definitions/neu.rb
  <span class="light-blue">@@ -42,10 +42,9 @@</span>
     step 'qualified-year' do
       radio :qualified_year do
  <span class="diff-red">-      option :'2024'
  -      option :'2023'
  -      option :'2022'
  -      option :'2021'</span>
  <span class="diff-green">+      MembershipApplication::Neu::QUALIFYING_YEARS.each do |year|
  +        option :"#{year}"
  +      end</span>

         divider

  <span class="diff-blue">(1/2) Apply this hunk to index and worktree [y,n,q,a,d,j,J,g,/,e,p,?]?</span>
</code></pre>

<p>I wil sometimes use this when I have a spike branch that I want to pull a subset of changes from to be tidied up and committed in my current branch.</p>

<h2 id="a-quick-note-on-git-restore-vs-checkout">A quick note on git restore vs checkout</h2>

<p>You may have noticed that the last two behaviours for discarding/restoring changes are also possible using <code class="language-plaintext highlighter-rouge">git checkout</code>. <a href="https://git-scm.com/docs/git-restore"><code class="language-plaintext highlighter-rouge">git restore</code></a> was <a href="https://public-inbox.org/git/xmqqy2zszuz7.fsf@gitster-ct.c.googlers.com/">introduced to Git in version 2.23</a> (alongside <a href="https://git-scm.com/docs/git-switch"><code class="language-plaintext highlighter-rouge">git switch</code></a>) in an effort to reduce the overloaded (and somewhat confusing) responsibilities placed on <code class="language-plaintext highlighter-rouge">git checkout</code>. Whilst it doesn’t look these features will be removed from <code class="language-plaintext highlighter-rouge">git checkout</code> <a href="https://github.com/git/git/blob/6a09c36371cbb902c573aee38d7cfd38f884f448/Documentation/BreakingChanges.txt">any time soon</a>, the more focused commands are easier to understand (and teach!), and arguably easier to use, at least assuming you don’t have years of muscle memory to overcome.</p>

<p>Thanks to <a href="https://ruby.social/@tom">Tom</a> and <a href="https://ruby.social/@bihi">Étienne</a> for the clarifying comments.</p>]]></content><author><name>Tekin Süleyman</name></author><category term="Git" /><category term="popular" /><summary type="html"><![CDATA[The --patch flag is handy for selectively staging changes in Git. But did you know it can also be used with other Git commands?]]></summary></entry><entry><title type="html">Appearance on the Maintainable Software podcast</title><link href="https://tekin.co.uk/2024/08/appearance-on-maintainable-podcast" rel="alternate" type="text/html" title="Appearance on the Maintainable Software podcast" /><published>2024-08-22T00:00:00+00:00</published><updated>2024-08-22T00:00:00+00:00</updated><id>https://tekin.co.uk/2024/08/appearance-on-maintainable-podcast</id><content type="html" xml:base="https://tekin.co.uk/2024/08/appearance-on-maintainable-podcast"><![CDATA[<p>I recently appeared on <a href="https://maintainable.fm/episodes/tekin-suleyman-balancing-complexity-and-team-size">the Maintainable Software podcast</a> with Robby Russell. It was fun chatting with Robby about some of the things that help keep software maintainable. <a href="https://maintainable.fm/episodes/tekin-suleyman-balancing-complexity-and-team-size">Check it out</a>!</p>]]></content><author><name>Tekin Süleyman</name></author><category term="popular" /><summary type="html"><![CDATA[Check out my recent appearance on the Maintainable Software podcast.]]></summary></entry><entry><title type="html">How to use introspection to discover what is exhausting your ActiveRecord connection pool</title><link href="https://tekin.co.uk/2023/08/introspecting-active-records-connection-pool" rel="alternate" type="text/html" title="How to use introspection to discover what is exhausting your ActiveRecord connection pool" /><published>2023-08-06T00:00:00+00:00</published><updated>2023-08-06T00:00:00+00:00</updated><id>https://tekin.co.uk/2023/08/introspecting-active-records-connection-pool</id><content type="html" xml:base="https://tekin.co.uk/2023/08/introspecting-active-records-connection-pool"><![CDATA[<p>This week I wrote about the reasons why you might need an <a href="/2023/07/active-record-connection-timeout-errors-with-puma">ActiveRecord connection pool larger than the number of configured Puma threads</a>.</p>

<p>Since then <a href="https://ruby.social/@bensheldon">Ben Sheldon</a> has pointed out that apps running embedded job workers (for example Sidekiq in <a href="https://github.com/sidekiq/sidekiq/wiki/Embedding">embedded mode</a>, GoodJob in <a href="https://github.com/bensheldon/good_job#execute-jobs-async--in-process">async mode</a> or <a href="https://github.com/brandonhilkert/sucker_punch">Sucker Punch</a>) will also be creating extra threads and therefor may need a larger thread pool.</p>

<p>In this follow-up post I’m going to describe the technique I used to uncover the source of the connection pool contention for the app I’m working on, and how you can do the same if your seeing mysterious <code class="language-plaintext highlighter-rouge">ActiveRecord::ConnectionTimeoutError</code> exceptions and don’t know where they’re coming from.</p>

<h2 id="uncovering-the-source-of-activerecord-connection-pool-contention">Uncovering the source of ActiveRecord connection pool contention</h2>

<p>These connection timeouts were a long-standing mystery in our app. Although they didn’t happen with great frequency, they were happening often enough to warrant some further digging rather than letting them become another broken window.</p>

<p>To figure out what was going on I employed some introspection on the connection pool. Whenever a thread asks for a connection from ActiveRecord, it is assigned one from the connection pool. The connection itself stores a reference to the assigned thread as its <code class="language-plaintext highlighter-rouge">owner</code>. We can inspect the assigned threads in the connection pool to learn a bit more about them:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection_pool</span><span class="p">.</span><span class="nf">connections</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">connection</span><span class="o">|</span>
  <span class="n">connection</span><span class="p">.</span><span class="nf">owner</span><span class="p">.</span><span class="nf">present?</span> <span class="p">?</span> <span class="n">connection</span><span class="p">.</span><span class="nf">owner</span><span class="p">.</span><span class="nf">inspect</span> <span class="p">:</span> <span class="s2">"[UNUSED]"</span>
<span class="k">end</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
</code></pre></div></div>

<p>You can try this in the rails console, although you’ll want to fire off a quick query first, otherwise there won’t be any connections assigned!</p>

<p>On the actual Puma web server, if connections are only assigned to Puma threads the output will look something like this:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#&lt;Thread:0x00007f0bbab3b740@puma srv tp 003 /app/.../gems/puma-6.2.2/lib/puma/thread_pool.rb:106 sleep&gt;
#&lt;Thread:0x00007f0bbab3b3d0@puma srv tp 004 /app/.../gems/puma-6.2.2/lib/puma/thread_pool.rb:106 run&gt;
#&lt;Thread:0x00007f0bbab3b8f8@puma srv tp 002 /app/.../gems/puma-6.2.2/lib/puma/thread_pool.rb:106 run&gt;
#&lt;Thread:0x00007f0bbab3bbc8@puma srv tp 001 /app/.../gems/puma-6.2.2/lib/puma/thread_pool.rb:106 sleep_forever&gt;
[UNUSED]
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">inspect</code> output tells us a bit about each thread. The key part is the path that points at the line of code where the thread was started, which in this case is inside the Puma thread pool <a href="https://github.com/puma/puma/blob/d79f59d69dd91cd1ea401ad5e9051e74b1ce0ebf/lib/puma/thread_pool.rb#L106">here</a>.</p>

<p>To narrow down the search for the mystery threads using up our connections I wired this code into our bug tracker such that we’d log this debug output whenever an <code class="language-plaintext highlighter-rouge">ActiveRecord::ConnectionTimeoutError</code> was raised:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Bugsnag</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">add_on_error</span><span class="p">(</span><span class="nb">proc</span> <span class="k">do</span> <span class="o">|</span><span class="n">event</span><span class="o">|</span>
    <span class="k">if</span> <span class="n">event</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">first</span><span class="p">.</span><span class="nf">error_class</span> <span class="o">==</span> <span class="s2">"ActiveRecord::ConnectionTimeoutError"</span>
      <span class="n">connection_pool_info</span> <span class="o">=</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">connection_pool</span><span class="p">.</span><span class="nf">connections</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">connection</span><span class="o">|</span>
        <span class="n">connection</span><span class="p">.</span><span class="nf">owner</span><span class="p">.</span><span class="nf">present?</span> <span class="p">?</span> <span class="n">connection</span><span class="p">.</span><span class="nf">owner</span><span class="p">.</span><span class="nf">inspect</span> <span class="p">:</span> <span class="s2">"[UNUSED]"</span>
      <span class="k">end</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>

      <span class="n">event</span><span class="p">.</span><span class="nf">add_metadata</span><span class="p">(</span><span class="ss">:app</span><span class="p">,</span> <span class="ss">:connection_pool_info</span><span class="p">,</span> <span class="n">connection_pool_info</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>It didn’t take long for the culprit to materialise:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#&lt;Thread:0x00007f32eca13ad0@puma srv tp 002 /app/.../gems/puma-6.2.2/lib/puma/thread_pool.rb:106 sleep_forever&gt;
#&lt;Thread:0x00007f32f23cd6e8 /app/.../gems/actionpack-7.0.5.1/lib/action_controller/metal/live.rb:341 sleep&gt;
#&lt;Thread:0x00007f32eca12a40@puma srv tp 003 /app/.../gems/puma-6.2.2/lib/puma/thread_pool.rb:106 sleep_forever&gt;
#&lt;Thread:0x00007f32f2936288 /app/.../gems/actionpack-7.0.5.1/lib/action_controller/metal/live.rb:341 sleep&gt;
#&lt;Thread:0x00007f32eca13e40@puma srv tp 001 /app/.../gems/puma-6.2.2/lib/puma/thread_pool.rb:106 run&gt;
</code></pre></div></div>

<p>We can see that two of the connections are assigned to threads spawned by <code class="language-plaintext highlighter-rouge">ActionController::Metal::Live</code> <a href="https://github.com/rails/rails/blob/fabd0b5827a3af1f189d726fbc7669f9fbdeef5e/actionpack/lib/action_controller/metal/live.rb#L341">here</a>.</p>

<p>This was the missing piece of the puzzle. It didn’t take much tracing of code to discover that it was <code class="language-plaintext highlighter-rouge">ActiveStorage</code> proxy requests that spin up extra threads for streaming responses, and it was these threads that were putting the extra pressure on our connection pool and causing the sporadic timeout errors.</p>

<h2 id="introspection-is-good-actually">Introspection is good actually</h2>

<p>I wanted to write this post partly to serve as a pointer to others that might be seeing these connection timeouts, but also to make a broader point about the blurred lines between our code and the libraries our code depends on. Because when our app is running, it <em>all effectively becomes our code</em>. Getting comfortable diving into the source for gems you use will serve you well, and will sometimes be the only way to get to the origin of a strange bug, or unexpected behaviour.</p>

<p>One of my favourite ways to do this is using <a href="https://bundler.io/man/bundle-open.1.html"><code class="language-plaintext highlighter-rouge">bundle open</code></a>. It gets you to the literal gem code that your app will be using right there in your preferred editor, ready for exploring. You can even make changes to the gem code for a spot of <code class="language-plaintext highlighter-rouge">puts</code> debugging (although do so with care and always remember to undo your changes when you’re done!)</p>

<p>Aside: If you’re a VSCode user, you may want to set the <code class="language-plaintext highlighter-rouge">BUNDLER_EDITOR</code> environment variable to <code class="language-plaintext highlighter-rouge">"code -n"</code> to stop <code class="language-plaintext highlighter-rouge">bundle open</code> replacing your current project window.</p>]]></content><author><name>Tekin Süleyman</name></author><category term="Ruby &amp; Rails" /><summary type="html"><![CDATA[A short post on a technique you can use to see what is using your Rails app's ActiveRecord connections.]]></summary></entry><entry><title type="html">Why the advice to have a connection pool the same size as your Puma threads is (probably) wrong for you</title><link href="https://tekin.co.uk/2023/07/active-record-connection-timeout-errors-with-puma" rel="alternate" type="text/html" title="Why the advice to have a connection pool the same size as your Puma threads is (probably) wrong for you" /><published>2023-07-31T00:00:00+00:00</published><updated>2023-07-31T00:00:00+00:00</updated><id>https://tekin.co.uk/2023/07/active-record-connection-timeout-errors-with-puma</id><content type="html" xml:base="https://tekin.co.uk/2023/07/active-record-connection-timeout-errors-with-puma"><![CDATA[<p>The <a href="https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#database-connections">standard advice</a> goes: <a href="https://devcenter.heroku.com/articles/concurrency-and-database-connections#threaded-servers">set your Rails database’s connection pool to have as many connections as you have Puma threads</a>. The idea being that you should only need as many connections as you have concurrent threads. This advice is coming from a good place, as most of the time you will be constrained on the number of connections you have available to your database.</p>

<p>The nuance missing from that advice is that it assumes no additional threads are ever spawned during your apps operation!</p>

<p>Now although you might know your code inside and out and be 100% certain that you don’t create additional threads anywhere in your code, chances are Rails is creating additional threads without you realising it…</p>

<h2 id="an-aside-on-threads-and-activerecords-connection-pool">An aside on threads and ActiveRecord’s connection pool</h2>

<p>On a basic level, the way ActiveRecord’s connection pool works is that it assigns each thread that asks for a connection its <a href="https://github.com/rails/rails/blob/35a614c227620a62d7a2a242e375a43e7e2affc5/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L178-L185">own separate connection</a>, which the thread then releases once it’s finished. Using a pool like this means that many threads can be querying the database at the same time, so many more requests can be processed in parallel.</p>

<p>If your app’s Puma config sets  the maximum number of threads to 5, then you can normally expect there to be at most 5 threads asking for their own database connection, hence the advice to set the pool size to the same as size as the number of threads.</p>

<p>If however one of those threads creates another thread of its own, and that thread needs a database connection to hit the database, it will need its own connection. <strong>The spawned thread does not share the connection of the thread that spawned it</strong>.</p>

<p>So if we have threads that spawn their own threads, it’s possible to end up in a situation where there are many more threads wanting a connection then there are available connections, and you could end up seeing <code class="language-plaintext highlighter-rouge">ActiveRecord::ConnectionTimeoutError</code> exceptions being raised.</p>

<h2 id="where-your-rails-app-might-be-spinning-up-additional-threads-without-you-realising-it">Where your Rails app might be spinning up additional threads without you realising it</h2>

<p>So, back to your app. Even if you are not explicitly spawning your own threads, Rails itself could be spinning up additional threads without you realising. The main culprit here is <code class="language-plaintext highlighter-rouge">ActiveStorage</code>, or more specifically, <a href="https://edgeguides.rubyonrails.org/active_storage_overview.html#proxy-mode">ActiveStorage configured in proxy mode</a> (commonly used if you’re serving assets via a CDN).</p>

<h3 id="threads-created-by-activestorage">Threads created by ActiveStorage</h3>

<p>ActiveStorage’s two <a href="https://github.com/rails/rails/blob/main/activestorage/app/controllers/active_storage/blobs/proxy_controller.rb">proxy</a> <a href="https://github.com/rails/rails/blob/main/activestorage/app/controllers/active_storage/representations/proxy_controller.rb">controllers</a> return <em>streamed responses</em>, which (you guessed it) are <em>processed in their own threads!</em> So to handle a request for an <code class="language-plaintext highlighter-rouge">ActiveStorage</code> file via one of the proxy controllers, two threads will be called into action, both of which need their own connection from the ActiveRecord connection pool.</p>

<p>Normally (hopefully?) your app is processing requests fast enough for you to expect connections to be freed up from one of the other threads and become available for the streaming thread before a timeout occurs. And in this way, most of the time, the limited connections available in the pool can be shared between more threads than there are connections. The problems start if your app receives many successive/concurrent <code class="language-plaintext highlighter-rouge">ActiveStorage</code> proxy requests, each of which spin up an additional thread to stream a response, and they take a long time to complete their work and free up their connections; either because they’re doing expensive/slow work server-side (downloading a large file or processing an image representation for the first time), or they’re streaming the response to a slow client. At the time of writing at least, both threads hang on to their connection until the entire response is complete.</p>

<h3 id="threads-created-by-activerecords-load_async">Threads created by ActiveRecord’s load_async</h3>

<p>Another place Rails will spin up additional threads is with the new <a href="https://edgeapi.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-load_async">load_async</a> method. This was introduced in Rails 7 as a way to parallelise expensive database queries. It’s less likely that these are going to cause connection timeout errors as (again, hopefully?) the expensive queries aren’t so expensive that they hang onto connections for an excessive amount of time. That said, if you’re making heavy use of <code class="language-plaintext highlighter-rouge">load_async</code>, you could again end up in a situation where you have much higher demand for connections than there are available connections.</p>

<h2 id="so-what-should-you-do-about-all-this">So what should you do about all this?</h2>

<p>The upshot of all this is: if you’re making use of either <code class="language-plaintext highlighter-rouge">ActiveStorage</code> in proxy mode, or calling <code class="language-plaintext highlighter-rouge">load_async</code> in our app, you probably want a connection pool size that is higher than the number of configured Puma threads. The theoretical maximum number of connections you’ll need will be whatever Puma’s configured thread count is x 2, based on the assumption that each thread can potentially spawn one additional thread, but you might be able to get away with a smaller multiple depending on the performance and load characteristics of your app.</p>

<p>And if you’re close to reaching or exceeding the connection limit offered by your database, consider using a separate connection pool like <a href="http://www.pgbouncer.org">pgbouncer</a> (available as a <a href="https://github.com/heroku/heroku-buildpack-pgbouncer">Heroku buildpack</a>) to increase the number of connections you can configure.</p>

<h2 id="update-6-august-2023">Update (6 August 2023)</h2>

<p>There’s now a <a href="/2023/08/introspecting-active-records-connection-pool">follow-up post</a> outlining one of the techniques I employed to help figure out why our connection pool was being exhausted in our Rails app.</p>

<h2 id="update-19-september-2024">Update (19 September 2024)</h2>

<p>Ben Sheldon has written a great post explaining how to <a href="https://island94.org/2024/09/secret-to-rails-database-connection-pool-size">perfectly calculate your Rails database connection pool
size</a>. The TL;DR: just make it very big!</p>]]></content><author><name>Tekin Süleyman</name></author><category term="Ruby &amp; Rails" /><category term="popular" /><summary type="html"><![CDATA[Are you seeing ActiveRecord::ConnectionTimeoutErrors in your Rails app? You probably need a bigger connection pool (and here's why).]]></summary></entry><entry><title type="html">List your Git branches by recent activity</title><link href="https://tekin.co.uk/2021/11/listing-most-recent-git-branches" rel="alternate" type="text/html" title="List your Git branches by recent activity" /><published>2021-11-25T00:00:00+00:00</published><updated>2021-11-25T00:00:00+00:00</updated><id>https://tekin.co.uk/2021/11/listing-most-recent-git-branches</id><content type="html" xml:base="https://tekin.co.uk/2021/11/listing-most-recent-git-branches"><![CDATA[<p>Even if you’re diligent and regularly <a href="/2020/01/clean-up-your-git-branches-and-repositories">delete merged and stale
branches</a>
you may still find it hard to pick out a particular branch from the
alphabetically-sorted output of <code class="language-plaintext highlighter-rouge">git branch</code>. How about something
more useful, like seeing them listed based on their freshness?</p>

<p>The <code class="language-plaintext highlighter-rouge">git branch</code> command accepts a <code class="language-plaintext highlighter-rouge">--sort</code> option which we can use to
list our branches based on the last committer date:</p>

<pre class="terminal"><code>  <strong>$ git branch --sort=-committerdate</strong>
  * main
    redact-abandoned
    log-downloads
    i18n-lint
</code></pre>

<p>We can also use the <code class="language-plaintext highlighter-rouge">--format</code> option to include the exact time and
see just how fresh each branch is:</p>

<pre class="terminal"><code>  <strong>$ git branch --sort=-committerdate --format="%(committerdate)%09%(refname:short)"</strong>
  Thu Nov 25 10:29:48 2021 +0000  main
  Fri Nov 19 16:08:49 2021 +0000  redact-abandoned
  Fri Nov 19 16:04:11 2021 +0000  log-downloads
  Thu Nov 18 21:53:37 2021 +0000  i18n-lint
  Sun Jul 18 12:49:18 2021 +0100  without-routing-key-overrides
</code></pre>

<p>Or for something more friendly and easy-to-parse we can ask for a relative date:</p>

<pre class="terminal"><code>  <strong>$ git branch --sort=-committerdate --format="%(committerdate:relative)%09%(refname:short)"</strong>
  5 hours ago     main
  6 days ago      redact-abandoned
  6 days ago      log-downloads
  7 days ago      i18n-lint
  4 months ago    without-routing-key-overrides
</code></pre>

<p>That’s better! Let’s add this as a <code class="language-plaintext highlighter-rouge">git recent</code> alias to our Git config:</p>

<pre class="terminal"><code>  <strong>$ git config --global alias.recent 'branch --sort=-committerdate --format="%(committerdate:relative)%09%(refname:short)"'</strong>
</code></pre>

<p>Hat tip to <a href="https://twitter.com/tenderlove/status/1392957802163802112">Tenderlove</a>
for this particularly tasty snippet.</p>

<p>Here are some other Git related articles you might find useful:</p>

<ul>
  <li><a href="/2020/01/clean-up-your-git-branches-and-repositories">An alias for cleaning up your old/redundant Git branches</a></li>
  <li><a href="/2021/01/how-atomic-commits-make-you-a-better-coder">How focused commits make you a better coder</a></li>
  <li><a href="/2020/10/better-git-diff-output-for-ruby-python-elixir-and-more">Better Git diff output for Ruby, Python, Elixer, Go and more</a></li>
  <li><a href="/2020/06/jump-from-a-git-commit-to-the-pr-in-one-command">An alias for jumping from a commit SHA to the PR on GitHub</a></li>
  <li><a href="/2020/01/git-alias-for-amending-your-last-commit">An alias for amending your most recent commit</a></li>
</ul>]]></content><author><name>Tekin Süleyman</name></author><category term="Git" /><category term="popular" /><summary type="html"><![CDATA[Sometimes you just want to see your most recent branches. Here's a handy Git alias that will help you do just that.]]></summary></entry></feed>