Understanding Revsets for a Better JJ Log Output

In git you can do something like HEAD~ to refer to the parent commit of HEAD. Mercurial has a similar feature called revsets which JJ took inspiration from (including the name).

The revset language is a declarative query language—not unlike SQL—that lets you specify a set of revisions (a revset) that match certain criteria. It ends up looking more like set operations than SQL, but the idea is similar. In JJ you can use @ to mean “the current commit”, or mine() to mean “all the commits that I authored”, or trunk() to mean “the base branch that code will be merged into”.

That’s getting a little bit ahead of ourselves. Why did I go down a rabbit hole of learning about revsets in the first place? Well, one of the many nice things about JJ is that the default output of the log command is to show just the stuff you care about, not the full history. For example here’s the current state of the repo for my website:

$ jj log
@  t Will Richardson now e
│  Add post about revsets
◆  k Will Richardson 4 weeks ago main HEAD@git 5
│  photo post
~

I don’t care about the contents of the other 300+ commits in the repo most of the time. Showing just the stuff that hasn’t been merged into main is great.

However, if you run jj log on some other repos, things aren’t quite as neat. Take the Crystal language repo as an example. The output of jj log is over 2000 lines! That doesn’t include any of the 15,000 commits that have been merged into the main branch, that’s just JJ showing commits that we might want to work on.

What causes this huge output is the fact that the Crystal repo has 93 branches, with commits that haven’t been merged into the main branch, including long-lived branches that have years of work that isn’t in master1. The default set of revisions that JJ logs is anything yet to be merged into trunk()—the main branch. This is a sensible default as it avoids a situation where commits become invisible to the user—they’re always either in the main branch, or they’re shown in jj log.

However what I want to show is just commits that I’ve written that haven’t been merged, and ignore all these other branches. To do that we need to understand some revsets.

If you want to learn the revset language properly, you should read the revset language documentation, but I’m going to walk through how I settled on my default log output.

[revsets]
log = '@ | ancestors(trunk()..(visible_heads() & mine()), 2) | trunk()'

That’s the config that I ended up settling on.

I wanted the log to show all the commit branches (branches as in the sense of a tree structure, not branches as in git branches) that I had authored that weren’t merged into the main branch.

visible_heads() gives me all the leaf nodes in the repository. In a git repo this would basically be all the feature branches that people had pushed to the remote but not yet had their pull request merged. Branches like main or master usually wouldn’t be in this list as there will almost certainly be commits somewhere that build on top of it.

In the Crystal repo this gives us a huge output, let’s peek at just the top:

$ jj log -r 'visible_heads()' | head -n 20
@  x Will Richardson 14 minutes ago c
│  (no description set)
~

◆  nsxyl Johannes Müller 2 days ago changelog/1.13.2@origin 7214
│  Add changelog for 1.13.2
~

◆  wvqv Johannes Müller 2 days ago revert-14878-docs-generator-dont-mention-nodoc-types@origin a17af
│  Revert "Fix: Don't link to undocumented types in API docs (#14878)"
~

◆  xxkqm renovate[bot] 3 days ago renovate/gh-actions@origin d6ce
│  Update actions/checkout action to v4
~

◆  rolu Johannes Müller 3 weeks ago infra/macos-14@origin b479
│  verbose spec output
~

There’s my working copy commit, and then a bunch of in-flight work like the changelist for version 1.13.2. Each of these heads are a single leaf commit, and JJ dutifully only prints the commit and omits their parent(s).

If we want to show a bit more context, we can use the ancestors() function (yeah there are functions in revsets) to get the parents of each of the heads:

$ jj log -r 'ancestors(visible_heads(), 1)'
@  x Will Richardson 21 minutes ago c
│  (no description set)
○  p Will Richardson 21 minutes ago HEAD@git 6
│  Allow serving index.html from StaticFileHander
~  (elided revisions)
│ ◆  nsxyl Johannes Müller 2 days ago changelog/1.13.2@origin 7214
├─╯  Add changelog for 1.13.2
◆  okrx Quinton Miller 2 days ago fa02
│  Support LLVM OrcV2 codegen specs (#14886)
~  (elided revisions)
│ ◆  wvqv Johannes Müller 2 days ago revert-14878-docs-generator-dont-mention-nodoc-types@origin a17af
├─╯  Revert "Fix: Don't link to undocumented types in API docs (#14878)"
◆  vrny Johannes Müller 3 days ago 93ac1
│  Refactor interpreter stack code to avoid duplicate macro expansion (#14876)
~  (elided revisions)
│ ◆  xxkqm renovate[bot] 3 days ago renovate/gh-actions@origin d6ce
├─╯  Update actions/checkout action to v4
◆  tnzt Johannes Müller 3 days ago f0fe
│  [CI] Update GitHub runner to `macos-14` (#14833)
~  (elided revisions)

JJ shows the heads, and then the commit immediately before the head. If you look at the full log output you can see that it starts linking up commits because some of them share the same parent—and it won’t print the same commit twice.

The default JJ log output uses the ancestors() function to show a bit more context in the log, rather than just showing the un-merged commits. This typically results in the tip of the main branch being visible in the log, which is nice as you can see where you’ve synced your repo to.

What we actually want is to show the commits that we’ve made, which we can access with the mine() function. This uses the configured author email to filter the commits. I can log the commits that I’ve made to Crystal with jj log -r 'mine()'.

This can be pieced together now using a few of the revset operators. I can find all the commits that I’ve authored that aren’t in the main branch by subtracting all the commits in the main branch from all my commits: mine() ~ ::trunk(). The :: prefix operator gives all the ancestors up until the revision, so ::trunk() gives me everything until the last commit on the main branch. The ~ operator subtracts the right hand side from the left hand side. I basically just think of these as set operations, which I guess they’re not quite but it gets me most of the way there.

That’s already a pretty nice log output, we could wrap that in ancestors() or just append | trunk() to show the tip of the main branch to get that extra bit of context.

My revset is slightly different, I use visible_heads() & mine() to get the leaf commits that I have authored, and then get all the commits between trunk() and those with the .. operator: trunk()..(visible_heads() & mine()). I then get the ancestors() of those to show additional context.

Using the union (|) operator with both trunk() and @ ensures that the main branch and current revision are always visible in the log output, even if I’ve got myself into a serious pickle—perhaps if I’ve pulled someone else’s un-merged branch and have checked out one of their commits.

I’ve had this in my config for a few months now and it seems to be working well. I have an alias that will log everything jj log -r 'all()' so if I do mess something up, I can still find my commit there. Or I can always just remove the revsets.log option from my config file and go back to the standard output. If you want this output, pop this in your .jjconfig.toml:

[revsets]
log = '@ | ancestors(trunk()..(visible_heads() & mine()), 2) | trunk()'
  1. There’s a branch named “toilet” with one commit from 4 years ago adding more calls to IO#flush throughout the standard library. 


A New Home for My Photos

A few weeks ago Eugen Rochko (creator of Mastodon) published a new photography portfolio website. This quickly made me want to improve my own photography website. Eugen’s site puts more of a focus on small groups of photos, and makes the photo metadata prominent—Eugen shoots on film so this is information about the camera as well as the type of film used and where it was developed.

My photo website was forked from this site a few years ago. The purpose of the design is to make it seem like it’s part of the same site—the colours and header match exactly. It’s built mostly around a photo grid, replicating the view you’d get on Instagram, with square thumbnails for all pictures. There isn’t really anywhere to show additional information or commentary about the photo, and the fact that the style matches this site means I have to keep the two repositories in sync.1

screenshot of my old photos website

That’s my old photos website, with an Instagram-style grid layout.

Over the course of about a day I pulled together a new site that is less of a copy of Instagram and more of a photo journal. You can see it live here.

screenshot of my new photos website

The current state of my new photos website.

I also took inspiration from Sebastiaan de With’s photo blog. I am perpetually jealous of the stunning photos he shoots on his phone.

Probably the biggest pain was my decision to not have titles for posts. They can have a location, but otherwise they are just identified by the date. Whenever I show a post I display a heading built from the location, date, and number of photos. The format of this is changed depending on how many photos there are, and whether there is a location set on the post. The complexity in implementation is worth it for me, as it reduces the overhead to publishing something—photos can be added with no additional commentary.

The photos linked to a single post are defined as a list in the Jekyll frontmatter. This includes information pulled from the EXIF metadata—camera, lens, aperture, shutter speed, and focal length. If any of these are omitted, they will be gracefully omitted on the website. The actual body of the post allows me to write as much or as little as I want.

The layout is fairly simple—it’s mostly just a vertical stack of elements. I’m no CSS wizard. The one trick that I think is worthwhile is setting the max-height of an image to 80vh so you’ll always be able to see the whole image, no matter the size of your browser window. I decided that any kind of fancy flex-box-y layout wasn’t really worth it since most people would be looking at this on a phone, where you only want to show one image at a time anyway.

I put the same amount of care into the RSS feed as I did into the main website. Since the photos are defined in the frontmatter and not in the body of the post, it’s easy to render them differently for the feed with fewer HTML tags, as well as formatting the pseudo-title (built from the date and location, remember) for the plaintext title of the feed item. My hope here is that if you follow the site via RSS (or JSON Feed!) you’ll get as good an experience as if you were viewing the web page.

There’s also a little Ruby script that reads the EXIF data using ImageMagick, creates the Markdown files with frontmatter, and recompresses the images. Who knows how I’ll end up publishing photos in the long term, but for now the script will have to do.

There are 80 posts and 200 photos already on there, a different collection to what I had on my previous site. Perhaps you should go and have a look.

  1. If I did GitHub Pages with a custom build action, perhaps I could turn this into a theme that could be properly shared between the two sites. 


Lazy Jekyll Hacks for More Accurate Publication Times

So here’s something terrible that I’ve just done. Enthusiastic readers with an attention to detail (especially those who read the RSS feed) will notice that all my posts are published at midnight. This is actually a lie—I do usually write posts late at night, but I don’t carefully wait until exactly midnight to publish them. The real reason is that I don’t include a published time on my posts—just a date—so if your feed reader wants to show a time, it’ll show 00:00.

Of course I could just add a time to each post, but that requires effort. The time only really matters for the first 24 hours or so after publication to make posts from multiple websites appear in order in a feed reader. After that, only the date matters, since I almost never publish multiple posts in one day.

Instead of adding the time in manually, I’ve made a lower-effort solution to do some terrible things with Jekyll to fake it.

Jekyll includes a site.time variable which is the current time when the site is built. Anywhere that uses this will be updated every time the site is regenerated. I use this in my feeds for the “last updated” fields, and in the footer to put $CURRENT_YEAR in the copyright notice.

What I’ve now done is when I need a post time, I check if the post was published on the same day as the site was built. If it was, I use the site build time instead of the post date. So when I add a new post, the site is built and that build time is used in all post metadata. When feed readers come along they will have an accurate publication time to show to users.

Of course if I push another change on the same day that doesn’t update the post, the publication date will jump ahead, and when I push a change the following day, the publication date will be truncated back to just being midnight. I did say it was a hack.

I don’t think this will cause problems for feed readers, as they should be using the guid (for RSS) or id (for JSON Feed) fields to identify posts. The most realistic failure most I can see is that the post jumps up or down a few places in a list of posts when the feed is re-fetched, but I don’t think that’s any worse than having the post appear out-of-order for everyone all the time.


Upsetting the Apricot Cart

Imagine for a minute that I had a pair of apricot trees in my garden. I don’t actually have a garden, so you’ll have to imagine that I have one of those too. I love a nice fresh ripe apricot. However, I can’t eat every single apricot that comes off the trees—there are just too many! There aren’t enough apricots to start an apricot-selling business, plus I’ve got a job anyway. Instead I setup a stall in the street and give away my apricots to anyone passing by that shares my love of stone fruits. After a while word gets around and people in the neighbourhood know that they can get some apricots from Will’s stall, and they’ll drop by to see if I’ve got any to give away.

Someone puts up a sign at the end of the street: “Tasty Free Apricots from Will’s Tree!”. It’s not what I would’ve done, but to be fair it is a bit difficult to find my stall, so making it easier to find is convenient for me too. I’m happy that more people appreciate the apricots, and I enjoy talking about fruit with the people that stop by. Having a few more people turn up that like apricots but didn’t otherwise know about my stall is nice.

A little later, someone notices my stall and the boxes of fruit that I have sitting behind it. They take a box of apricots and give them away at a local market.

Multiple people have the same idea—it entices more people to their stand if they’ve got apricots to give away. Some of them will tell shoppers where the apricots came from—and where they can find more themselves—but others don’t pass that information on to whoever is attending their stand.

When I find out, I’m a little put out by this. My friends say that the result is the same—the apricots get given away to people that want them, what’s the problem? Wasn’t the whole point of this stall to give away the fruit that I didn’t need?

They don’t understand that while yes, I was giving away the fruit for free so I haven’t lost anything financially. What I have lost is a connection with the people that received the fruit. I don’t get to see that they appreciate it, they don’t come back and tell me about the desert they cooked with the apricots last week. They can’t see the time and effort that I put in to building the stall, or hear about my many apricot recipe recommendations.


Completely unrelated to that imaginary scenario, on this website I share my thoughts, opinions, and projects I’ve been working on. I’m happy to give them away because I have such an abundance of opinions I have to put them somewhere, and I’m self-aware enough to know that no one would be bothered paying for this nonsense. However I write with the hope that readers will appreciate not just the ideas, but the way the ideas are presented, both in how they’re written and how they’re presented on my website.


Merging JJ Repos

So here’s a weird thought: can you merge two repositories in JJ? I have just ended up in a weird state (for unimportant reasons) where I had a remote repo and a local repo that diverged entirely due to me rewriting the commit history. I was going to delete the local copy and re-clone (since the remote was now the source of truth) but I thought “what happens if I add the remote and fetch from it?”, and thought I might as well satisfy my curiosity first.

I added the remote and fetched it, and sure enough it worked without any issues. Since there were no commits in common, I was left with two diverging chains starting at the root commit (an empty commit present at the base of all JJ repos). I could then delete the unwanted chain of commits and get to the state I’d be in if I’d just done a fresh clone.

Or I could do one jj rebase and move all the commits in one chain atop the commits in the other, effectively rebasing one repo on another. This led me to the cursed realisation that I could take two completely unrelated repos and just splice them together, resulting in a repo with the contents and commit histories of both. Of course this will almost certainly result in horrible merge conflicts, but you can do it.


Interoperability Tier List

There is almost nothing built-in to your computer, phone, tablet, etc that allows you to interact directly with someone else’s computer. If you have a photo on your phone, and your sitting next to someone with their phone, the easiest way to transfer the data between those two devices usually involves using someone else’s computer in a datacenter thousands of kilometres away.

In general, the interoperability of physical devices is pretty good. If you’ve got a monitor or TV, you’re almost guaranteed to be able to plug it into your computer with HDMI or DisplayPort. Maybe you’ll need a dongle, but you don’t need to worry about buying a new TV just because you switched to a different game console. The same goes for ethernet, USB, and the 3.5mm headphone jack. However as soon as you cut that cable, things get more limited.

We can think of this as tiers of interoperability:

Starting at the bottom, we have vendor-specific features, tied to a specific hardware or software platform. My favourite example is AirDrop, it’s excellent for proximity-based file transfers, and gets decent speeds because it creates an ad-hoc wifi connection, but it’s only available on Apple hardware. (There are of course projects that have reverse-engineered the protocol, but for the purposes of this post I’m only considering things that are generally available).

Moving up one level gives us software that runs across different platforms because the vendor chooses to develop for multiple platforms. This is what most people would consider “cross platform”, as you can use the software on basically any mainstream hardware/software combination. Most proprietary software falls into this bucket.

Often “cross-platform” software leverages the web to run on less popular (read: non-mobile) OSes. This has the side-effect of allowing it to run on any platform with a compatible browser. Obviously this makes it much easier to access proprietary software from an “unsupported” platform (for example if you use Linux), but often the limitations of web APIs (or lack of investment from the vendor) make this an incomplete experience. You may be able to access the web interface for your favourite cloud storage provider, but it’s unlikely that you can do automatic syncing to a local folder using the web app.

Another half-step up the interoperability ladder is vendor-specific servers that allow for arbitrary cross-platform clients. If you squint, this is how most web applications work—there’s a server controlled by the vendor, and the clients are browsers that can be running on any platform. Similarly you could have a centralised service that allows for third-party clients to use a full-featured API. This is almost what Twitter used to allow (pre-2023) where you could use the service on the web, or via an app of your choice.

The point at which we get practical interoperability is federated services, like email. No two email “users” have to be signed up to the same service, use the same client application, or even have the servers running the same software. There is both a standard API between the servers (SMTP) as well as standard APIs for clients to interact with mail servers (POP3 and IMAP)—although there is of course nothing stopping an email provider from only supporting their own API, as long as they interact with other mail servers using SMTP. Your provider not supporting IMAP has no effect on my ability to use IMAP with my provider.

On a similar level there are SMS, RCS, and the phone system. However they require certain hardware, and are treated differently to email by the operating system. Most OSes don’t allow for replacing the interface used to interact with SMS, RCS, or phone calls, and so even if the standard is open, users don’t get the benefit of being able to swap components out to gain more features or an alternate interface.

If you’d asked me even just a year ago I would have said that the messaging interoperability requirements in the DMA were pointless, but in lieu of mandating a protocol that must be supported by all devices, this is likely the best way to allow for breaking the network effects of social software. I’d love to be able to use a single app for messaging, instead of having to swap between five different apps depending on who I’m talking to. Of course, the actual result of this is yet to emerge.

Where there is an amazing amount of interoperability is networking. Every consumer device that is designed to connect to the internet either supports wifi or ethernet, and has an implementation of the various protocols necessary to send and receive data—allowing most applications to just interact with a higher-level protocol like HTTP. Imagine if this wasn’t the case, and you had to buy a new wifi router when you bought a new device. Or you visited someone and they had an incompatible network, leaving you without a connection.

It’s hard to imagine that would ever happen, since the interoperation is such a huge convenience. Although you don’t have to look very far to see places where there is little to no interoperability: smart home devices are rife with them. I have a handful of LIFX smart lights,1 and they’re basically just an accessory to my phone—and only my phone. LIFX have to integrate with each platform individually, so if you’re not on one of those platforms they are useless, and if you have any kind of heterogeneity in your devices, you’ll have to pick a platform that supports all of them. At this point I think having a smart home in a household that spans multiple platforms—Android and iOS, say—is so impractical you might as well not bother. At least if you want to make use of any vaguely “smart” features and don’t want to be a sysadmin.

In theory Matter will solve the cross-platform problems, but the rollout is happening at a glacial pace and for me will almost certainly require replacing my lights. I can use a years-old wifi router with no problem, but similarly aged smart lights are counting their days until the e-waste bin.

I have to tell you about a burger franchise in Sydney that is not cross-platform. How can burgers have platform dependence? Well, previously you could order takeaway on the web, which I would often do in advance so I could pick up my order on my way home. A few months ago they removed this functionality from the website, instead they would only take orders through their app. So if you didn’t have an iOS device or Android device (with access to the Play Store), you couldn’t make a takeaway order. Why they made this decision is beyond me. I can only assume that they’re trying to get people to install the app as a ploy for more data or high-engagement promotions via push notifications.

Of course what actually happened is that I just phone them up—they’ve still got a phone number—and place an order the old-fashioned way. Or I could just show up and make an order in person.

Devices supporting open standards gives them a longer useful lifetime, especially if you end up changed computing platform at some point. I have a UE Boom 3, and while it does have some platform-specific features, the core functionality of playing audio is just bluetooth. Sadly one of the platform-specific features is that the biggest button (that everyone thinks is the power button) is tied to starting playlists in specific music streaming services that I do not use. But I know which button turns it on, and I can find a playlist myself.

While they’re not quite in the same category, smart speakers that have built-in voice assistants and music streaming will become virtually useless if you switch platforms. Either you must remain using the services supported directly on the device, or use applications that support the proprietary streaming protocol for the device (ie Google Cast or AirPlay). The same is true for video streaming; there is a standard for video streaming (Miracast2), however platform vendors prefer their proprietary protocols.

It’s easy to take the standards that we do have for granted—the idea of worrying about wifi compatibility seems absurd—but for many things there isn’t even the possibility of worrying about compatibility, you just know that it won’t work. It’s worth thinking which tech islands you inhabit, and whether the rising tide of tech advancements is ensuring that it’s never practical for you to migrate. If you switched to Linux, would you just be giving up some convenience, or the ability to be part of social life? At the moment I’d say that you can only really rely on three things: support for networking, a mostly standards-compliant web browser, and the ability to execute mostly arbitrary code.

My barometer for “interoperable standard” as examples for this post are things that you can count on being supported on any device without additional software, and without reverse-engineering a protocol. It’s likely that I’ve got some things wrong, but hopefully the general gist is still somewhat clear.

  1. They’re fine. I wouldn’t really recommend them for all the annoying incompatibility issues I’ve mentioned here, my attitude to smart devices now is that you should only buy things you’d be ok with replacing every time you got a new phone. 

  2. While writing this I learnt about Matter Cast which is another standard for doing video streaming to a TV, seemingly only supported by Amazon at the moment. 


A Critique of Closure Syntaxes

I love a good closure, but not all languages have a good syntax for writing them. What makes a good closure? What does your closure syntax say about your language? Do you call them lambdas, blocks, closures, or anonymous functions? Does Will know how to end this intro?

Go

Let’s start off with something boring. Go does the absolute minimum while still actually allowing closures. You can use the exact same syntax you use for regular functions.

// Here's a regular function
func main() {
  // and here's a closure
  fun := func() {
    println("hello!")
  }
  fun()
}

It’s a function in a function. Can’t really ask for much else, can we? (spoiler: we can).

To write type of a closure (for example to receive it as an argument) also uses the same syntax: func(int32, int32) string is the type of a function that receives two int32 args and returns a string.

Python

Python just goes that extra few centimetres, you can use the same syntax inside a function, but there’s also the lambda keyword as a shortcut for single-expression closures:1

def main():
  def func(a):
    return a + 41
  # can be written as:
  func = lambda a: a + 41

The lambda keyword is a bit verbose for my liking, and Python’s indent-based blocks don’t really lend themselves well to many alternatives that are longer than one expression.

Clojure

Clojure is similar to Python in that it has a fairly mundane shorthand:

; This is a normal function definition
; I included this because not everyone knows Clojure
(defn normal-func [a]
  (+ 41 a))

; And here's a closure
(fn [a] (+ 41 a))

You can pop that fn form as an expression anywhere:

((fn [a] (println a)) "hello!")

This is your brain on Lisp. Parents: talk to your children about Lisp before Paul Graham does.

Closure also has an even shorter form backed by a “reader macro”, which allows for single-expression closures with implicitly named arguments:

; This expression
#(println %1)
; Translates to
(fn [%1] (println %1))

Elixir

Elixir is similar to Clojure in that its “full” function syntax isn’t that much more verbose than the closure syntax:

# This is a function
def some_function(args) do
  args
  |> Enum.map(fn arg ->
    # And that ^^ is a closure
    arg * 2
  end)
end

Closures have all the same pattern-matching, multiple body abilities as regular functions, and since everything in Elixir is immutable, they don’t really “capture” variables in the same way as other languages.

The pattern matching syntax is slightly different as the closure is written as one expression, whereas the full function is written as overloads:

def list_empty?([]) do
  true
end

def list_empty?(_) do
  false
end

list_empty_closure = fn
  [] -> true
  _ -> false
end

Rust

For a fancy modern language, Rust is fairly conservative with its lambda syntax. I guess it makes sense with Rust’s focus on correctness and predictable behaviour, they’re not going to add something to build crazy DSLs—we’ll get to some of those later.

The syntax looks like this:

// With a type-inferred argument, and a single expresssion
my_vec.map(|x| x * 2);
// With a typed argument, and multiple statements
my_vec.map(|x: i32| {
  let result = x * 2;
  result
});

The multi-statement syntax works well since all code blocks in Rust can produce a value by omitting the trailing semicolon on that line (or with a return statement, I’m not too hot with Rust to tell you for sure).

Java

Java’s closures are a pretty horrible hack, but we try not to hold that against them, after all we’re rating the closure syntax, not their implementation.

The syntax comes in three basic forms:

// No arguments, single expression.
() -> println("Hello!");
// Single argument, single expression.
message -> println(message);
// Multiple arguments, multiple statements.
(a, b) -> {
  String c = a + ": " + b;
  return c;
};

This is pretty nice, especially compared to most of Java’s syntax. It’s fairly terse, clear, and doesn’t get mixed up with other parts of the language. -> is a nice token to split the arguments from the lambda body, it doesn’t appear anywhere else in the Java language. Allowing the brackets (both round and curly) to be omitted in certain cases allows this to be really syntactically lightweight in common cases:

List.of(1, 2, 3)
    .filter(x -> x % 2 == 0)
    .map(x -> x * 2)
    .reduce(0, (acc, x) -> acc + x);

The weirdest thing about Java’s lambdas is that they’re the only place where the types of parameters can be omitted, and the compiler will infer them from context. This is obviously a syntax benefit, but it’s odd to break the rules established in the rest of the language—we’ll see this come up again a few times. If you want to be explicit, or the compiler can’t infer types correctly, you can specify the type of arguments:

Function<String, Integer> getLength = (String input) -> input.length;

The biggest difficultly when using lambdas in Java is the fact that you need to understand how they’re implemented, and remember the built-in “functional interfaces” that back them.

For the uninitiated, Java lambdas are just syntactic sugar around anonymous implementations of interfaces with single methods. The most common is Runnable—an interface with a single run() method that takes no arguments and returns no result. Another common one is Supplier<T> which takes no arguments but returns an object of type T. You’ve then got Consumer<T>, which does the opposite, Function<T, R> which does both, and BiFunction<S, T, R because variadic generics are too complicated. So basically:

// This lambda
Runnable r = () -> println("hello!");
// gets translated to
Runnable r = new Runnable() {
  @Override
  public void run() {
    println("hello!");
  }
};

JavaScript

Somewhat appropriately, JavaScript’s closures are pretty similar to Java. The traditional syntax looks like a normal function (without the name):

let closure = function(input) {
  return input * 12;
};
closure(4); // returns 48

And then the new syntax looks just like a Java closure, except with a => instead of ->. It shares the same shortcuts to omit the round brackets if there’s a single argument, and omit the curly brackets if there’s a single expression in the body.

// No arguments, single expression.
() => console.log("Hello!");
// Single argument, single expression.
message => console.log(message);
// Multiple arguments, multiple statements.
(a, b) => {
  let c = a + ": " + b;
  return c;
};

Just like Java, the short form doesn’t bind to this in the closure body, but if you write out the function (or anonymous class) in full, it will bind this.

C++

C++ does the classic C++ thing of using all the different symbols in one go.

auto closure = [&capture](int argument) {
  return argument * 2;
};

The obvious difference is that unlike most other languages, C++ won’t automatically capture variables for you. You need to specify how variables are closed over, either by reference with &, by pointer with *, or by value with no prefix. Most of the time I’d just capture everything by reference with [&], since for functions that don’t store the closure and just call it before they return, you’re not likely to run into retention issues.

It’s a messy syntax, but it gets the job done and fits in with the requirements of C++.

Swift

Someone found Swift’s syntax to be confusing enough that they registered fuckingclosuresyntax.com to list all the different spellings of Swift closures.

This is the point in the syntax list where the syntaxes flip from being good at defining a standalone value, to instead being better for passing as an argument for a function. You can always do this in Swift:

let myClosure = { (arg: Int) in
  print("the arg: \(arg)")
}
myClosure(1234)

The syntax starting with a curly brace is a bit weird, as in many C-inspired languages, that is used to define a scope. Swift actually forbids you from having an unused closure expression, I assume due to the fact that people might incorrectly assume that they’re creating scopes.

What Swift really wants you to do is use a trailing closure where possible:

// Boring
let mutator: String -> String  = { element in
  element.reversed()
}
myList.map(mutator)

// New and exciting, with trailing closure
myList.map { element in
  element.reversed()
}

I think that all languages should allow trailing closures, it makes closures feel like a first-class part of the language, not something that’s bolted on from spare parts, like it is in Java.

Swift does have one (mostly understandable) messy syntax: @escaping. If your closure is going to outlive the function it’s passed to, you need to annotate the argument with @escaping. Nothing wrong with that, but it looks a bit weird to have something that’s basically a language keyword look like it’s an arbitrary annotation.

The other annotation-looking thing you might come across is @autoclosure, which is absolutely awesome and I don’t know why more languages don’t have this. It lets you change the evaluation order of expressions passed in as function arguments. Every other language is boring and evaluates them in the order they’re listed at the call site, but Swift lets you change that so the arguments are evaluated whenever you want.

func test(message: @autoclosure () -> ()) {
  print("Hello")
  message()
}

test(message: print("world"))

This will print “Hello” and then “world”. To get this behaviour with any other language you’d have to wrap print("hello") in the appropriate lambda syntax at every call site.

Clearest win for @autoclosure is logging libraries: you can use all the nice—and expensive—string interpolation in what looks like a normal method call, but you don’t have to evaluate it if logging isn’t enabled. Other languages like Crystal make the block syntax part of the logging API to get this same behaviour.

class Logger
  static func log(msg: @autoclosure () -> String) {
    if loggingEnabled {
      // The message is only built if we're actually going to use it
      logInternal(msg())
    }
  }
}
// That debug info will only be calculated when we're actually going to use it
Logger.log("I just did \(getExpensiveDebugInformation())")
// Other languages use an explicit closure
Logger.log {
  "I just did \(getExpensiveDebugInformation())"
}

This is so neat that the builtin && and || operators are implemented with @autoclosure to support short-circuiting (where the right hand side of the expression is only evaluated depending on the result of the left). I learnt this from this post but you can go and look at the code yourself.2

Where most languages would accept that trailing closure syntax is best limited to a single block, and to use a different pattern if you need to pass multiple blocks, Swift doesn’t know how to say no. You can pass multiple trailing closures:

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

The second closure is passed as onFailure to loadPicture. At this point I would probably opt for a builder pattern, where you set callbacks and then execute the request. That would have the disadvantage of all the closures being marked as @escaping—since they would outlive the method on the builder that set them—which I think has a performance penalty.

This is where you consider what the actual problem you’re trying to solve is, and realise that setting a bunch of callbacks is not productive and instead you should support concurrency in your language so that loadPicture can just suspend and return a result when it’s ready, removing the need to write closures entirely.

Kotlin

The syntax in Kotlin is very similar to Swift:

myList.map { value -> value * 2 }

The in keyword is replaced by -> , and they also allow trailing closures.

Kotlin goes beyond what Swift has with receiver blocks. Instead of the closure being evaluated in the lexical scope of where it is written, it can be evaluated within the scope of another object. This is the building block that backs the Flow API, a lot of the couroutines helpers, and a whole bunch more.

To a developer, this basically gives the impression that within a block, you’ve got access to additional functions and variables that aren’t accessible outside. In the structured concurrency API this means you can only call async inside of a coroutineScope block, or similar.

Receiver blocks can definitely get confusing—it’s basically breaking the method-lookup pattern that is almost the same across every language—but it gives the ability for libraries to make APIs that look like they’re part of the language, which is something that I am a fan of.

What I don’t like about Kotlin’s closures is that if you want to return a value from them, the return must be annotated with the name of the function that is receiving the closure. For example:

myList.map { num ->
  // Not allowed
  return num * 2
}
myList.map { num ->
  // Gotta do this
  return@map num * 2
}

I understand why you’d do this—heavy use of the block syntax can make it confusing where you’re returning from—but it does mean that you end up structuring your code a bit differently to avoid having to write out return@coroutineScope too many times.

Kotlin does also allow using the normal method-definition syntax to create a closure, so this is a totally valid Kotlin program:

println((fun(a: String) = "$a world!")("howdy"))
// => howdy world!

Crystal

Crystal doesn’t really have a closure syntax that stands alone, instead you use the block syntax to pass some code to a function that returns a callable closure.

closure = Proc(String, Nil).new do |name|
  puts "Hello #{name}!"
end

closure.call "world"

The do |name| ... end syntax can’t be used anywhere other than being passed as an argument to a function. The Proc type just wraps that block up in a thing you can call later. The thing that always catches me out is the generic types on Proc—a single type is the return, if you give multiple types then the last one listed is the return type and the rest are the argument types. It’s just arguments*, return, but somehow I forget that every time I write a Proc. The good thing is that you rarely construct a Proc in most Crystal code.

Just like Kotlin (and Ruby), Crystal also allows for evaluating blocks in a different lexical scope than the one they are defined in:

struct String
  def with_me(&block)
    with self yield
  end
end

"a string".with_me do
  # this calls #size and #upcase on "a string"
  puts size, upcase
end

I’ve used this to make a simple HTML builder for my status page library.

Ruby

Ruby works the same way as Crystal, except they’ve also got the “stabby lambda” syntax:

closure = ->(args) {
  puts args
}

Which I think looks a bit weird, and prefer to just use the proc helper method that works like Proc(T).new in Crystal:

closure = proc do |args|
  puts args
end

Things get a little bit weird when you consider how trailing blocks work with function calls that omit brackets:

method_call argument do |a|
  puts a
end
# Does that code work like this:
method_call(argument) do |a|
  puts a
end
# Or like this:
method_call(argument() do |a|
  puts a
end)

More specifically: is the block passed to method_call, or to argument? Every Ruby programmer probably knows this intuitively, and this is where the curly brackets come in:

# The block is passed to `argument`
method_call argument { |a|
  puts a
}
# The block is passed to `method_call`
method_call argument do |a|
  puts a
end

I’d explain this in term of associative-ness, but I can never remember which is which. The curly brackets will stick to the function call closest to them, do ... end will stick to the outermost call. This means you can do:

method_call method_call { |a| puts a } do |a|
  puts a
end

And each method_call will receive a block each. It’s just up to your good taste to avoid writing code like this.

There’s a pretty clear split here between languages with closures that are better for using as values, and others with closures that are better as arguments to functions. Java and JavaScript’s shorthands are nicely suited to being standalone values, Crystal and Ruby only work as function arguments, and Swift and Kotlin can be used as values, but they work much better as function arguments.

In general I like the ability to build APIs that appear to extend the language, so closures that look like code blocks are my favourite. However, no language has what I think is the ideal: a terse closure syntax that matches how functions are defined. Take Swift, for example:

func some_function(arg: String) -> String {
  ...
}

let closure = { arg: String -> String in
  ...
}

In a closure, the argument list goes after the token that starts the block ({ in this case), but in a function it goes before. In other parts of the language, the bindings for the block go before the curly brace, like in a conditional:

if let binding = the_optional {
  ...
}

We’re defining that binding will be available within that block, and it is listed before the brace, more like a function definition than a closure. You can’t use closures to make an API that has this pattern of defining a binding before the block where it is going to be used.

I’ll keep on the lookout for a language that makes these syntaxes match, but I think it’s a natural tradeoff between these two types of closures.

Closures seem to be the point in a language where everyone suddenly gets on board with heavy type inference, implicit returns, and removing unnecessary syntax like curly braces around single expressions. It’s amazing how much is inferred in a Java lambda compared to the rest of the language3.

The closure feature I think more languages should adopt is compile-time guarantees on how many times a block will run. I was pleasantly surprised that Rust closures come in three flavours: FnOnce, Fn, and FnMut. This is necessary to work with Rust’s ownership model, but other languages could make use of this to allow for smarter checking of variable initialisation. To give an example:

def initialise_with_random(&block)
  yield Random.rand
end

number: Int32
initialise_with_random do |num|
  number = num.to_i
end
puts number

It is not possible to get to that puts call without number being assigned, but the compiler doesn’t know that. If I could annotate that block as an FnExactlyOnce, the compiler could both check that I do actually call it, and also know that my variable will always be initialised.

Of course the real answer is that I should just use Lisp and be able to define my own syntax for everything using macros.

  1. Previously my example incorrectly showed it being possible to omit the name in def and use it as a value, but this is not the case, thanks Susanne for the correction! 

  2. Woooo, open source! 

  3. In Java 8, aka the only Java version anyone actually uses. I don’t know what weird stuff has been cooked up in newer Java versions. 


Apple Watch Running Apps

This year I’m running the Sydney Marathon, and so I’ve got a lot of running on the cards for the next few months. I’ve been using an Apple Watch to track my runs since early 2019, first a series 3 and now a series 8. Here’s what I’m using to keep track of training and races.

WorkOutDoors is a kitchen-sink-included Apple Watch activity tracker, mostly focussed on running, cycling, and hiking in areas where a map is required. Some Garmin watches have maps built in, which is super useful to make sure you take the right turns on a trail run.

Often I’ll do trail runs with my friend Max1, who is always organised enough that I don’t need to know where we’re going. I bought WorkOutDoors as a backup but then never got around to using it, until a few months ago. It’s a bit intimidating, but it’s super useful for trail runs where you’re not sure of exactly which track you need to be on.

Last month I ran UTA 22 in the Blue Mountains just to the west of Sydney. I used WorkOutDoors to not just show a map, but also show the route with elevation and key points of interest (like the aid station) so I knew what to expect as I was running. Perhaps it takes some of the adventure out of the run, but it was useful to know how much of an ascent or descent was coming up. Without it, I would have been trying to guess where I was based on a patchy recollection of the elevation profile.

You need a GPX file to show the route in WorkOutDoors. Some races (like UTA) supply an official one, but if I’m doing a training run the best tool I’ve found to make one is the Garmin Connect course planning tool. You don’t need a Garmin watch to use it, but you do need to make an account. Once you’ve made a route by clicking waypoints and adjusting the path-finding, you can download the GPX file.

The “Routes” section of WorkOutDoors has an “Import” button where you can load the GPX file from the iOS Files app. You can then give it a name, send it to the watch, and tell the watch to download the surrounding map tiles. The trick that confused me initially was that you need to go into the settings for WorkOutDoors on the watch and select an active route to have it appear on the map when you start an activity. If you don’t do this you’ll still get a map, which can be useful, but not as useful as having the real route.

If I’m just doing a run from home—where I don’t need a map—I’ll track the run using the standard Workouts app on the Apple Watch. It’s not perfect, I’d really like the ability to increase the size of certain metrics and make better use of the screen real estate. Currently about 30% of the screen is just empty, and I can’t use that space to make the existing metrics more readable, only to add more clutter. It does work reliably and I’m used to reading the tiny numbers.

I’m a bit miffed that the ability to have completely custom metric sets was removed a few years ago. You can’t put any metric on any screen, some metrics are restricted to particular pre-defined screens. For example you can’t have an altitude graph on the same screen as your pace. That being said, the only screen I really care about is the main one. This is configured to show time, distance, rolling pace, and average pace. The second screen I use exclusively during recovery runs to show the heart rate zone indicator.

The reason I want to see the heart rate indicator is because of another frustrating design decision. You can have pre-defined runs with a target, either time based, distance based, or “custom”. I would like to be able to setup a “recovery run” with a time goal and a heart rate alert to catch me if I’m putting in too much effort. However the alerts (including both heart rate and pace) are defined globally for all runs, not a particular flavour, so if I set a heart rate goal I risk forgetting about it and being spammed with alerts when I do my next run. So instead I just scroll to the second screen and glance at the heart rate zone every so often.

“Custom” runs are a welcome addition, as they make interval training much easier. You can set work and rest periods defined either by distance, time, or “open” (you double-tap the screen to advance to the next interval). They have a custom screen with some interval-specific info—but it seems like you can’t customise that view at all.

Custom runs are definitely only designed for intervals, however. I wanted to setup a 5K run that included a warmup as part of the workout. This would skip having to fiddle around and swap to a new workout after the warmup, instead I could just go from my warmup straight into the main course. This didn’t end up working because the “distance” selector for an interval only goes in 5 metre increments, so I would have had to scroll 1000 times to put in my 5K goal. I just put up with stopping and starting a new workout.

The iOS Fitness (previously “Activity”) app is reasonable for viewing information about runs, but the last few updates have sacrificed usability in this area to put more focus on Fitness+—which I have no interest in. For example, it now only shows your latest activity on the main screen (previously it would show multiple) in order to make room for a weekly “trainer tip” video.

Instead I use HealthFit2, which reads the same HealthKit data, but surfaces much more information. The main view lists all activities with nice big maps. Each activity has graphs for pace, elevation, and heart rate, and a whole host of other stats. Some of this is available in the Fitness app, but not easily accessible.

What the Fitness app doesn’t have is nice graphs for keeping track of activity per week, month, or year. I’m currently using the weekly “kilometres run” graph to keep up with my marathon training. This is much more actionable than the trends shown in the Fitness app, which work on a fairly long time frame (previous 3 months compared to the last year) and offer frustratingly obtuse advice—if I start running significantly further, I get told off because my average pace is dropping.

If you’re at all serious about running and use an Apple Watch to track your exercise, buying both WorkOutDoors and HealthFit (about $10 each, HealthFit has an optional subscription for minor features) dramatically improves the experience of using the watch while trail running and visualising the data afterwards.

  1. Website pending. 

  2. Weirdly they don’t have a website listed anywhere, only a Facebook page. 


Repairing My Roborock S6

About three weeks ago my previously-trusty Roborock S6 (named Henry) stopped halfway through a clean. Usually this means that he found a tasty looking cable or shoelace and got tangled up, but when I got home he was just sitting in the hallway unobstructed. I popped him on his base, and didn’t think much of it. The next time he was scheduled to clean up I got a notification saying that the laser distance sensor had malfunctioned, and that I should remove any obstructions and retry. That was cause for more concern.

I sat him in the middle of the floor and turned him on, and sure enough the LiDAR sensor (housed in the knob on top of the vacuum) didn’t spin. After trying and failing to start three times, I got the error notification again.

After a look online, this seems to be a reasonably common failure, and the official advice is to contact customer support. So I dutifully contacted Roborock and explained the failure, and eventually they quoted me $70 for an “assessment” of whether it was repairable, and then estimated that a repair could cost $60-200. I’d also have to pay for shipping either way, which I’d conservatively estimate at $35 each way.

Customer service also pointed out that because the S6 was “phased out”, including spare parts. I was not particularly thrilled at the prospect of paying $140 to send Henry off to someone who didn’t have the parts needed to do the repair.

Naturally, I disassembled Henry to see if I could notice anything obviously wrong—my concern was that something had just got lodged and was preventing the LiDAR from spinning. Disassembly was straightforward, the only trick being that you have to pry the front cover off with a little bit more force than I’d be comfortable with if I didn’t know that was the correct procedure.

Henry with his top off

The LiDAR “laser distance sensor” module is the black and orange unit in the centre.

This didn’t reveal any obvious failures, but it did give me confidence in replacing the LiDAR unit myself. It’s a self-contained slide-in component—you just undo some screws and it disconnects from a single port that connects it to the rest of the vacuum.

Now that I knew what the part looked like, and that doing the replacement would be easy, I found a replacement part on AliExpress for $70 (with shipping included). I don’t think I would’ve trusted the compatibility advertised in the description if I didn’t know the shape of the component I was looking for. The shape and screw locations matched, and it would be weird for Roborock to make two seemingly self-contained parts that are physically identical but incompatible in software.

The part arrived after about a week, and it had some subtle differences in the design but not in the overall shape. It turned out the new part was coded LDS01RR but the broken one was LDS02RR, so perhaps this was made for the S5 originally. I slotted it in, booted the vacuum up (sans top) and it worked perfectly. After putting the top back on, Henry was able to catch up on all the vacuuming he’d missed in the last three weeks.

I’m glad the repair worked so I didn’t have to spend money buying a new part and shipping Henry off to Roborock. It’s not great that this part can seemingly fail spontaneously, I’ve looked for any blown out components but haven’t seen anything. Naturally, I will hoard the old broken part in case the new one fails and I have to Frankenstein them together to get Henry going again.


Using JJ for the Version Control Operation Audit

So I just wrote about the version control operations that I use day-to-day. My new favourite thing is JJ—a git-compatible version control system that I’ve also written about before—so I thought I would explain how each of these operations are done with JJ.

View what’s about to be committed

So we’re already at a “well, actually” moment, because all changes in JJ are automatically committed, but basically jj diff will do what you want.

Making a commit

You do jj new to start a new commit, jj describe to set the commit message, or jj commit to set the commit message and start a new commit in one go.

Uploading a change to be reviewed

This depends on your workflow, but jj git push --all will upload every branch to your remote. I also use jj git push -c @- to create a new auto-named branch, and jj git push -b 'glob:willhbr/push-*' to upload every auto-named branch. These all sit behind convenient aliases that I’ve mentioned before.

If you’re submitting a change to someone else’s repo via your own fork, it works really well to set the upstream remote to be theirs, and origin to be yours, then edit the repo config to pull from upstream and push to origin:

[git]
push = "origin"
fetch = "upstream"

Altering a change based on code review feedback

You can do this a few ways:

jj edit $change to swap your working copy to point to the change you want to alter, but this makes it a little trickier to see what your alterations are since you’re editing the change directly (jj diff will show the diff for the whole change). There are ways around this using jj obslog but that’s more work.

jj new $change will create a new change on top of the target you want to alter. You can make changes, view the diff compared to the target (the parent) with jj diff, and then do jj amend to move the changes into the target.

Of course you could just do jj new anywhere, make your edits, and then do jj squash --into $change to move the changes. This works from anywhere to anywhere1. This does run an increased risk of creating conflicts, but you should live dangerously every once in a while.

Revert a file back to the original state

Either jj restore --from=$change <paths>, or jj diffedit (I haven’t used that one).

Alternatively I just do jj split and then jj abandon on the commit that has the changes I don’t want.

Splitting a change in two

It’s just jj split. No tricks.

Merging two changes into one

I’d like to be able to do this with a murcurial-style histedit-and-fold, but JJ doesn’t have histedit yet so the next best thing is jj squash --from $a --into $b, and then jj abandon the empty commit.

Writing dependent changes

The depends on the review system you’re using, but in the common branch-based ones (GitHub/GitLab) you just use jj git push -c $change to create a branch that can be uploaded for review. This won’t move as you add more commits, so you don’t have to remember to branch before you continue working.

Reordering dependent changes

JJ doesn’t have a mercurial histedit command (yet), so I’d do this with multiple rebase -s X -d Y invocations. This is less than ideal, but gets the job done.

Make a dependent change independent

jj rebase -r @ -d main will pop the working copy change off its parent and put it on top of main.

Context switch between changes

Either jj edit or jj new, depending if you want to be editing the change directly, or a new change on top of it. Since your working copy is always recorded in a commit, there’s no need to have any stash mechanism.

Jump back to main

jj new main, and you can do this at any point because you don’t have to worry about stashing.

Test someone else’s change

This is another one that depends on your workflow. If the changes have been pushed to a remote you’ve already got setup, you just need to jj git fetch --all-remotes and then jj new $branchname. If the change is in someone else’s fork, you’ll need to jump through a couple of hoops to add the remote first, fetch from it, then start a new change on top of their branch.

Build off someone else’s change

This looks just the same as testing someone’s change, you just start writing some code. You might need to fetch and rebase if they update their code.

Rolling back a change

jj backout -r $change will create a new commit that reverses everything done in $change. This may have some conflicts, depending on how old $change is.

Update your work based on newly-merged code

jj git fetch --all-remotes is my go-to, I have this aliased as jj sync. It only fetches though, it doesn’t actually alter any of your pending changes. I then run jj rebase --skip-empty -d 'trunk()' (aliased to jj evolve) to put my current changes back on top of main. If I was working on top of someone else’s change, I would have to replace trunk() with their branch name.

Show what changes are pending

I think the default jj log query shows too much stuff, so I’ve got a custom query that will typically show less than half a screen of output:

[revsets]
log =  '@ | ancestors(trunk()..(visible_heads() & mine()), 2) | trunk()'

How I worked this out is a topic for another day, but this basically just shows my changes that haven’t been submitted yet, and ignores other people’s unmerged branches.

I’m not using JJ for my day-to-day work, just for my personal projects (like this website!) and so I’m not actually doing most of these operations that often.

  1. I’m pretty sure? I haven’t checked though. 


The Version Control Operation Audit

Often I see people dismiss complaints about a particular version control system’s usability1 because you “just need to learn like six commands”2, and so it doesn’t matter that some things are complicated, because you won’t use them day-to-day. If you do use them, it’ll be infrequent enough that looking up an example is not a big deal.

So with that in mind, here’s all the operations—not commands—that I use a version control system for. I’d say these are things I’d do with enough regularity that it’s not at all noteworthy that I did them.

View what’s about to be committed

Before I commit I always want to do a quick check to make sure I’m committing what I expect. This gives me an opportunity to undo any changes I’ve made just for debugging, or spot any issues that I need to resolve before sending the change off for review.

Similarly, I will fairly often want to check the contents of an existing commit in a diff format compared to its parent.

Making a commit

Making a commit is obviously table stakes, but something I’ll do is commit only some of my changes—either just certain files, or certain chunks in certain files.

Uploading a change to be reviewed

Initially I forgot this one because it’s basically a reflex, but it should definitely be on the list!

Altering a change based on code review feedback

So you fix a bug or implement feature or whatever, commit those changes, and upload them to be reviewed3. Your reviewer leaves some comments and you need to make some changes. Assuming that you want a clean change history, you should be able to easily make the alterations the reviewer suggested, alter your commit, and re-upload that back to be reviewed again. This is almost certainly the thing I do the most often.

The optimal granularity of changes is a well-discussed topic, which I won’t go into here, but in general I would prefer to not have “fix test” and “oops wrong value” in the commit history if I could avoid it. My ideal is that the project should compile and the tests should pass at every commit in the main branch.

Revert a file back to the original state

You make some changes, then you realise that they were rubbish, or maybe you added a bunch of debugging code to a file that you don’t need any more. Whatever the case is, it should be easy to blast away any changes to a file and get it back to the latest version on the main branch.

This includes both discarding uncommitted changes from your working copy, as well as dropping the changes in a file from a commit. Just today I was working on a change and added a bunch of debugging code to a particular file. When it came time to send the change for review, I needed to get rid of all the changes in that file.4

Splitting a change in two

Often I’ll make a variety of changes across the codebase and then realise that what I’ve done is actually better thought of as two separate changes—it just happens that I did them at the same time.

You should be able to take your change—committed or uncommitted—and turn it into two changes that can be reviewed independently.

This is useful to get feedback from different people (without having to explain the unrelated changes they should ignore), to keep the history logical, or to make it easier to roll back one of the changes if it breaks something.

Merging two changes into one

Sometimes you thought a change could be made in parts, but for whatever reason you’re going to need to land everything at once. Maybe you thought you could adjust an API and the migrate call sites over later, but it turned out to be impossible. Whatever the case is, your two changes need to become one.

Writing dependent changes

You make one amazing feature, and send the code to review, but then have an idea for a second amazing feature that builds on top of the first feature. You should be able to continue building on top of your existing work while you wait for a review on the first feature.

A bit of a git-gotcha—at least for branch-based review tools—is that it’s easy to just continue committing on the same branch, push it, and then have the commits for the second feature be included in the review for the first. You’d then have to manually point the branch back to the right commit. You have to remember to proactively create a new branch when you start working on what will be a new change.

Reordering dependent changes

A bit of a less common operation, but if you’ve made a series of changes in a dependent chain and they’re not actually dependent on one another, it is really convenient to be able to re-order them to get something submitted before the others.

The most obvious example is a refactor that has to touch every call site for a method. You don’t want to send it all as one change, you don’t want to swap back to main and lose track of which call sites you’ve updated, and it doesn’t matter which part of the refactor is submitted first.

Make a dependent change independent

Instead of rearranging the order of changes, I’ll instead just move a change to be in a separate series of changes before sending it for review. If my log looked like this:

@  q Will Richardson 1 second ago
│  Some useful bug fix
◉  u Will Richardson 3 hours ago
│  A second, equally useful feature (also huge)
◉  u Will Richardson 5 hours ago
│  Implement a huge feature
~

Then I would move that top commit to be separate from the feature work:

◉  u Will Richardson 3 hours ago
│  A second, equally useful feature (also huge)
◉  m Will Richardson 5 hours ago
│  Implement a huge feature
│ @  q Will Richardson 25 seconds ago 0b
├─╯  Some useful bug fix
◉  z root() 00

Then I can continue working on the useful features, and send the bug fix for review.

Context switch between changes

Chances are you’ve got multiple changes on the go at any given time, and so you want it to be super easy to swap from working on one change to another. Usually what happens is you send something for review, get started with a new task, and then when the review comes back you need to swap back to editing the first change to make some fix-ups and get the code submitted.

Part of this is being able to switch while you’ve got some uncommitted changes in your working copy. You need to be able to record these somewhere, swap to the other change, and not lose them when you need to swap back.

Jump back to main

Similar to the previous one, but something that I find I’ll do while working on a single change, as I’ll want to verify some behaviour without my in-progress changes, and then jump back to whatever I was doing.

This means being able to store your working copy changes, so your working copy is empty when you move back to main.

Test someone else’s change

Sometimes you just need to run someone else’s code locally, either to check out the feature they’ve implemented, or to do some debugging into a problem that they’re having.

It should be easy to get the version of the code that they’ve sent for review, and start making changes.

Build off someone else’s change

Similarly, you might need to start working on a change that requires an API or fix that someone else hasn’t merged into the main branch yet. Once you’ve got their change locally, you need to be able to commit your own changes, and re-update them based on any alterations they make after you started.

Rolling back a change

It doesn’t take long doing operations work to appreciate a simple rollback. Having a way to say “make a change that reverses everything done in that change” is invaluable. Of course you might have to resolve some conflicts if there have been other changes in the interim, or maybe make some manual changes if you don’t want a 100% pure rollback.

Update your work based on newly-merged code

An active codebase is a moving target, and you’re going to need to keep your code up-to-date with the latest code in the repo. This makes the review simpler, the submission faster, and reduces the chance of you making a change that interacts poorly with someone else’s work.

Show what changes are pending

I don’t want to lose track of work that I’ve made locally, so having some way of listing all the changes that exist on my machine that haven’t yet made it into the main branch is very useful. Usually this is necessary when I’ve swapped between tasks and need a reminder to upload a change and get it reviewed.


I’m fairly sure that’s the lot. I’ll leave it as an exercise for the reader which commands those actions would map to in your favourite version control system. If you’re one of these mythical 6-command people, what do you think of my operations? Do they fit in your memorised commands, or are these just things that you never need to do? Do you use a graphical interface for your favourite VCS that abstracts these things away? Send me a toot, I’d be fascinated to know!

What I haven’t included here is operations that involve reading the whole history of a project. My main interaction with version control for making changes is via command-line interfaces, and CLIs are not very good for reading changes. Instead I’ll do these through a browser-based code review and code browsing tool (eg the GitHub/GitLab UI). For what it’s worth, the things I view in that UI are:

  • Change history for a file
  • State of a file at a particular point in time
  • Blame for a file
  • Blame for a file at a particular point in time
  • Diff for a particular already-submitted change
  • Cross-references, interface implementations, and other non-VCS information
  1. It’s git, obviously. 

  2. Replace six with your favourite number that the average person would consider “small”. 

  3. You are doing code review, right? 

  4. Reading this back I realise just how many of my examples are about separating debugging code from real code. This is probably because I’m a serial printf debugger. If you’re a real debugger person, I guess you never have to do this? 


Some Hot JJ Tips

I spent a bunch of time learning how to use JJ properly after I gave up on git. Up until this point, I had been dumping commits directly onto main and just pushing the branch occasionally. I had avoided learning the pull/merge request flow because it’s not something I use on personal projects, but it turns out to work pretty well. With a few tactically-deployed aliases I’ve got a pretty simple flow going.

We start a new change with jj new, and make some edits to some files. We’ll end up with something like:

@  lw Will Richardson now 2
│  (no description set)
◉  w Will Richardson ago main main@origin HEAD@git 03
│  Bump version number to 0.8.1
~

Once we’ve made some changes and got stuff working, we’ll give it a commit message with jj commit -m 'do some stuff'. With that super meaningful commit message, I’m ready to send this change for review. The easiest way to do this is to use jj git push -c lw (lw is the change ID we’re pushing):

$ jj git push -c lw
Creating branch willhbr/push-lwwlpunxnpnu for revision @-
Branch changes to push to origin:
  Add branch willhbr/push-lwwlpunxnpnu to af2e2412e623
remote:
remote: To create a merge request for willhbr/push-lwwlpunxnpnu, visit:
remote:   https://gitlab.com/willhbr/.../-/merge_requests/new?merge_request?...

JJ auto-creates a branch for us based on the change ID. I’ve customised this with the git.push-branch-prefix option to include willhbr/ at the front so I know it’s mine.

The change has been pushed, and the remote—GitLab—has given us a handy link to create a merge request. This command is a bit wordy, so I’ve got an alias that will push the change automatically:

[aliases]
cl = ['git', 'push', '-c', '@-']

A little side note: @- refers to the parent of the current change, since when I’m running this I will have just created a new commit, and my log will look like:

@  x Will Richardson 4 minutes ago 6
│  (empty) (no description set)
◉  lw Will Richardson 4 minutes ago HEAD@git a
│  do some stuff
◉  w Will Richardson 1 month ago main main@origin 03
│  Bump version number to 0.8.1
~

So to push that first non-empty, non-working-copy commit I use @- as the change ID.

Now I just need to wait for someone to review and approve the merge on GitHub or GitLab or whatever, and do the merge via the web UI. Once that’s done, I can fetch changes from the remote, and my changes will disappear from the default log view as they’re now just part of main@origin.

Depending on how the remote is setup, we might have to do one more step. If the changes were merged into the main branch, the commit hashes remain the same and everything works normally—JJ knows the commits now in main are the ones you authored. This is the default behaviour in GitHub and GitLab. However, if a GitHub project is setup to rebase or squash into main, you’ll end up seeing duplicate changes. This is because the commit hashes get updated when they’re rebased, so JJ can’t reconcile them when it fetches new changes. If you rebase your existing changes on top of main, your local changes will become empty—since their content is already present in the other commits. Instead when you rebase, pass --skip-empty, and these empty commits will be dropped.

I’ve got two more aliases to make this easier:

[aliases]
sync = ['git', 'fetch', '--all-remotes']
evolve = ['rebase', '--skip-empty', '-d', 'main']

So I just jj sync to get all the changes from the internet, and then jj evolve to put my changes back on the new location of main.

If you use a web UI to accept some changes based on reviewer feedback, the next time you jj sync, the changes will be added to your local branch. You could then make further edits, or squash the suggested changes back into the original commit to have a cleaner history.

If you make any alterations locally the branch name in the log will have an asterisk after it to indicate that it has changes that need to be pushed. Update all branches with jj git push --all (I have this aliased to jj upload).

Something of note is that if you have two changes in succession (one is the parent of the other) and you make two pull requests from them, the child pull request will contain all the content from both changes. Unless your code review tool has some way to change the base of the diff1, you’ll want to get them reviewed in sequence. Alternatively, if the child change doesn’t actually depend on the parent—perhaps it’s just an unrelated bug fix you made while working on a feature—you can just rebase it to be a sibling of its parent

If you end up in this situation, and now want to get that bug fix submitted ASAP, but it’s currently sitting on top of a huge feature that’ll take ages to get reviewed:

$ jj log
@  q Will Richardson HEAD@git a
│  Fix how the bugs are created
◉  u Will Richardson 9
│  Implement a huge feature
~
$ jj rebase -s @ -d @--
@  q Will Richardson 0c
│  Fix how the bugs are created
│ ◉  u Will Richardson u
├─╯  Implement a huge feature
~

That little rebase trick takes the current change and moves it to be a sibling of its parent. I’ve used the hg equivalent of this for years to get code merged that I had written in the opposite order I should have.


I’ve got some aliases to make it easier to quickly get going with a JJ repo. I’ve only been using colocated JJ/git repos, which means there’s both a .git as well as a .jj directory, so any git tool or command also works with no modification. In my ~/.gitconfig I have:

[alias]
jj = "!jj git init --git-repo=."
setup = "!git init && git jj"

This allows me to run git jj in an existing repo, or git setup to get from no version control immediately to good version control, with no intermediate steps.

In my ~/.jjconfig.toml I have a bunch of aliases, I’m not fully settled on these but here they are anyway:

[aliases]
# Old init alias, before I added the aliases in git
ig = ['git', 'init', '--git-repo=.']

# If I want to just push directly to main
# This just sets it to be the second-latest commit
setmain = ["branch", "set", "main", "-r", "@-"]
# Sync everything, mentioned above
sync = ['git', 'fetch', '--all-remotes']
# Put stuff back on top of main
evolve = ['rebase', '--skip-empty', '-d', 'main']

# Do a full log, rather than just the interesting stuff
# Basically the same behaviour as the default git log
xl = ['log', '-r', 'all()']
# Progression log? Shows how the current change has evolved
# A bit more on this later
pl = ['obslog', '-p']

# Pushing changes and auto-creating branches
cl = ['git', 'push', '-c', '@-']
push = ['git', 'push', '-b', 'glob:willhbr/push-*']
upload = ['git', 'push', '--all']

# This might be useful, opens an editor to set per-repo settings.
configure = ['config', 'edit', '--repo']

Ok, about that jj pl alias. jj opslog will show the progression of a commit, so you can view or revert back to an intermediate state without having to actually make intermediate commits. So if you do a bunch of work, and get stuff working, and then decide to make everything better but actually make a huge mess of it, you can get back to the middle state even if you forgot to commit at that point. Here’s the progression for my website while I’ve been working on this post:

$ jj obslog
@  z Will Richardson 13 seconds ago a
│  (no description set)
◉  z hidden Will Richardson 1 hour ago a6f
│  (no description set)
◉  z hidden Will Richardson 3 hours ago b2a3
│  (no description set)
◉  z hidden Will Richardson 3 hours ago 80f
│  (no description set)
◉  z hidden Will Richardson 3 hours ago 2e
   (empty) (no description set)

The -p option shows the patch diff between each version, so I can quickly see what I had changed.

The big caveat is that this only works at points in time where you ran a jj command. If you haven’t run jj status or jj log or whatever, it won’t have picked up your changes.2

This is a side-effect of the working-copy-as-commit model, so every time you modify a file and run a JJ command, it amends the changes into the working copy commit. However this just creates a new commit (that’s how git works), so you’re leaving a trail of commits as you work. jj opslog just exposes that trail to you. I don’t think I’d rely on this—I’d rather create a bunch of commits then squash them later—but having this as a backup just gives me more confidence that I can find something I’ve lost in a pinch.

Most of my learning was done reading the JJ docs on working with GitHub and GitLab, and perusing the CLI Reference. I also read jj init by Chris Krycho the other day and enjoyed his detailed look at things.

  1. If you know, you know. 

  2. It does have some filesystem watcher, which I assume will keep this fully up-to-date, but I assume you’re not running that. 


Happy Birthday to Website

My website is now ten years old!

Screengrab of willhbr.net today

How willhbr.net appeared as this was posted.

Ten years ago today I made the first commit to this website, consisting of just an index.html with the contents:

<p>Sup.</p>

Thankfully it didn’t stay like that for long (just over three hours) and soon after I committed a simple “about me”-style homepage:

Screenshot of my 2015 website

Hilariously this included adding the entirety of Bootstrap just to get that div centred, and loading jQuery just to expand a section when you click the button.

It was a few months later in late 2014 I added Jekyll and wrote my first post. This started out as hiding behind a /blog URL, but later moved onto the main page. I refuse to link to any old posts directly because that’s just embarrassing.

In 2015 I spruced up the design and added this photo of my shiny new OnePlus One:

Screenshot of my 2015 website, background is a photo of a OnePlus One in a bright orange case

Yep that’s me using an Oculus DK 2.

Big images are super cool, for ages I have wanted to have that cool layout where the images extend to the full width of the page but the text remains in a narrower centred block. I don’t have enough CSS enthusiasm to implement this, and I don’t post enough images to make this worthwhile. The main reason I’ve ended up back with an image-less layout is that even on a reliable, high speed connection it can take the best part of a second to load the image. That’s not too bad for small part of the page, but when it’s taking up the whole page it’s super jarring to have it pop in after the rest of the page has loaded.

I did have big fancy images for a while, but ended up giving up on it. It’s also a lot of effort for something that is basically invisible in a mobile layout.

My post about my OnePlus One with a big, wide image

Who needs any kind of readability or contast!

It’s amusing that the basic layout and style of my site were locked in at not too long after I setup Jekyll. Big title of my name at the top, subtitle (since removed), and a few links. Posts had a big coloured title, with some basic metadata below.

basic website design circa 2015

A substantial part of this design is that I have limited enthusiasm for complicated layouts that change substantially with screen size. Most of my web development experience was just before everyone started looking at everything on their phones. My site layout doesn’t require any @media queries to change based on viewport width, it’s max-width and margin: auto doing all the heavy lifting.

Website design January 2023

The website in early 2023, after I settled on purple but before I started endless tinkering.

There’s definitely an aesthetic that I’m following of “technical personal website/blog” that’s typically shades of grey with a single highlight colour, default sans serif font, minimal layout complexity. I’m definitely influenced by the sites that I follow, as well as the ease of implementation.

The goal is to make all the information a reader might want as available and obvious as possible. The text of the post is right there—that’s the main thing. If someone decided to use a reader mode on my site I would consider that a design failure. Post metadata (most importantly the date, but now also tags) is clear right under the post title. I’ve definitely come across posts with no obvious date and been unsure if it’s still relevant, or maybe if it just needs to be read in a different mindset. This is especially true for technical writing, where things change somewhat frequently. If your writing is timeless, I would forgive you for omitting the date. But I do think you should still include it for completeness.

Last year I added some more links at the bottom of the post: next and previous, links to the archive, RSS Feed, and my Mastodon account. When I come across an interesting post I want to see what other things the author has written, so I made this easily accessible at the end of every post.

I would quite like to have links to related posts based on keywords or tags, but I don’t really have a big enough collection of posts for this to work super well, and I don’t do Jekyll plugins at the moment anyway. This might change as I play around with tags more.

In a bunch of places I have a link to the archive, which is a chronological list of every post on the site. Some sites have similar pages with just lists of months, each linking to the full content of the posts published during that month. I find this annoying. It’s hard to search for a particular topic or keyword as the post titles aren’t in the list, if you don’t know the exact month of a post you have to click through each page manually. It just pushes people into leaving the site and using a search engine with a site: filter.

I’m such a fan of the archive page that I try and slip links to it in as many places as possible. It’s in the site header1, the date on every post goes to the archive, there’s a link after every post, it’s next to the pagination links, and if you go to a page other than the first one there will be pagination at the top and bottom of the page (both with links to the archive).

Hopefully if you’re looking for something I’ve written, skimming the archive will find it.

Picking an accent colour is tricky, if you look throughout the history of the site I’ve dabbled in blue, teal, green, Ubuntu-flavoured orange/brown, and now purple. It’s hard to not just fall back to using blue for everything, and I’m really happy with the purple accent—both in light and dark mode. I think of the dark mode as being the canonical colour scheme, since I’m almost always writing something late at night.2

It’s hard not to use web fonts, since there’s basically no guarantee as to which fonts will be available on any particular system. Previously I did use Raleway but removed it was pointed out how the whole page “pops” when the font loads. Using Helvetica or the system sans-serif font is good enough, most systems have decent looking options3—and it’s probably a font that the reader is used to seeing.

Of course, I put all this effort in and then the best-case scenario in my view is that someone subscribes via RSS and never sees the site. Although there are some affordances that make the feeds4 friendlier. First is putting the whole post content in them—my posts aren’t too long so it’s not like the feed becomes unwieldy. I copied the idea of a feed-only footer after seeing it on Pixel Envy, it’s a really simple way to tell the reader “this is the end, there’s nothing more to read, you can leave now”. As a reader I never know if someone’s feed contains the whole post or if it’s just a snippet—it’s possible to get to the bottom and wonder if there’s something you’re missing. Writing well is another option, but I find that more difficult.

If you are going to just include stubs in the feed, then you should do a similar thing—put a “continue reading” link at the bottom of the entry. This makes it clear that you’re not just posting a one-paragraph quick thought, and it’s probably easier to click that link than rely on the feed reader UI to make opening the post in a browser obvious.

It’s especially annoying when a feed includes a few paragraphs, so you read that, wonder if you’ve got to the end, scroll back up to the top, open the post in the browser, scroll down again to find the point you’d read to, and then continue reading from that point. I don’t want to put anyone in that position.

Now for some indulgent behind-the-scenes details.

When I first made the site I was using TextMate for editing code and MacDown for markdown editing. Later, when I was doing things on my iPad I used Bear and then iA Writer, probably with some others in the middle. On the iPad I could just use Working Copy to push the changes directly to GitHub, but for some reason I absolutely must see the post as it will look on the site before I push it publicly. Seeing the post in a different context—not a syntax-highlighted, fixed-width markdown editor—helps spot mistakes, and gives me confidence I haven’t colossally messed up some markdown syntax or post metadata. I do the same thing when I’m sending code to be reviewed—I’ll look at the diffs in the terminal but then upload to a code review tool and immediately spot mistakes.

Running the website “locally” is actually running it on one of my home servers, so I’ve come up with plenty of mechanisms for getting posts from my iPad or Mac onto the server. Despite all my efforts, the easiest is still just to copy-paste into Vim.

Now that I’m doing everything on my Mac, I use MarkEdit after a brief dabble with Obsidian. I just keep drafts in my documents folder like the good old days. There’s a little helper script that deals with the front matter and file naming convention for Jekyll. Turning a human-readable title into a URL slug manually is just not a good use of energy.

What will the site look like in another ten years? Have I reached the optimal design, or has drudging through ten years of website screenshots inspired a proper redesign? Wait until 2034 to find out!

  1. Only on the main page, if you’re on a different page then it just links back to the main page. 

  2. Written at 10:22 PM. Side note, I don’t know how anyone uses MacOS in light mode, it’s so bright! 

  3. Apart from Ubuntu Sans, I don’t know what it is but I always recognise it and that makes it stick out as “Hey I’m written like the ubuntu logo!”. Back in my day the Ubuntu logo was 100% curves. 

  4. Don’t forget about the JSON Feed