Hardscrabble šŸ«

By Max Jacobson

See also: the archives and an RSS feed

Joker Wedding

January 16, 2023

This recent SNL sketch, starring Andrew Dismukes, Heidi Gardner, and host Jack Harlow, had me cracking up.

It kinda runs out of steam halfway through but it starts so strong.

I feel like repetition and yelling are always funny to meā€¦

xargs

January 16, 2023

This is a quick tribute to and summary of xargs, the glue that holds together most of my shell scripts.

Imagine you have a file called fruits.txt:

$ cat fruits.txt
apples.txt
oranges.txt
bananas.txt

Itā€™s a file with fruits all listed out on separate lines. xargs lets you squash that list down into a single line:

$ cat fruits.txt | xargs
apples.txt oranges.txt bananas.txt

What itā€™s doing is taking a list of things and turning them into arguments.

What do I mean by ā€œargumentsā€? Letā€™s look at this exampleā€¦

$ rm apples.txt oranges.txt bananas.txt

In that statement, weā€™re passing three arguments to the rm command, which will remove the files. In order to pass multiple file names to rm, they need to be written as arguments, meaning theyā€™re all on one line and separated by spaces.

Soā€¦ what if we have a fruits.txt that contains a list of filenames, and we want to execute the rm program, and pass it that list of files as arguments?

We can do it like this:

$ cat fruits.txt | xargs rm

In English, you might read this as ā€œTake the contents of fruits.txt, turn that into arguments, and then execute the rm command with those argumentsā€.

This kind of problem comes up all the time when writing shell scripts. For me, it comes up most often when Iā€™m writing one liners that Iā€™m executing at the command line. Here are a few real examples I found in my shell history:

$ git ls-files | grep -E "\.(rb|jbuilder|ru)$" | xargs rubyfmt --write

In other words: ā€œFind all of the ruby files in my git repository and format them with rubyfmt.ā€

$ git ls-files app/assets/javascripts/checkout | grep -E "\.js$" | xargs code

In other words: ā€œFind all of the JavaScript files in a particular folder of my git repository and open them in VS Code.ā€

$ git ls-files | grep "test.jsx" | xargs grep -l  enzyme | xargs rm

(This one has a double xargs šŸ‘€)

In other words: ā€œFind all of the react tests in my git repository and then search just those files for the word enzyme, and then print out the list of those file names, and then pass them as arguments to the rm command.

That last example is a pretty good category of problem that xargs solves. I often search through a codebase to find source code that matches a pattern. Itā€™s easy enough to do that with a simple grep (or the search in your preferred editor). But I occasionally want to narrow down the search to a subset of files, and thatā€™s not always easy to do with a single statement.

For example, I might want to search for ā€œFind me everywhere in this codebase that contains the string FOO but narrowed down to files that happen to also contain the string BARā€. Thatā€™s easy enough to achieve once youā€™re comfortable with xargs:

$ grep -lr BAR . | xargs grep FOO
./app/models/human.rb:    puts FOO.inspect

Note: in my day-to-day life, Iā€™m generally using ripgrep, not grep, but I wanted to keep the examples simple by using more standard things. But once youā€™re using ripgrep, the command gets a bit simpler and faster too:

$ rg -l BAR | xargs rg FOO
app/models/human.rb
22:    puts FOO.inspect

All right, thatā€™s all I got. Have fun.

Rachel

January 15, 2023

This short film is so funny even while it kind of feels like a horror story the entire time.

Kate Berlant and John Early are usually so heightened and wacky that itā€™s almost disorienting to see them in a slightly natural mode.

You can read more about the true story that inspired this on Vulture.

Also: this reminds me I need to see Kate Berlantā€™s new show, Kate while I still canā€¦

Miley Cyrus - Flowers

January 15, 2023

This new Miley Cyrus song is a bop:

I feel like Cyrus is pretty underrated. See also this recent performance:

How to reduce recruiter spam

January 5, 2023

If you work in the tech industry, you may get a lot of emails from recruiters, and maybe even the occasional cold call.

In times of abundance, we like to complain about this. It can be genuinely annoying to be spammed, even if it is a bit of a humble brag to complain that so many people want to hire you.

So, how to reduce the amount you get?

Let me back up a little and give some context for this storyā€¦ Back in November 2013, I registered hardscrabble.net, signed up for an email account on Fastmail, and promptly tried to retire the gmail account I had been using since 2007. I was bought in to the idea that you need to own your own domain, and avoid being locked in to services that may not stick around.1

But I had a problemā€¦ even if I want to stop using my gmail account, the rest of the world is still going to keep emailing me at that address, because itā€™s still in their address books. I was prepared to take on the tedious task of updating all of my own accounts on various services to use my fancy new custom address, but I had no way to update the address books of my aunts and cousins.

Over the years, emails to my old address mostly dried up. But I still got the occasional recruiter email there. This surprised me, because my new address was listed publicly in several places (linkedin, this blog, my github) and my gmail wasnā€™t listed publicy anywhere I was aware of.

So, several months ago, I started replying to any recruiter emails sent to my gmail with something along these lines:

Hi Molly,

Iā€™m not currently interested in exploring new job opportunities, but Iā€™d love to ask you a favor if you have a momentā€¦

Where did you get this email address? Iā€™ve been trying to retire it for a decade (I list a different email address on LinkedIn for example), but people continue to email it, so I assume itā€™s still listed somewhere. Or is it in some database? If so, would you mind telling me what service youā€™re using to source candidates? I donā€™t mind being in a database but Iā€™d love to perhaps update my info thereā€¦

Iā€™ve been curious about this for years and if you can help me resolve the mystery I would be in your debt and might even be able to refer some candidates who are actually on the market šŸ˜‰

All the best, Max

I was basically lying about referring them candidates but I felt like I needed to offer a back scratch if Iā€™m going to ask for one. And it worked! These recruiters were super nice, and were happy to answer the question, which I really appreciated.

Hereā€™s an example reply:

Hahaha, thank you for the response Max and this is probably the most fun Iā€™ve had with ā€œnot interestedā€ emails!

We use GEM in addition to LinkedIn for our recruiting efforts - it is relatively newer compared to LinkedIn but I think itā€™s been a pretty popular software since a few years ago and to my knowledge a lot of companies are utilizing it. I managed to remove this particular email address from our database so you shouldnā€™t receive any outreach from my team (unless we switch for a brand new software maybe?) Unfortunately, Iā€™m not quite sure where GEM pulled this email from, but it looks like theyā€™ve listed a few more: redacted. Let me know if youā€™d like to remove all of them - happy to do so as well!

Hereā€™s what I learned: there are a whole bunch of companies out there which are scraping the internet for data and then selling that data to companies. And they all offer a flow to opt out2.

Here are some examples:

And then in the interest of naming and shaming, here are some services recruiters cited which do not seem to have a publicly-discoverable page to opt out of having your data sold:

Opting out on those pages reduced the amount of recruiter spam Iā€™ve received.

And then hereā€™s the punchline to this blog post: I got laid off yesterday and now I need to start thinking about a new job search, and my inbox is crickets. Whoops!

  1. Almost a decade later, gmail is still holding pretty strong I have to admitā€¦Ā 

  2. Which I assume they are legally required to do? Not sureā€¦Ā 

Steam Locomotive

December 8, 2022

A few weeks ago, Facebook1 published a blog post called Sapling: Source control thatā€™s user-friendly and scalable that I read with great interest.

Source control is an interest of mine. Itā€™s this absolutely critical tool in our software lives, but itā€™s got this ruthless learning curve that leads to most people carving out a small handful of commands they feel confident using, and never straying outside of those. People keep trying to figure out ways to make it easier to use ā€“ Iā€™m thinking of things like Tower, GitHub Desktop, and Visual Studio Codeā€™s Source Control ā€“ and theyā€™re actually pretty great. Iā€™m particularly impressed with the Visual Studio Code one, because it embraces the exact same terminology that you find when using the git command line interface2, but it organizes that information and presents it all in very usable ways (the conflict resolution tooling in particular is night and day better than anything Iā€™ve used before).

Plus, itā€™s always fun to hear about people dealing with devex scaling issues. Theyā€™ve got so many people over there writing so much code that they have to have huge internal teams to make tools for the rest of them to use just to keep up. Thatā€™s nuts. Godspeed to ā€˜em.

Enter sapling, their new CLI tool, which seems to be a new client for git repositories. That is, you can bring your own git repository, but instead of using the git CLI or any of the graphic clients I mentioned above, you use their alternative CLI, which you invoke on the command line with sl.

Hereā€™s the thingā€¦ ā€œsaplingā€ is a great project name3 and sl is a great name for a command. Thereā€™s only one problem, and itā€™s a showstopper: sl is taken!

Hereā€™s what I see when I run sl in my terminal:

output of sl, an ASCII train crossing the screen

Iā€™ll go into why this is very important, and in fact the remainder of this blog post is entirely about how much I love this command and what it means to me, so I apologize if you want to learn more about sapling. I literally cannot even try it, because I cannot install it, because of this naming collision. So youā€™re on your own. I hope itā€™s great.

Hereā€™s the output of man sl, which has a lot to unpack:

SL(1)			     General Commands Manual			   SL(1)



NAME
       sl - cure your bad habit of mistyping

SYNOPSIS
       sl [ -alFc ]

DESCRIPTION
       sl is a highly advanced animation program for curing your bad habit of
       mistyping.

       -a     An accident is occurring. People cry for help.

       -l     Little version

       -F     It flies like the galaxy express 999.

       -c     C51 appears instead of D51.

SEE ALSO
       ls(1)

BUGS
       It sometimes lists directory contents.

AUTHOR
       Toyoda Masashi (mtoyoda@acm.org)



				 March 31, 2014 			   SL(1)

First of all.. letā€™s check out some of these delightful alternate modes.

Hereā€™s the output of sl -a:

output of sl -a, an ASCII train crossing the screen with people crying for help

Youā€™ll notice, as promised, that people cry for help.

There are a few other fun flags, but Iā€™ll leave exploring those as an exercise for the reader. One tip: try combining flags.

Some fun facts about sl:

  1. Looking at the source code, it seems this program dates back to 1993, when this blogā€™s author was five years old.
  2. sl, of course, is short for steam locomotive.
  3. The author, Masashi Toyoda, is currently a professor at The University of Tokyo, but he still highlights his true claim to fame ā€“ sl, of course ā€“ on his homepage
  4. It takes about six seconds to run
  5. If you try to kill it by pressing ctrl c to send an interrupt signal, it just ignores you. This is very annoying in a peevish way that I find charming. You will sit through the animation.

The man page makes it sound like the motivation for this was to help break a habit of misstyping ls as sl. If, each time you make that mistake, youā€™re forced to watch a 6 second animation that you cannot skipā€¦ perhaps that will help you break that habit.

This is kind of like the command line version of Mavala Stop Deterrent Nail Polish Treatment, a product Iā€™ve personally found very helpful. You may have heard of it. Itā€™s a clear nail polish you can put on which tastes terrible. If you find yourself idly biting your nails, it will help you stop. Itā€™s incredibly helpful as a habit breaker.

I personally do not misstype ls as sl very often. In fact I often will idly type sl on purpose and watch the train animation when Iā€™m thinking thru a problem.

But I have found it useful to help break some other habits. Iā€™ll give one more example.

One command line habit I have is running hub browse to open the current repo on github.com. Itā€™s great. Itā€™s context dependent, so for example if youā€™re currently on a branch, it will open that branch on github.com.

But I want to break this habit. A few years ago, GitHub introduced an official CLI called gh. I installed that too, and itā€™s also pretty good, and in fact it also has a command called gh browse. Based on this doc comparing hub and gh, I would like to stop using hub entirely and just use gh.

Thereā€™s only two problems:

  1. gh browse does not seem to be as clever as hub browse; it just loads the repo home page, no matter what branch youā€™re on
  2. my damn muscle memory continues to type hub browse no matter what I do

Soā€¦ where does sl come in?

First step: uninstall hub with brew uninstall hub.

Itā€™s gone now. Nice. Now when I type hub browse, I see this:

$ hub
zsh: command not found: hub

Thatā€™s accurate enough, but itā€™s not sufficiently punitive to be effective.

Second step: bring in the steam locomotive

I added this alias to my shell configuration:

alias 'hub'='sl -a'

Now, whenever I type hub browseā€¦ I get the train.

Third step: make gh browse context dependent.

Running man gh-browse, I see that gh browse actually has some handy-looking options:

OPTIONS
       -b, --branch <string>
	      Select another branch by passing in the branch name


       -c, --commit
	      Open the last commit

(plus some others, not pictured).

Which is to say that you can run gh browse --branch asdf to open github to branch asdf. Or you can run gh browse --commit to browse directly to the last commit, wherever youā€™re currently checked out.

So, a script like this, built on top of the gh CLI, kinda-sorta recreates the behavior of hub browse:

#!/usr/bin/env sh

branch="$(git symbolic-ref --short HEAD 2>/dev/null)"

if [ $? -eq 0 ]; then
  gh browse --branch="$branch"
else
  gh browse --commit
fi

So, now we can save that file as git-browse, and put it somewhere on our $PATH.

Now I can type simply git browse and git will find that git-browse script and invoke it. Under the hood, it uses the gh command. And if I ever forget, and type hub browse, I get to look at a train and contemplate my choices.

  1. I guess I should say ā€œMetaā€ but idcĀ 

  2. Why, in GitHub Desktop, do you ā€œpublishā€ your branch instead of ā€œpushā€ it? I can see the argument that itā€™s more intuitive to a beginner, but it forces them to learn two things instead of one.Ā 

  3. In fact I made a little project called sapling 7 years ago which had nothing to do with git. It was a knock-off of tree another favorite tool of mine, that I made when I was first learning the basics of Rust.Ā 

Learning to love vim colorschemes

August 1, 2021

Up until a few months ago, I was living in the dark.

Iā€™ve been using vim as my main editor for most of my career. It has a built-in feature for setting a colorscheme, and it comes with a handful of color schemes with intriguing names like elflord, delek, and darkblue.

All you have to do is a add a line like this to your ~/.vimrc to set a colorscheme on boot:

colorscheme darkblue

I never used it. I tried it out occasionally, but none of them looked that good.

When I wanted to customize the colors in my editor, Iā€™d do it indirectly, by configuring the color schemes for my terminal app. Doing that affects the colors of all programs you run, not just vim, and it seemed good enough for me.

I kind of thought that was the best I could hope for. And, unfortunately, it was never great. Often it wasnā€™t even readable; Vim didnā€™t necessarily know which of the terminalā€™s 16 colors made sense for which contexts, and it might choose to render dark grey text on a black background, because it didnā€™t really know better.

It was a ā€œyou get what you get and you donā€™t get upsetā€ situation.

delta

Hereā€™s what changed: I came across delta, a new-ish project which bills itself as ā€œA viewer for git and diff outputā€, and it just looks so good that it made me realize I need to raise my expectations a bit.

I configured git to use delta when displaying diffs (I use git show and git diff all the time), and I configured it to display them in a way that looks a lot like how the same diff would look on GitHub. That means a nice shade of green when highlighting new lines, a nice shade of red when highlighting removed lines, and actual syntax highlighting on all of the lines. That last bit is still quite cool to me, months later.

True color

But wait a second? How does delta look so good? I thought terminal programs were constrained to only have access to the 16 colors provided by your terminal theme?

Welp, turns out thatā€™s not true. Modern terminal apps support True Color, which basically means they support whatever colors you want. For years, I used the Terminal app that comes with Macs because it seemed fine, and I didnā€™t understand why people preferred iTerm2. Now I get it. Per this indispensable reference, Terminal.app does not support True Color, whereas iTerm2 does.

bat

Before I heard of delta, Iā€™d heard of bat, which bills itself as ā€œA cat(1) clone with wings.ā€ Itā€™s a lot like cat (you can use it to look at the contents of a file), but it will syntax highlight the output.

Delta is actually building on top of bat, and it also looks great.

Go ahead and install both delta and bat if you want to follow along with me here as I talk you through all the color-related changes Iā€™ve made in my dotfiles recently.

Getting a custom color scheme

Both bat and delta come with some number of syntax themes built-in, which might be good enough for you. But my heart yearns for smyck.

Smyck is not a standard color scheme, but itā€™s one that I happen to really like. Itā€™s got a chill pastel vibe. The icy blue, mustardy yellow, and salmon pink all set me at ease. These steps should work for other themes too, but Iā€™m going to take this opportunity to evangelize Smyck.

Hereā€™s the one-time setup to get Smyck assets in place:

  1. Clone the smyck repo, which contains the color scheme in a handful of formats
  2. Double click Smyck.itermcolors to load the iTerm2 color scheme
  3. Copy the smyck.vim file into ~/.vim/colors, which is where vim will look later on when we run colorscheme smyck
  4. Copy the Smyck.tmTheme file into ~/.config/bat/themes and run bat cache --build ā€“ this will make the theme available to bat and delta

Configuring vim to use smyck

In my ~/.vimrc:

if $COLORTERM == 'truecolor'
  set termguicolors
  let &t_8f = "\<Esc>[38;2;%lu;%lu;%lum"
  let &t_8b = "\<Esc>[48;2;%lu;%lu;%lum"
  colorscheme smyck
endif

The conditional is there because this configuration only works in terminals that support true color. Iā€™m definitely sold on iTerm2 now, but I donā€™t want everything to look wacky if I did try and use vim in Terminal.app.

Those funky &t_8f and &t_8b things are there for tmux compatibility. I have no idea what they mean. I copied them from the internet.

I also added this:

let g:fzf_colors =
\ { 'fg':      ['fg', 'Normal'],
  \ 'bg':      ['bg', 'Normal'],
  \ 'hl':      ['fg', 'Comment'],
  \ 'fg+':     ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
  \ 'bg+':     ['bg', 'CursorLine', 'CursorColumn'],
  \ 'hl+':     ['fg', 'Statement'],
  \ 'info':    ['fg', 'PreProc'],
  \ 'border':  ['fg', 'Ignore'],
  \ 'prompt':  ['fg', 'Conditional'],
  \ 'pointer': ['fg', 'Exception'],
  \ 'marker':  ['fg', 'Keyword'],
  \ 'spinner': ['fg', 'Label'],
  \ 'header':  ['fg', 'Comment'] }

I use fzf as a fuzzy-file-finder in vim. This fzf_colors configuration tells it to infer its colorscheme from the active vim colorscheme.

Configuring git to use delta and smyck

In my ~/.gitconfig:

[core]
  pager = delta --syntax-theme Smyck
[interactive]
  diffFilter = delta --line-numbers --color-only --syntax-theme Smyck
[delta]
  features = unobtrusive-line-numbers
[delta "unobtrusive-line-numbers"]
  line-numbers = true

Youā€™ll note that I abandoned the GitHub theme and went all-in on Smyck.

Configuring tmux to enable true color and smyck-friendly colors

In my ~/.tmux.conf:

set -g default-terminal 'screen-256color'
set -g status-bg '#96D9F1'
set -g status-fg '#282828'

Here Iā€™m just manually specifying some of the Smyck colors so that tmuxā€™s status bar at the bottom blends in.

Configuring bat to use smyck

In my ~/.config/bat/config:

--theme="Smyck"

This tells bat to use the Smyck theme by default, without me needing to specify it every time.

Configuring my muscle memory to use bat

In my ~/.zshrc:

alias 'cat'='bat'

The end result

As I tweeted recently, the end result feels like a pretty luxe experience:

Everything is Smyck.

I used to think the vim colorschemes command wasnā€™t very good, and that I was better off on my own. But Iā€™ve matured. Come join me.

On conflicts

April 3, 2021

Usually when git tells me that my feature branch has a conflict, it makes sense to me. Occasionally though, Iā€™ll have a conflict, and Iā€™ll look at and thinkā€¦ ā€œHuh? Why is that even a conflict?ā€

I think it makes sense to me now though. Letā€™s talk about it.

An example conflict

Letā€™s look at a classic example of a git conflict.

Letā€™s say on the main branch, thereā€™s a file that looks like this:

class Dog
  def speak
    puts "woof"
  end
end

Then letā€™s say I check out a feature branch my-feature, and I change that file to look like this:

class Dog
  def speak
    puts "ruff"
  end
end

So far, so good.

However, unbeknownst to me, my colleague also checked out a feature branch and changed the file like so:

class Dog
  def speak
    puts "bark"
  end
end

And then merged that in.

The commit history looks a bit like this:

* dog.rb is introduced     -- commit A, on branch main
| \
|  * I update dog.rb       -- commit B, on branch my-feature
* colleague updates dog.rb -- commit C, on branch main

I have a merge conflict on my branch now! But why exactly and what is a conflict?

Why we have conflicts, and what they are

Derrick Stolee recently published a very good blog post on the GitHub blog called Commits are snapshots, not diffs which I would encourage you to read. It seeks to clear up that common misconception by going into a lot of the weeds of how git organizes its information.

In our example, we have three commits, which means we have three snapshots of the repository at three points in time.

The problem, then, is: how do you combine two snapshots?

Commits are snapshots, not diffs. That just means that git is not storing diffs on your file system. However, each commit does know which commit came before it (its parent), and git does know how to generate a diff by comparing two snapshots.

In that example, commit Bā€™s parent is commit A. Commit Cā€™s parent is also commit A.

So what happens if, while on the my-feature branch, we run git rebase main, to keep our feature branch up-to-date with what our colleagues are doing?

  1. git figures out which are the new commits that arenā€™t yet on main (just commit B)
  2. git figures out what is the latest commit on main (commit C)
  3. git resets to how things are as of the commit C snapshot
  4. git goes through the new commits, one by one. For each one, git generates a diff between it and its parent, and then attempts to apply that diff to the current workspace. As soon as thereā€™s a conflict, it halts.

In this simple example, thereā€™s only one new commit, and its diff with its parent looks like this:

$ git diff A..B
diff --git a/dog.rb b/dog.rb
index 825e50b..079f1c1 100644
--- a/dog.rb
+++ b/dog.rb
@@ -1,5 +1,5 @@
 class Dog
   def speak
-    puts "woof"
+    puts "ruff"
   end
 end

When git attempts to apply this diff, it needs to do a handful of things:

  1. locate the file
  2. locate the old lines that need to change
  3. replace them with the new lines

This is a deceptively difficult task. Letā€™s review the information that is available in the diff:

  1. the old filename (--- a/dog.rb means it used to be named dog.rb)
  2. the new filename (+++ b/dog.rb means itā€™s still named dog.rb)
  3. the line numbers represented in the diff. We can interpret @@ -1,5 +1,5 @@ as: ā€œin the old version of the file, this diff starts at line 1 and covers 5 lines and in the new version of the file, ā€¦ same!ā€
  4. the lines that were removed (the ones starting with -)
  5. the lines that were added (the ones starting with +)
  6. some contextual lines before and after

That certainly sounds like a lot of information, but consider a scenario. Letā€™s say someone added a one line comment on the main branch. Our diff thinks it applies to lines 1-5, but now it applies to lines 2-6.

The line numbers, then, are not enough to locate the lines that need updating.

Interesting, right?

Ok then, so we can imagine that it scans through the file looking for the line to be removed (puts "woof"), and replaces it with the line to be added (puts "ruff"), whatever line it is on now.

This gives us our first hint of what a merge conflict actually is. If we want to change puts "woof" to puts "ruff", but it no longer even says puts "woof", how exactly are we supposed to do that? The diff cannot be applied.

Fair enough, right?

It isnā€™t that simple, though. We can imagine a scenario where that same line shows up several times in the same file, but only one of them should be updated. So how does git figure out which ones to update?

Look: perhaps itā€™s too late in the blog post to disclose this, but Iā€™m just guessing and speculating about how any of this works. But hereā€™s what I think. I think it looks at those unchanged contextual lines before and after the changed lines.

By default, git-diff produces diffs with three lines of context on either side of a change. With that, git can locate which is the relevant change. Donā€™t just look for the line to change, but also make sure it appears in the expected context.

This points to another kind of conflict, which I hinted at the beginning of this post. This is a conflict which can feel unnecessary, but when I think about how git applies diffs in this way, it makes more sense.

A more interesting example

Letā€™s say on the main branch, thereā€™s a file that looks like this:

def calculate(a, b, c)
  a + b + c
end

Then letā€™s say I check out a feature branch my-feature, and I change that file to look like this:

def calculate(a, b, c)
  a + b * c
end

So far, so good.

However, unbeknownst to me, my colleague also checked out a feature branch and changed the file like so:

def calculate(a, b, c)
  # Tells us how many lollipops there are
  a + b + c
end

And then merged that in.

The commit history looks a bit like this:

* calculate.rb is introduced     -- commit A, on branch main
| \
|  * I update calculate.rb       -- commit B, on branch my-feature
* colleague updates calculate.rb -- commit C, on branch main

We updated different lines, so there shouldnā€™t be a conflict, right?

Wrong! There is a conflict.

And it kind of makes sense now, doesnā€™t it? When git is applying the diff, it relies on those context lines to figure out where in the file to make the change. If the context lines in the diff no longer exactly line up, git cannot be 100% sure itā€™s doing the right thing, and so it raises a flag.

And I suppose all I can say isā€¦ fair enough!

How to watch tennis

March 27, 2021

I like to watch tennis, but I had to learn what to pay attention to. Here are some of those things. This is all pretty basic stuff, and Iā€™m not at all an expert, etc, etc. Iā€™ll assume you know the basic rules.

Actually watch

While the ball is in motion, tennis audiences are silent. They will erupt in uncontained screams and cheers, but only at the appropriate times: when a point has been won. This means you actually need to watch. If youā€™re on your phone and youā€™re waiting for some audience cues to tell you when something exciting is happening and you should pay attention, youā€™re going to miss everything.

What do you see?

Itā€™s two people hitting a ball back and forth, adversarially?

Yes.

Gladiators

Itā€™s also very high drama. Two players enter the tennis court alone, carrying their own gear, and enter into a battle of wills, endurance, resolve, and skill, and push each other to their limits, while thousands of people cheer and boo. At the end, one slinks off, carrying their own bag into the locker room, defeated.

The other survived.

Grand slams

The tennis season is super long, itā€™s basically the whole year, and there are events pretty much all the time. As a tennis fan, what should you actually watch?

Watch the Grand Slams. Theyā€™re the four biggest tournaments each year. Thereā€™s the most prize money, so everybody shows up. And because theyā€™re in this special class of their own, people pay attention to how many Grand Slams a player has won, and thatā€™s the measure of who are the very best players.

The grand slams are:

  1. The Australian Open. It happens in January. Players are well-rested after their holiday break. Itā€™s summer there, which is fun. They call it ā€œthe happy slamā€. Itā€™s played on hard courts.
  2. The French Open (aka Roland-Garros). It happens in May. Itā€™s played on clay. On the menā€™s side, Rafael Nadal always wins, because he is ā€œthe king of clayā€.
  3. Wimbledon. It happens in July, in London. Itā€™s played on grass. Everyone dresses in all white. Itā€™s a bit retro.
  4. The US Open. It happens in August in Queens, New York. Itā€™s played on hard courts. Iā€™ve gone to this one a few times.

If you arenā€™t sure where to start, just wait until the next one of these and dive in.

Where do I watch?

I mostly watch on Tennis Channel Everywhere a streaming site that I pay for. During Grand Slams, some of the matches are only streaming on ESPN, so I might temporarily subscribe to cable for a month to get those matches, too.

How do I enjoy a tournament?

At the start of a tennis tournament, there may be something like 128 players in the draw. Youā€™re not gonna know who most of them are, but if you stick around youā€™ll start learning some names. In the early rounds, just pick some matches at random and watch them. Because of the way brackets get seeded, all of the top players will be paired up against bottom players and are fairly likely to advance, and itā€™s not that fun to watch somebody get clobbered. Itā€™s probably going to be more fun to watch some of the middle players who got paired up against each other, who are more evenly matched. Pick someone and root for them. If theyā€™re both boring, just switch to another match. The menā€™s and womenā€™s events run at the same time, which means that thereā€™s really quite a lot of matches on at the beginning of an event. Try to find someone you like rooting for and stick with them for the whole match.

Each round, half of the players are knocked out. Itā€™s vicious, thrilling, efficient.

Storylines start to emerge. Someone seems unstoppable. Someone no one has heard of has made it to round three. Someone sustained an injury and we wonder how itā€™ll affect them in their next match. Last yearā€™s finalists are on a collision course for a rematch in round four!

Etc.

Pick the story that resonates with you. Root for someone to go all the way. Believe that they will. See what happens for you emotionally, if anything.

Get comfortable

Sometimes tennis matches are really long. Like itā€™s not that weird for a match to be five or six hours long. You can sometimes take a break from a match, watch a movie, and come back to catch the end.

Womenā€™s matches are basically always three sets, while menā€™s matches are sometimes three and other times five, depending on the particular tournament.

A typical set takes anywhere from 30-90 minutes depending on factors like how many games there are (a 7-6 set takes a lot longer than a 6-0 one, of course), how long the players are taking between serves, how long the rallies are, how many deuces there are (you have to win by two, so individual games can technically go on forever), and whether there are any medical timeouts.

Some tournaments use a tie breaker in the final set, while others let the final set go on indefinitely, until someone wins by two games.

There are perennial debates about whether they should downsize all five set match events to three sets, which are less grueling for the player and audience. No one can agree. Part of the sport is stamina, and some are loath to decrease that element.

Youā€™ll know youā€™re a lost cause when you start wishing matches were longer.

Enjoy the crescendos

The most exciting moments in a tennis match are at the end of sets, especially sets which could decide the match. Iā€™m reminded of that Seinfeld bit about muffin tops. Why donā€™t they make the whole match out of the exciting ending?

Well, in tennis, they kind of do. In a normal sport, thereā€™s only one exciting ending. In tennis thereā€™s as many as five, if itā€™s a five set match.

True, after each set they reset and start over, and that can be a little bit boring. But it can be fun to pay attention at the start of the new set. The person who just lost the last set, do they seem defeated or do they seem pissed off and full of resolve? The person who just won, can they keep up the momentum, or are they acting like theyā€™re just happy they were able to win one and they can go home now?

Are we on serve?

One thing I didnā€™t realize when I first started watching tennis is that for each game, one of the players is ā€œsupposedā€ to win, and thatā€™s the player whoā€™s serving. In each game, only one player is serving, and they alternate. If the serving player wins each game, you will hear the commentator say ā€œthey are on serveā€. When the set is on serve, the score will move like this:

  • 0-0
  • 1-0
  • 1-1
  • 2-1
  • 2-2
  • 3-2
  • 3-3
  • 4-3
  • 4-4
  • 5-4
  • 5-5
  • 6-6
  • And then theyā€™ll play a special tiebreaker game to determine who wins the set

If thatā€™s whatā€™s happening, it means that the players are well-matched, and youā€™re building to that dizzying crescendo of the set tiebreaker.

If the serving player loses a game, thatā€™s called ā€œgetting brokenā€, and it puts them on a course toward losing the set. We can imagine a sequence like this:

  • 0-0
  • 1-0
  • 2-0 (a break of service!)
  • 3-0
  • 3-1
  • 4-1
  • 4-2
  • 5-2
  • 5-3
  • 6-3

In that sequence, there is only one break, but the first player will take the set 6-3. In a set like this, itā€™s pretty clear after the second game which player is on course to win the set. That can still be very exciting, though, because it puts a lot of pressure on the losing player to right the ship. Every single game they serve, they must win and they need to ā€œbreak backā€ to ā€œget back on serveā€. And even if they do that, which is not easy, then they still need to break again to get ahead, or try their luck in a tiebreaker.

If a player goes up by two breaks, thereā€™s not a ton of suspense. Itā€™s so hard to come back from being down two breaks. Soā€¦ when someone does come back from two breaks, you would not believe how exciting it is. When you, as the spectator, have given up on the player, but the player has not given up, it teaches you something.

Whoā€™s your favorite chair umpire?

The chair umpire sits in a big chair and directs the flow of the match. Youā€™ll hear their voice throughout the match, doing things like:

  • impassively declaring the score of the current game
  • stating that a serve hit the net, and the serving player will need to serve again
  • acknowledging when a player has challenged a call (e.g. the ball was called out but they really think it was in)
  • issuing warnings for taking too long to serve or cursing
  • acknowledging that a player is taking a medical time out

The tours have a stable of chair umpires who they re-use all the time. The die-hard fans know their names and have favorites. Some of them have legendary voices. Thereā€™s one guy who says ā€œdeuceā€ with such abundant gravitas that you almost wonder if heā€™s joking.

I donā€™t know any of their names, but I think Iā€™ll get there.

Watch out for the fist pumps

The universal tennis celebration is the fist pump. If someone wins a long rally they might let out a furtive little fist pump If someone wins a set, you know that fist is pumping above their head. Absolutely nobody pumps their fist better than Rafael Nadal. There are YouTube compilations. Itā€™s a thrill. I encourage you to adopt this habit in your day-to-day life.

Another common celebration is to scream something like:

  1. ā€œCome on!ā€
  2. ā€œVamos!ā€
  3. ā€œLetā€™s go!ā€

These are all also great, the more guttural the better.

ā€œNew balls, pleaseā€

During a match, thereā€™s a finite set of balls that theyā€™re playing with. Youā€™ll notice that there are a number of ā€œball kidsā€ on court. After each point, a ball kid will scurry after the ball and grab it, then get back into position. At the next opportunity, when the next point ends, theyā€™ll roll that ball down the court in the direction of the player whoā€™s serving. At that end, there are two dedicated ball kids who are accumulating all of the balls, so that theyā€™re ready to feed them to the serving player.

The players really wallop these balls, and they pretty quickly lose their bounciness. After every X games, the chair umpire will say ā€œnew balls, pleaseā€, at which point they replace the dead ones with some fresh ones. The commentators will often act like this has a big impact on the vibe of the gameplay but I honestly have never noticed this.

Geography

It really matters where on the court the player is standing. Some players will stand way behind the baseline. This can be great: if their opponent hits the ball all the way to the left or all the way to the right, they have more time to get to it. This can be bad: their opponent can hit a ā€œdrop shotā€; if they pop the ball just over the net, thereā€™s no way to make it in time.

Some players like to work their way toward the net. This can be great: from the net, you can smash the ball into the ground, sending it flying out of your opponentā€™s reach. This can be bad: your opponent can make a ā€œpassing shotā€ where they send the ball flying past you, and because youā€™re so close to them, you need to have lightning reflexes to actually return it; or, they can make a ā€œlobā€ where they hit the ball in a high arc over your head, forcing you to try and chase it down, staggering backwards, eyes peering into the sun.

Nothing makes me happier than watching a beautiful lob float over someoneā€™s head and drop right on the edge of the baseline.

Running ragged

It can be instructive to pay attention to which player is running more. Youā€™ll often see one player standing stock still in the middle of the court around the baseline, hitting the ball left, then right, then left, then right, forcing their opponent to run back and forth. Which one would you rather be?

If youā€™re just looking to get a quick sense of which player is ā€œin controlā€, pay attention to who is moving more.

Second serves

There are very specific rules for what makes a valid serve. If your serve isnā€™t valid, you get to try again. If your second serve isnā€™t valid, you just lose the point, which is called ā€œdouble faultingā€.

Thatā€™s all clear enough, but what I didnā€™t realize until I started watching tennis is that this system incentivizes players to serve in a particular way. Basically: on your first serve, players take more risk and on second serves they take less risk. A riskier serve is one where youā€™re hitting it harder, or aiming for the edges of the service box, making it more likely that you wham the ball right into the net, or out of bounds. Those riskier serves are going to be harder to return, so youā€™re more likely to win that point if you get that sweet serve in. And if you donā€™t, who cares, you just get to try again. The second serve has higher stakes, because you can actually lose the point if you mess up twice. So a typical ā€œsecond serveā€ is much slower, and itā€™s aiming for right square in the middle of the service box. These are going to be much easier to return, but at least you didnā€™t double fault.

Some players make the calculated choice to always go all out and do ā€œtwo first servesā€. With this strategy, they will double fault sometimes, but theyā€™ll never give their opponent something easy to work with. If theyā€™re consistent enough, that can be a smart calculation. Players with very consistent, aggressive serves are sometimes called ā€œservebotsā€.

If one player is making more of their first serves in than the other player, thatā€™s a big advantage, and something worth paying attention to.

Tennis twitter

One of the best parts of following tennis is tennis twitter. I have a list with ~75 people on it that I will check in on during tournaments to know if thereā€™s an exciting match I should be watching or a storyline brewing I should be aware of.

Some of the active/informative/fun ones I follow, if you want to bootstrap your own tennis twitter:

Other media

If you want more tennis media to stay up-to-date, I recommend these:

Wrap up

In conclusion, tennis is great. Itā€™s also fun to play it. But thatā€™s maybe another post.

Making FactoryBot.lint verbose

February 10, 2021

Hereā€™s a quick blog post about a specific thing (making FactoryBot.lint more verbose) but actually, secretly, about a more general thing (taking advantage of Rubyā€™s flexibility to bend the universe to your will). Letā€™s start with the specific thing and then come back around to the general thing.

If you use Rails, thereā€™s a good chance you use FactoryBot to help you write your tests. The library enables you to define ā€œfactoriesā€ for the models in your system with sensible default values.

FactoryBot has a built-in linter, which you can run as part of your CI build. It will try to identify any factory definitions which are faulty.

At work, we run this as a Circle CI job. It almost always passes, but every now and then it catches something, so we keep it around.

Recently, it started failing occasionally, but not because of anything wrong with our factories. Instead, it was failing because it was justā€¦ slow. Circle CI is happy to run things like this for you, but it gets antsy when something is running for a while and printing no output. Is it stuck? Is it going to run forever? If something is running for 10 minutes with no output, Circle CI just kills it.

Our factory linter apparently takes ten minutes now, we learned.

So, what to do about that?

Per that support article, one option is to just bump up the timeout. Thatā€™s easy enough. We could tell Circle CI to wait 15 minutes. In a year or two, maybe weā€™ll need to bump it up again, assuming such pedestrian concerns continue to dog us then, while we drive around in our flying cars.

Another option would be to just stop running it. Itā€™s useful-ish but not essential. Thatā€™s easy enough.

Another option would be to configure the linter to print verbose output while itā€™s running. If we could do that, then weā€™d get multiple benefits: first of all, Circle CI would be satisfied that it is not stuck, and that it is making progress, and that it might eventually finish, even if it takes more than ten minutes; but also, having some output might be interesting and useful, no? Hmm. I pulled up the FactoryBot docs and saw an option verbose: true, but it didnā€™t seem to be what I wanted:

Verbose linting will include full backtraces for each error, which can be helpful for debugging

I want it to print output even when there are no errors. I didnā€™t see anything like that.

Imagine a ruby file with this stuff in it:

require 'factory_bot'

class Dog
  attr_accessor :name

  def save!
    sleep 4
    true
  end
end

class Band
  attr_accessor :name

  def save!
    sleep 4
    true
  end
end

FactoryBot.define do
  factory :dog, class: Dog do
    name { 'Oiva' }
  end

  factory :band, class: Band do
    name { 'Bear Vs. Shark' }
    albums { 2 }
  end
end

factories = FactoryBot.factories
FactoryBot.lint(factories)

Thereā€™s actually a bug in our Band factory: it references an attribute called albums which does not exist in our model code. The linter will catch this.

Looking at that last line, it looks like we just pass in the list of factories, and then presumably it will loop over that list and check them one-by-one.

Looping over things is a really common thing in Ruby. Anything that you can loop over is considered ā€œenumerableā€. Arrays are enumerable. Hashes are enumerable. When you query a database and get back some number of rows, those are enumerable.

A list of factories is enumerable. Hmm.

Letā€™s try writing our own enumerable class, to wrap our list of factories. Weā€™ll call it ChattyList. Itā€™ll be a list, but when you loop over it, itā€™ll chatter away about each item as they go by.

In general, if youā€™re calling a method and passing in one enumerable thing, it would also be fine to pass in some other enumerable thing. Itā€™s just going to call each on it, or reduce, or something like that from the Enumerable module.

class ChattyList
  include Enumerable

  def initialize(items, before_message:, after_message:, logger:)
    @items = items
    @before_message = before_message
    @after_message = after_message
    @logger = logger
  end

  def each
    @items.each do |item|
      @logger.info @before_message.call(item)
      yield item
      @logger.info @after_message.call(item)
    end
  end
end

factories = ChattyList.new(
  FactoryBot.factories,
  logger: Logger.new($stdout),
  before_message: -> (factory) { "Linting #{factory.name}" },
  after_message: -> (factory) { "Linted #{factory.name}" },
)

FactoryBot.lint(factories)

When I run the script, the output looks like this:

I, [2021-02-10T22:43:03.676359 #57462]  INFO -- : Linting dog
I, [2021-02-10T22:43:07.678616 #57462]  INFO -- : Linted dog
I, [2021-02-10T22:43:07.678712 #57462]  INFO -- : Linting band
I, [2021-02-10T22:43:07.679373 #57462]  INFO -- : Linted band
Traceback (most recent call last):
        2: from app.rb:59:in `<main>'
        1: from /Users/max.jacobson/.gem/ruby/2.6.6/gems/factory_bot-6.1.0/lib/factory_bot.rb:70:in `lint'
/Users/max.jacobson/.gem/ruby/2.6.6/gems/factory_bot-6.1.0/lib/factory_bot/linter.rb:13:in `lint!': The following factories are invalid: (FactoryBot::InvalidFactoryError)

* band - undefined method `albums=' for #<Band:0x00007ff7b9022890 @name="Bear Vs. Shark"> (NoMethodError)

Nice! As the linter chugs thru the factories, it prints out its progress. With this, Circle CI will see that progress is happening and wonā€™t decide to kill the job. This option wasnā€™t offered by the library, but that doesnā€™t have to stop us. Isnā€™t that fun?

By the way: that might be a good option to add to FactoryBot! Feel free, if youā€™re reading this, to take that idea.