Skip to content

Friday, 30 October 2026

Last weekend I joined the OSM Hack Weekend at Wikimedia Deutschland’s office in Berlin, mainly to work on a few Transitous-related topics.

A bunch of participants with laptops sitting around large tables.
OSM Hack Weekend at Wikimedia Deutschland (source)

Schedule data augmentation

One topic we wanted to move forward was how Transitous can augment the public transport data provided by operators and agencies, as having people from Transitous, OpenStreetMap and Wikidata together in one room is the ideal place for that.

Official public transport data often only covers the bare minimum, while Transitous and apps using it such as KDE’s Itinerary are continuously increasing the level of detail. A few examples:

  • Translated, transliterated or otherwise localized names for operators, stops, lines, etc.
  • Extended route types rather than just “train” or “bus”.
  • Line colors and logos.
  • Accessibility information about stops or vehicles.
  • Information whether a service requires a mandatory reservation or a surcharge.
  • Information whether taking along a bike is allowed, and whether that might require an extra ticket/surcharge.

Crowd-sourcing works for filling such gaps, and people are spending quite some effort on this, as seen e.g. in an 8.000 lines Lua import script patching line colors into the German public transport dataset. This isn’t really maintainable and scalable though.

That’s where OSM and Wikidata come in. Some of the extra data we want is already in there, and more importantly many of the objects we want to augment already exist in there (stops, operators/agencies, public transport lines, vehicle types). Collecting data in there would also make this more widely usable than just for Transitous.

Easier said than done though, as it’s not always obvious what should live in OSM and what should live in Wikidata. Public transport lines are particularly contentious, as they have a geospatial aspect but are difficult to maintain in OSM due to the way they are modeled, and likewise Wikidata wouldn’t want to see every bus stop in the world added just to model line stop sequences.

Another challenge is how to efficiently collect and match all this to the existing public transport data, as that generally doesn’t come with stable identifiers we can rely on. The approach currently explored is to generate a partial pseudo-GTFS feed from the OSM and Wikidata data, which MOTIS should be able to match and merge against the schedule data with little changes, using its existing matching and de-duplication infrastructure.

With another relatively small change to MOTIS we should also be able to retain and pass through Wikidata ids to clients, potentially enabling other interesting usecases there.

For more details also see the meeting notes in the Transitous wiki.

Mandatory reservations and surcharges

We also discussed how to model mandatory reservations and required surcharges in GTFS. MOTIS can also consider mandatory reservations during routing and avoid such services, which can be useful when you have a flat rate ticket for other services already. No matter whether operators provide this information themselves or whether it’s augmented from other sources, it needs to be properly represented in the data model though, which currently isn’t the case.

This tends to be somewhat abstract work, coming up with definitions of what a reservation and a surcharge are, and working through possible similarities, interactions, conflicts and ambiguities with existing elements of the GTFS data model such as the pick-up and drop-off constraints.

The best approach we could find would be an extension to the booking rules object, something currently mostly aimed at on-demand services but which should be flexible enough to also model e.g. services that require a reservation only during parts of their run, such as DB ICEs crossing into France.

Transitous usage policy

Something that unfortunately needs continuous attention is adapting and fine-tuning the Transitous Usage Policy. In recent weeks the main Transitous Matrix channel has been flooded with overly lengthy LLM-generated usage approval requests for LLM-generated single-use applications. These were not and are not the intended target audience of Transitous, but given their single-use nature most of those will never result in a relevant resource usage. The biggest problem they cause for now is making our main communication channel hard to use.

If the usage policy is actually read and understood we should hopefully see an improvement there now.

Upcoming conferences

We also did a bit of coordination and preparation for the larger events coming up in the second half of the conference season and which all have their CfP deadlines (very) soon:

Those are good opportunities to meet the Transitous team or the Open Transport community in general, same for the upcoming hack weekends listed on the Transitous website.

Recently I had the pleasure of talking with Jonah Naylor of the SteamOS Gamer magazine for an interview about KDE’s 30th birthday!

The interview appears in issue 2, which you can buy in digital or print form (or as an ongoing subscription). Here’s a preview:

We had a nice discussion about KDE’s history, the Steam Deck, weird quirks about me, and my favorite video games — this latter part unfortunately being woefully out of date, as I’ve become completely obsessed with the magnificent Terra Invicta since the magazine went to press!

Thursday, 29 October 2026

Kirigami Addons 1.16.0 is out! Kirigami Addons is a growing collection of nice components you can use in your Kirigami and QML applications.

Platform Integration

A huge chunk of the development for this release was dedicated to better platform integration.

Material

The highlight is the support for the Qt Quick Controls Material style. This means when using Kirigami Addons components with the Material style, they will blend more with a native application on Android. It’s not perfect yet and there are still some rough edges in the whole stack, but it seems promising for our flagship applications on Android like Itinerary or Tokodon.

Here are some screenshots:

 
 
 
 
 
 

Note that this is still missing a new release of Kirigami and there are a bunch of merge requests still under review (but the list is getting smaller).

I am hoping this will make the Kirigami ecosystem more attractive to mobile application developers as they can target both Android and Plasma Mobile, while having a native look and feel. Additionally, I am working on material icons theme which goes hand to hand with this style.

macOS and iOS

On iOS, Máté Gyöngyösi fixed a build issue on iOS with static linking.

And on macOS, Ishan Mitra made the settings dialog use a regular modal window with a title bar and close button.

Similar to the material style, if anyone is interested in working on macOS or iOS styling, while I don’t have access to the hardware, I am happy to help and support this initiative.

FormCard

The FormCard module also received several improvements.

Méven made FormCardDialog scrollable when there is too much content.

FormCardDialog also has a new API autoSeparators which is similar to the one added in the last release for FormCard. Internally all uses of FormCardDialog and FormCard now use this as this makes the code simpler.

The FormLinkDelegate now has a tooltip to show the destination URL and we disabled the mobile-centric long-press menu on desktop, but it is still available via right click.

There were also various small improvements and bug fixes to the AboutPage. The hard-wrapped license texts are now correctly displayed. We are now supporting bug-report addresses which are email addresses. Some visibility binding loops were fixed in the AboutPage and FormGridContainer and finally the accessibility was improved.

 

Settings

Search in the settings dialog now includes the contents of settings pages that use FormCards, and keyboard navigation has also been improved.

 

I18n

We migrated to the relatively new I18nContext system from ki18n, which means we don’t rely on deprecated context properties for internationalization anymore, which helps with the modern QML tooling.

Other

  • The date and time popups are now working correctly with right-to-left layouts.

  • ConvergentContextMenu handles actions with mnemonic text.

  • Jonah Brüchert made KirigamiAppDefaults handle setting a QWidgets style when using org.kde.desktop style. And Bill Trần fixed a crash in KirigamiAppDefaults.

  • Mathis Brüchert made some small styling changes to RadioSelector.

  • Sarah Stamper fixed multi-char emojis in the Avatar and AvatarButton components.

  • Christoph Martin improved the navigation in the documentation.

Thanks to everyone who contributed fixes, ideas, and bug reports!

Packager section

You can find the package on download.kde.org (kirigami addons) and it has been signed with my GPG key.

I honestly love the 3d view of harmonicon, but it’s just eye candy. The amount of lines of code is exceeding 75.000 so I took the difficult decision to remove it, so I can focus on more important tasks.

I believe that harmonicon currently is the best harmonica song editor that exists, and removing some eye candy helps me focus on what it actually needs: polish and bugfixes.

That’s all.

Tuesday, 27 October 2026

We are happy to announce the release of Qt Creator 21 RC.

Clate Beta is out today, free for everyone. It's a no-code, AI-native UX design tool built for embedded and HMI applications, with QML under the hood, so designers can stay in control of the full user experience.

Explore the AI Openness Timeline by Tagoross, tracing how the language of AI openness evolved from Free Software to open source AI, open weights, transparency and digital sovereignty.

I am going to tell the story of fixing a very high priority bug that was 15 years old. Arguably the worst KIO bug.

In September 2011, against KDE 4.7, someone opened bug 281270: "When copying files to a removable disk, speed and progress reflect copying to the local cache not to the disk itself". The report notes the copy notification showed about 50 MB/s to a USB 1.1 device, which cannot go faster than 12 Mbit/s. The reporter had asked a kernel developer, Alan Stern, what was going on, and summed up his answer:

it shows that the copying operation is finished when it has not even been started.

Seventy-seven comments and 17 duplicates later, that is still how a copy to a USB stick behaves in the current release of KIO. My last post about copying was about making KIO copy faster. This one is about the opposite case, a destination that is slower than KIO, and about making the progress bar and copy completion accurate.

The changes are in four KIO merge requests: MR 2506 finds out which devices are removable, MR 2517 and MR 2518 flush the device at the end of a copy or a move, and MR 2283 adds the writeback window. All of it is merged, and will be in KDE Frameworks 6.32, released November 13th 2026.

We will see the cause of the bug, its history, and what it took to fix it.

Why a copy finishes before it has started

The file worker copies a file by handing its data to write(). write() returns as soon as the data is in the page cache, in memory. The kernel writes it to the device later, when it decides to. On a local SSD, later means very soon, and you never notice. On a USB 2.0 stick that writes at 2.3 MB/s, it is another story. Here is a 128 MiB file:

A 128 MiB copy to a USB 2.0 stick before the change: the copy reports complete after 0.06 to 0.13 s, and the data is on the stick after 68 to 91 s, over four runs.

For more than a minute, Dolphin considers the copy finished while the stick is still writing. The progress bar was showing how fast KIO filled memory. Pull the stick out during that minute, and your data is gone.

Network filesystems have a different symptom from the same cause. A large copy to an NFS or SMB share leaves hundreds of MiB in memory to send to the server, and the progress runs far ahead of it. On SMB it is worse than a wrong number: with the default cache=strict, looking at a file waits until its dirty data is written. Listing the folder you are copying into looks at that file. Over a 5 MB/s link, almost every listing during a 1 GiB copy waited 15 to 20 seconds.

Fifteen years in one bug report

The right tool actually showed up very early. Two days after the report, Christoph Feck asked whether there was a way to learn when the data is really on the disk "without causing massive slow downs that any sync type calls would cause". Two weeks later, the reporter passed on Alan Stern's answer, sync_file_range(), and Christoph replied that it looked like "exactly what we need". That was September 2011, and it is the call KIO now uses.

Then not much happened for years, apart from users confirming it and seventeen duplicates. In 2021, Nate Graham raised it to the highest priority, because a move deletes the source files while the copies are still in memory. In 2022, he changed the Disks & Devices applet, so that ejecting a device explains that it is still writing. In 2024, Christoph tested the cachestat() call of Linux 6.5, which reports how much of a file is still dirty in memory, but only for one file at a time.

This bug has been my white whale. I have been contributing to KIO since 2018, and I am still learning how the Linux storage stack behaves underneath it. I tried twice in 2024 and did not get sync_file_range() right. In MR 1718, the sync_file_range() path stayed commented out. In KIO MR 1764 and Dolphin MR 862, the worker waited for the writeback of the chunk it had just written, which had not started, so it never waited at all. Waiting for the window before the one just written is what makes it work. I didn't spot the issue, nor any reviewer unfortunately.

Since then, another contributor proposed turning off the cache for copies, in MR 2162, which was closed.

Why it took so long

Making the copy correct is the easy part: call fsync() after every file, or open the destination with O_DIRECT and skip the cache. Both make copies several times slower, and they would slow down the fast local disks too, where nothing was wrong. Making it correct without making it slow is what took fifteen years, and it needed several things that were not there before.

  • A way to wait for part of a file. POSIX only offers fsync(), which waits for everything. sync_file_range() is Linux only, and its own manual calls it "extremely dangerous": it does not write the file's metadata, so on its own it guarantees nothing after a crash. It can bound how much is in memory, but the end of the copy still needs a real flush.
  • Knowing which destinations need it, without asking them. The filesystem type does not show whether a device can be unplugged, and asking a network filesystem anything is a round trip to a server that may be slow. KIO reads the answer from the mount table and from sysfs instead, and two earlier changes to KMountPoint made that cheap enough to do for every copied file:
    • MR 2326, in KIO 6.30, caches mounts by the mount id that statx() returns since Linux 6.8, instead of parsing /proc/self/mountinfo on every lookup. A lookup by id became about 700 times faster.
    • MR 2487, in KIO 6.32, reads a missing mount with statmount(), which does not walk any path, so a network server that does not answer no longer blocks the lookup. In that case, the lookup went from more than 15 seconds to about 1 ms.
  • A place to flush at the end of a copy. The file worker copies one file per command, in its own process, and never knows which file is the last one. Only the CopyJob, in the application, knows when the whole copy is over. So the flush needed a new worker command, and a way for the worker to notify the job that a flush is needed. On the way, I found that FileCopyJob dropped the metadata that the worker sent with a copy, so the CopyJob could never have seen it.
  • A window size that fits every destination. A window too small slows a fast link, and a window too large never fills on a slow one. That is why it now adapts, and why the progress has to stay monotonic when the window shrinks.

A writeback window

Waiting for the device after every write would be correct but very slow. Waiting only at the end leaves the progress wrong for the whole copy. KIO now does something in between, with sync_file_range(), which lets a program start the writeback of part of a file and wait for it.

The data is written in windows. When a window is full, the worker hands it to a helper thread, which writes it back and waits for it, while the copy fills the next window. The copy only waits when the window before the one it hands over is not written yet. So at most two windows of the copy are ever in memory and not on the device, and the progress bar is never more than one window ahead of what is really written.

The helper thread was needed to accommodate CIFS, the kernel client for Samba and Windows shares. On CIFS, the call that starts the writeback of a window does more than start it: it sends the data to the server, and waits for the server to grant the credits to do so, blocking the calling thread for about 157 ms per 128 MiB window. So the copy could not fill the next window while the previous one was sent. With the helper thread making that call, sending a window and filling the next one overlap.

A window holds about 200 ms of writeback, at the speed measured on the window before. It starts at 1 MiB, can at most double from one window to the next, and stays below 16 MiB, or 128 MiB on CIFS. On a fast link the windows grow, so the copy keeps its speed. On a slow link they stay at about 1 MiB, and anything that has to wait for the file waits a fraction of a second, not for the whole file.

Complete means on the device

A window bounds how much is left in memory, but there is still something left at the end. For a removable device, KIO now also makes the end of the copy wait for it.

A file larger than the largest window, 16 MiB, is flushed with fsync(), and then its folder, before the copy reports it done. By then the window has already written all but the last two windows, so the flush is short.

Small files are the hard part. The obvious fix, an fsync() for every file, made 3000 files of 16 KiB take 10 seconds instead of 0.04 seconds on a local disk. So the worker flushes no small file by itself. After a copy to a removable device, it notifies the job, and the CopyJob sends it one more command before reporting success: write back the whole filesystem of the destination, with one syncfs(). While that runs, the copy notification shows "Writing the copied data to the device...". If the flush fails, the copy fails, because the data may not be on the device.

A move needed more than that. Before, KIO deleted each source file right after its copy, while the copy could still be only in memory, which is why the bug got its highest priority. Now a move to a removable device keeps its sources until the flush has succeeded, and deletes them after it. If the flush fails, or the stick is unplugged during it, every source stays. A source that changed while it waited for the flush stays too, with a warning. A copy or a move that stops on an error, or that you cancel, still flushes the device, and a move then deletes only the sources of the files that are on it.

Here is the same USB stick again, with the small files added:

Copies to a USB 2.0 stick, before and after the change. 128 MiB file: the copy reports complete after 0.06 to 0.13 s before, over four runs, and 28.1 s after; the data is on the stick after 68 to 91 s before and 28.1 s after. 3000 files of 16 KiB: complete after 1.2 s before and 183.9 s after; on the stick after 185.2 s before and 183.9 s after. Progress ahead of the stick: 128 MiB before, 3 MiB after.

Yes, the copies now take longer by the clock, but only because the clock now includes the time the stick spends writing. The time until the data is really on the stick did not get worse. The speed of this stick varies a lot from run to run, so the 28 seconds against 68 to 91 seconds for the large file is not a speedup I would count on. The small files never fill a window, so they only depend on the flush at the end.

Which destinations get a window

A local fixed disk does not need any of this, and keeps the current behaviour exactly. With the removable policy forced onto a local disk for testing, the 3000 small files took 1.55 s against 1.54 s before the change.

Deciding what is removable is less obvious than it sounds. The filesystem type does not expose it: exFAT can be a memory card, and FAT is also the EFI partition of your laptop. Most USB disks do not even report removable media. MR 2506 adds a private function to KIO that asks sysfs: the disk's own removable flag, then the USB port or PCIe slot it hangs off, which the kernel marks removable when it can be reached from outside the machine, and SD cards. It follows encrypted and LVM volumes down to the disk under them.

Network filesystems are easier: every mount that KIO already considers slow gets a window. That is NFS and CIFS, but also Ceph, GlusterFS, 9p, sshfs, and FUSE filesystems such as rclone. The worker finds the mount from the mount id of the destination folder, without a round trip to the server.

Numbers on network shares

To measure this, I used KIO's own copy job, comparing KIO master just before the change with KIO master after it, while another thread listed the destination folder every 50 ms. The SMB server ran on the same machine, with 2 ms of delay added to its traffic, and for the slow link a limit of 40 Mbit/s, about 5 MB/s.

On the slow link, the listing is the difference you would feel:

Listing a folder during a copy of 256 MiB to an SMB share at 5 MB/s, median seconds. Round 1: 11.3 s before the change, 1.7 s after. Round 2: 5.5 s and 1.7 s.

Copy of 256 MiB to an SMB share at 5 MB/s, 2 rounds. Time until complete: 54.2 and 54.4 s before, 55.9 and 55.9 s after, 4.95 MB/s against 4.80 MB/s. Progress ahead of the server: 256 MiB before, 1 MiB after. Slowest listing during the copy: 27.4 and 15.3 s before, 1.70 and 1.70 s after.

The copy runs about 3% slower with the window. On a fast link, the copy keeps up:

Copy of 4 GiB to an SMB share with 2 ms of delay, 5 rounds. Median time until complete: 7.15 s before, 6.06 s after. Largest progress ahead of the server: 1334 MiB before, 128 MiB after. Listing during the copy: median 60.9 ms before and 59.6 ms after, slowest 731 ms before and 111 ms after.

One of the five copies before the change failed with a write error from the share, so these numbers come from four rounds. The times vary between runs: in an earlier run, the copy with the window was 4% slower.

Getting there took me four tries. My first version was 10 to 20% slower on the fast link, because of the CIFS behaviour described above. Then, with the windows aimed at 500 ms, listings on the slow link went up to 4.2 seconds, because each request of a listing waits behind the window being sent. Aiming the windows at 200 ms brought the listings down to 1.7 seconds.

NFS, on the same machine with 2 ms of delay:

Copy of 4 GiB to an NFS share with 2 ms of delay, medians of 3 rounds. Time until complete: 9.8 s before, 10.4 s after. Progress ahead of the server: 14 to 23 MiB before, 1 to 5 MiB after. Listing during the copy: median 24 ms before and 11 ms after, 95th percentile 63 ms before and 40 ms after.

The copy times are close: 11% slower in the first round, and slightly faster in the two others.

Part of a frozen Dolphin

Bug 423187 collects many ways in which a slow mount can freeze Dolphin. In one of its comments, Frank Gehann copied a 100 MB file to an SMB share while browsing it, and found two causes: Dolphin's main thread waiting for answers from the share, and folder listings waiting behind the queued upload.

The window helps with the second cause: each request of a listing now waits behind at most two windows, about 400 ms of writeback, and a whole listing took 1.7 seconds on the 5 MB/s link instead of 5 to 11. The statmount() change helps with part of the first one. KFileItem::isSlow(), which Dolphin calls on its main thread before it reads a file to find its type, no longer waits for any server, on Linux 6.8 or later. Work is underway to fix the rest of the first cause in Frank's open MR 2484 and MR 2485.

The stick that pauses

One measurement sent me down a rabbit hole. During a large copy to the USB stick, listing a folder on the stick sometimes took about 15 seconds, before the change as much as after it. It looked like the writeback window was making something wait.

It was the stick. Roughly every 8 seconds, a single 120 KiB write to it took 4 to 7 seconds to complete, and the listing waited for a page that was stuck behind such a write. KIO cannot fix a device that stops for seconds at a time, but at least we now know the copy is not to blame. In the runs for this post, the slowest listing took 3.6 ms during the copy of the large file, but 7 seconds during the copy of the small files, against 15 seconds before the change.

Copying between two filesystems

One more thing landed along the way, and this one is a plain speedup. In 2021, in Frameworks 5.88, I replaced sendfile() with copy_file_range() in the file worker, in MR 602. At the time, Linux copied with copy_file_range() across filesystems too. Linux 5.19 changed that back: between two filesystems, the call fails again with EXDEV. Since then, a copy from one filesystem to another went through read() and write() in the worker, one buffer at a time.

MR 2520 now falls back to sendfile() in that case, which copies from one file to another inside the kernel:

Copy of 1 GiB between two filesystems, medians of 5 rounds. tmpfs to ext4: 443 ms before, 272 ms after. ext4 to tmpfs: 531 ms before, 278 ms after.

A copy to the USB stick takes as long as before, because its write speed is the limit. This one is merged too, and will also be in KDE Frameworks 6.32.

Closing

With all this, a copy to a USB stick now finishes when the data is actually on the stick. On network filesystems, the progress now follows what the server has received, and the share stays usable during the copy.

sync_file_range() is mainly used by databases and storage engines. No other file manager that I know of uses it. Now KIO, and so Dolphin, uses it too.

Happy unplugging.

Today KDE releases a bugfix update to KDE Plasma 6, versioned 6.8.2.

Plasma 6.8 was released in October 2026 with many feature refinements and new modules to complete the desktop experience.

This release adds a week’s worth of new translations and fixes from KDE’s contributors. The bugfixes are typically small but important and include:

View full changelog

Intro🔗

I’ve been learning Jujutsu (jj), a a version control system that serves as a frontend for git, but changes the workflow itself instead of just being a GUI or a set of aliases.

The thing is cool for many reasons, but the most important one is that workflows like rebase, splitting a commit, changing history or resolving conflicts are so, so much easier than with git. You can see this in I want to move added changes in a working copy ("currently checked out commit") into a parent change and I have a working copy with too many changes and I want to split it into multiple. It also has many quality of life features, like autorebase, autostash, a literal undo and redo command, the default behavior for squashing sends current changes to the parent commit (which is probably 90% of my squash usage pretty much), and it comes with nice TUIs for split, squash, and resolving conflicts.

It has facilitated my git workflow immensely, but it also scared me when it comes to some design decisions. After all, I’m familiar with git and know how to avoid screwing things up with it; I was unfamiliar with jujutsu and didn’t know how to not screw up there.

When I tried to apply it to my KDE workflow, I learned about what it does that I don’t want. Here’s my notes about it, in case anyone is interested in trying it.

For the record, I've done all the following workflows before:

  • keep all changes in semantically split commits regardless of file
  • keep each file into its own commit <-- this is what I do most nowadays because I docs is easier this way
  • squash everything into one commit
  • make a single large commit, then split it into multiple

Maybe I'll write about this in the future, we'll see.


Configuration🔗

This section explains what I feel are required changes to the default Jujutsu settings so it matches my usage.

I don't want jj to track main/master unless I do so explicitly🔗

Jujutsu has a concept of “tracked bookmarks” (where a bookmark is essentially a git branch for our purposes). When you do track a bookmark, every time you push to a remote, all tracked bookmarks get pushed at the same time.

While this is handy for most people (like those who have their own small projects and don't need branches), well, in KDE we usually don’t ever commit to master unless the change is tiny and uncontroversial; instead we put our work in work/<username>/branch-name (a work branch) and make a merge request. And jujutsu defaults to tracking master/main.

If a contributor has the ability to commit to master, this may be a hazard. I can easily see someone making a commit in their local master branch for testing or whatever reason (bisecting? triggering tool behavior? I don't remember what for, but I've done this before), forgetting about it after some time, and then being surprised they pushed to master.

This may be an irrational fear, but I’d rather the tool cater to my own stupidity instead. Hence, I disable tracking the master branch.

[git]
track-default-bookmark-on-clone = false

I don't want jj to automatically track bookmarks at all🔗

I once tried this setting after being confused at the official documentation, whose listed use cases seemed beneficial to me; in reality I don’t want it at all, and I spent quite some time trying to figure out why Jujutsu was not giving me the behavior I wanted.

As mentioned above, every tracked bookmark gets pushed by default when you do a generic jj git push without specifying a bookmark. Like in I don't want jj to track main/master unless I do so explicitly, there can be an accident where you accidentally push a change made to some branch you didn’t intend to push. This is probably more likely to happen here, in fact.

So the jujutsu defaults for this are fine, actually, and I just make a deliberate note here to avoid setting this up, and if I ever do, it will only happen with work branches.

# [remotes-origin]
# auto-track-bookmarks = "work/username/*"

I want jj to advance the bookmark as soon as I commit🔗

Jujutsu doesn’t have a concept of “current branch”. Instead, you work on “detached commits”, in git lingo, and just make a bookmark point to the tip of the last detached commit.

This is of course incredibly annoying to do every time you want to push your branch, so the jujutsu folks implemented a feature to do this: advance branches.

Every time you use jj commit, your branch is automatically advanced; in other words, the new commit becomes the tip of the branch.

For this I want two settings.

I want jj to only advance the bookmark in my work branches (I sometimes work on other people’s branches, I don’t want to push them to the remote accidentally):

[experimental-advance-branches]
enabled-branches = ["glob:work/username/*"]

I don't want jj to ever move the bookmark on main/master unless I do so explicitly (again, for my (irrational) fear of accidentally committing something to master):

[experimental-advance-branches]
disabled-branches = ["main", "glob:push-*"]

When I do want to have these settings to be different, I can just jj config edit --repo to apply that config to my specific repo. I do this for this blog's repository, for example.

I want more descriptive autogenerated bookmarks🔗

Jujutsu does a neat thing where if you never specify a bookmark/branch name, it will simply autogenerate one for you in the push-randomletters format.

I can see why people would want that (in principle you don't need to care about branch names if you always do PRs/MRs immediately), but I don’t want it. It’s ugly and non-descriptive. While not exactly often, KDE contributors sometimes make branches that don’t get an MR until later in life; if I were to browse a list of non-descriptive branch names, it would be more annoying than helpful.

If it ever happens, I want it to be at least somewhat descriptive. So...

In the case I ever intentionally or accidentally push an autogenerated bookmark, I want jj to at least have a more readable default autogenerated branch name based on the commit name without spaces and without single quotes in lowercase:

templates.git_push_bookmark = "\"work/username/\" ++ description.first_line().replace(\" \", \"-\").replace(\"'\", \"\").lower()"

There’s a bit of quote witchcraft going on because TOML, but this means the generated bookmark for a commit named Add setup page would be named work/username/add-setup-page.

This does have the limitation of being based on the last commit (I think?), but come on, I'm not that good with jj yet.


Resulting configuration🔗

The result, including username, email and ssh signing set up, resides in ~/.config/jj/config.toml:

[user]
name = "Your User"
email = "your-email@example.com"

[git]
# Don't want to track main/master
track-default-bookmark-on-clone = false

# [remotes.origin]
# Generic `jj git push` sends it to all tracked bookmarks
# auto-track-bookmarks = "work/username/*"

[templates]
# Any autogenerated bookmarks become work/username/add-setup-page
git_push_bookmark = "\"work/username/\" ++ description.first_line().replace(\" \", \"-\").replace(\"'\", \"\").lower()"

[experimental-advance-branches]
## Automatically runs jj bookmark advance on commit
enabled-branches = ["glob:work/username/*"]
disabled-branches = ["main", "glob:push-*"]

[signing]
behavior = "own"
backend = "ssh"
key = "~/.ssh/mysshkey.pub"

Instead of manually specifying the path to the file, you can just run jj config edit --user.

If you want a programmatic way to set these settings, there's jj config set --user <name> <value>. For example: jj config set --user git.track-default-bookmark-on-clone false.

Workflow🔗

I’m not going to explain how jujutsu works in this blog post.

I am going to link you to the official documentation and to Steve Klabnik’s Jujutsu tutorial instead.

Here in this blog post I’m simply going to show you the jujutsu commands I use for my workflow, based on actions, so you know what you need to do in these situations.

I want to turn an existing cloned git repo into something I can use jj on🔗

jj git init

I want to clone a repo and have it ready for use with jj🔗

jj git clone <repo-link>

I want to create a new branch, push it, and start working on it🔗

jj bookmark create work/username/i18n
jj git push --bookmark work/username/i18n
jj bookmark track work/username/i18n@origin

It’s necessary to track the bookmark so it gets pushed with jj git push later on. Otherwise you may simply jj git push --bookmark <bookmarkname> every time.

I want to create a new empty working copy🔗

jj new

I want to “checkout” (switch?) to a commit, change or branch, no matter if detached or not🔗

jj edit <change-or-bookmark>

This is great. You don’t need to do the whole git dance to edit a previous commit at all:

  • git log
  • git rebase --interactive
  • mark as edit/e
  • make a change
  • git rebase --continue

You just jj edit, edit what you want, then you jj edit back to the tip/branch. All subsequent commits are automatically rebased for you, and jujutsu will warn you of any conflicts.

I want to turn a change into an actual commit and have it be the latest tip of my branch (I’m already working on a branch as set up earlier)🔗

jj new
# Make a change
jj describe
jj bookmark advance

Or, with experimental-advance-branches:

jj new
# Make a change
jj commit

I recommend using jj commit, but it’s good to know that it’s equivalent to jj describe + jj bookmark advance.

I want to move added changes in a working copy ("currently checked out commit") into a parent change🔗

jj squash
# That's the same as
jj squash --from @ --into @-

This is great because I feel the most common use case for a squash is from child to parent. You’d normally do git rebase --interactive and mark a commit as squash/s for this. Here you don’t even need to check the log or rely on tab completion.

Convenient if you’re the type to squash all changes into a single commit, but you really want to jj diff your working copy first to make sure your changes are how you want them.

Instead of switching to a change and editing it, I want to move added changes in a working copy to a specific change, interactively🔗

jj squash --interactive --into <change-id>
# This will modify that change's change-id btw!
# Don't reuse the same id from shell history.

If you know the change ID or commit you want to squash into, you can actually use it with interactive mode, which provides you a nice TUI to select the exact things you want to transfer.

I made a new commit (which automatically creates an empty working copy) and want to merge the previous commit into its parent🔗

# If I don't want to leave the current working copy:
jj squash --from @- --into @--
# Or if I do leave the current working copy:
jj edit @-
jj squash

Here @ is the current working copy (like git’s HEAD) and @- is the working copy’s parent (like HEAD~ or HEAD~1). There’s stuff like @-- which is the parent of the parent too (like HEAD~2).

I have a working copy with too many changes and I want to split it into multiple🔗

jj split --interactive

It doesn't matter if your working copy is an existing named commit or if it's an anonymous working copy.

Let's say your case is the former: after splitting the changes, the ones that were marked will be the only things in the current named commit; the ones that you did not mark will be in a separate commit.

Let's say your case is the latter: after splitting the changes, the ones that were marked will be part of the current working copy, which will get a new name like if you did jj describe; the ones you did not mark will be in a separate commit.

Split does not create a new empty working copy (because you might want to split the commit further).

I want to get back to the parent change🔗

jj edit @-

I want to stash changes in my unnamed working copy and edit something else🔗

jj edit <commit>

Yeah, you don't really do anything. You can just leave that commit and go back to it later, it's autostashed for you. See the next section.

I want to get back to the child change (that's not an empty working copy)🔗

jj edit @-
# Finished, I want to go back!
jj edit @+

Of course, if the child change was an empty working copy, then this command fails with Error: Revset '@+' didn't resolve to any revisions, and you'll need jj new.

However, if there was work in an unnamed working copy before you moved to a parent commit, then trying to edit the child change actually will restore the working copy you had (with a different change id). You can think of this as jj essentially stashing your working copy changes, and restoring them immediately upon editing it again.

Let's think of a similar hypothetical scenario where, after doing jj edit @-, instead of doing jj edit @+ you were to do jj new. What happens then? Rather than abandon the changes, you branch it. The parent @- will actually have two unnamed children (that you can edit with their change ids), and jj log will show this.

This distinction is only briefly mentioned in the official docs despite being awesome, so I wanted to elaborate it here.

I am in an edited working copy but I want to drop changes while keeping the bookmark pointer if there is any🔗

jj abandon --retain-bookmarks

You probably always want to use --retain-bookmarks. Might as well keep that in your shell history instead of jj abandon.

Using jj abandon with experimental-advance-branches means the tip loses the bookmark, and you’ll need to mark it again. With jj abandon --retain bookmarks the bookmark is sent to the previous commit. I wish that was the default when I have experimental-advance-branches enabled.

I made an error that might have been destructive in my repo or history and I need to correct it🔗

# Acts as a literal ctrl + z
jj undo

This reverts the last operation in jj operation log.

Particularly good when you’re resolving a conflict and you accidentally confirm the changes instead of quitting!

If the undo operation itself was a mistake, you can fix that with jj redo, which is a literal ctrl + y.

I wish I had that in git when I was starting... I've had so much trouble even trying to read the damn git reflog.

I want to change the order of my commits🔗

jj arrange

This is usually done with git rebase --interactive. It’s fine. I like that it semantically makes sense, though it takes a bit of effort to lose the muscle memory.

I want to rebase my changes onto up-to-date master🔗

jj git fetch
jj rebase --onto master@origin

No need to git pull --rebase origin master or update your local master branch to in turn rebase your branch locally, jj uses the remote reference directly. It just needs to know what has changed in the remote beforehand (hence the git fetch).

By the way, in KDE we rarely ever do merge commits, or that rebase process involving "merge master into commit" or something. In fact, I'm so used to our workflow that I actually don't know how to rebase that other way!

I want to resolve conflicts that originate from a rebase🔗

# Upon attempting to rebase, an error message will pop up
# Saying to do exactly the following
jj new <first-commit-with-conflicts>
jj resolve
jj diff
# If it's resolved
jj squash
# Continue resolving until everything is done
jj edit <bookmark>
jj git push --bookmark <branch you were working on>

Probably the most elaborate process here.

It helps that jj resolve gives you a nice TUI with individual changed lines with context, like a git frontend TUI would.

I want to view only the log for my current bookmark🔗

jj log --revision 'trunk()..@'

This is similar to:

git log master..HEAD

In both jj and git it can be annoying to see the whole log, especially if the last commit on master/main was your own.