{"version":"https://jsonfeed.org/version/1","title":"Essays — Sympolymathesy, by Chris Krycho","home_page_url":"https://v5.chriskrycho.com/","feed_url":"https://v5.chriskrycho.com/essays/feed.json","description":"Learning in public: on theology, technology, ethics, software, politics, art, and more.","items":[{"id":"https://v5.chriskrycho.com/essays/jj-init/","author":{"name":"Chris Krycho","url":"https://v5.chriskrycho.com/"},"title":"[essays] jj init","url":"https://v5.chriskrycho.com/essays/jj-init/","date_published":"2024-02-02T11:30:00.000-07:00","content_html":"

What if we actually could replace Git? Jujutsu might give us a real shot.\n

Assumed audience: People who have worked with Git or other modern version control systems like Mercurial, Darcs, Pijul, Bazaar, etc., and have at least a basic idea of how they work.\n


Meaningful changes since publication:

Jujutsu is a new version control system from a software engineer at Google, where it is on track to replace Google’s existing version control systems (historically: Perforce, Piper, and Mercurial). I find it interesting both for the approach it takes and for its careful design choices in terms of both implementation details and user interface. It offers one possible answer to a question I first started asking most of a decade ago: What might a next-gen version control system look like — one which actually learned from the best parts of all of this generation’s systems, including Mercurial, Git, Darcs, Fossil, etc.?

\n

To answer that question, it is important to have a sense of what those lessons are. This is trickier than it might seem. Git has substantially the most “mind-share” in the current generation; most software developers learn it and use it not because they have done any investigation of the tool and its alternatives but because it is a de facto standard: a situation which arose in no small part because of its “killer app” in the form of GitHub. Developers who have been around for more than a decade or so have likely seen more than one version control system — but there are many, many developers for whom Git was their first and, so far, last VCS.

\n

The problems with Git are many, though. Most of all, its infamously terrible command line interface results in a terrible user experience. In my experience, very few working developers have a good mental model for Git. Instead, they have a handful of commands they have learned over the years: enough to get by, and little more. The common rejoinder is that developers ought to learn how Git works internally — that everything will make more sense that way.

\n

This is nonsense. Git’s internals are interesting on an implementation level, but frankly add up to an incoherent mess in terms of a user mental model. This is a classic mistake for software developers, and one I have fallen prey to myself any number of times. I do not blame the Git developers for it, exactly. No one should have to understand the internals of the system to use it well, though; that is a simple failure of software design. Moreover, even those internals do not particularly cohere. The index, the number of things labeled “-ish” in the glossary, the way that a “detached HEAD” interacts with branches, the distinction between tags and branches, the important distinctions between commits, refs, and objects… It is not that any one of those things is bad in isolation, but as a set they do not amount to a mental model I can describe charitably. Put in programming language terms: One of the reasons the “surface syntax” of Git is so hard is that its semantics are a bit confused, and that inevitably shows up in the interface to users.

\n

Still, a change in a system so deeply embedded in the software development ecosystem is not cheap. Is it worth the cost of adoption? Well, Jujutsu has a trick up its sleeve: there is no adoption cost. You just install it — brew install jj will do the trick on macOS — and run a single command in an existing Git repository, and… that’s it. (“There is no step 3.”) I expect that mode will always work, even though there will be a migration step at some point in the future, when Jujutsu’s own, non-Git backend becomes a viable — and ultimately the recommended — option. I am getting ahead of myself though. The first thing to understand is what Jujutsu is, and is not.

\n

Jujutsu is two things:

\n
    \n
  1. \n

    It is a new front-end to Git. This is by far the less interesting of the two things, but in practice it is a substantial part of the experience of using the tool today. In this regard, it sits in the same notional space as something like gitoxide. Jujutsu’s jj is far more usable for day to day work than gitoxide’s gix and ein so far, though, and it also has very different aims. That takes us to:

    \n
  2. \n
  3. \n

    It is a new design for distributed version control. This is by far the more interesting part. In particular, Jujutsu brings to the table a few key concepts — none of which are themselves novel, but the combination of which is really nice to use in practice:

    \n\n
  4. \n
\n

The combo of those means that you can use it today in your existing Git repos, as I have been for the past six months, and that it is a really good experience using it that way. (Better than Git!) Moreover, given it is being actively developed at and by Google for use as a replacement for its current custom VCS setup, it seems like it has a good future ahead of it. Net: at a minimum you get a better experience for using Git with it. At a maximum, you get an incredibly smooth and shallow on-ramp to what I earnestly hope is the future of version control.

\n

Jujutsu is not trying to do every interesting thing that other Git-alternative DVCS systems out there do. Unlike Pijul, for example, it does not work from a theory of patches such that the order changes are applied is irrelevant. However, as I noted above and show in detail below, jj does distinguish between changes and revisions, and has first-class support for conflicts, which means that many of the benefits of Pijul’s handling come along anyway. Unlike Fossil, Jujutsu is also not trying to be an all-in-one tool. Accordingly: It does not come with a replacement for GitHub or other such “forges”. It does not include bug tracking. It does not support chat or a forum or a wiki. Instead, it is currently aimed at just doing the base VCS operations well.

\n

Finally, there is a thing Jujutsu is not yet: a standalone VCS ready to use without Git. It supports its own, “native” backend for the sake of keeping that door open for future capabilities, and the test suite exercises both the Git and the “native” backend, but the “native” one is not remotely ready for regular use. That said, this one I do expect to see change over time!

\n

One of the really interesting bits about picking up Jujutsu is realizing just how weirdly Git has wired your brain, and re-learning how to think about how a version control system can work. It is one thing to believe — very strongly, in my case! — that Git’s UI design is deeply janky (and its underlying model just so-so); it is something else to experience how much better a VCS UI can be (even without replacing the underlying model!).

\n\"Yoda\n

Time to become a Jedi Knight. Jujutsu Knight? Jujutsu Master? Jujutsu apprentice, at least. Let’s dig in!

\n

Outline

\n\n

Using Jujutsu

\n

Since I published this in early 2024, many details about Jujutsu have changed — especially around specific CLI invocations — , and a fair number of the papercuts have been fixed. The big picture is the same as it was, but I can even more strongly recommend it than I did originally.

\n
\n

That is all interesting enough philosophically, but for a tool that, if successful, will end up being one of a software developer’s most-used tools, there is an even more important question: What is it actually like to use?

\n

Setup is painless. Running brew install jj did everything I needed. As with most modern Rust-powered CLI tools,1 Jujutsu comes with great completions right out of the box. I did make one post-install tweak, since I am going to be using this on existing Git projects: I updated my ~/.gitignore_global to ignore .jj directories anywhere on disk.2

\n

Using Jujutsu in an existing Git project is also quite easy.3 You just run jj git init --git-repo <path to repo>.4 That’s the entire flow. After that you can use git and jj commands alike on the repository, and everything Just Works™, right down to correctly handling .gitignore files. I have since run jj git init in every Git repository I am actively working on, and have had no issues in many months. It is also possible to initialize a Jujutsu copy of a Git project without having an existing Git repo, using jj git clone, which I have also done, and which works well.

\n
\n\n
Cloning true-myth and initializing it as a Jujutsu repo
\n
\n

Once a project is initialized, working on it is fairly straightforward, though there are some significant adjustments required if you have deep-seated habits from Git!

\n

Revisions and revsets

\n

One of the first things to wrap your head around when first coming to Jujutsu is its approach to its revisions and revsets, i.e. “sets of revision”. Revisions are the fundamental elements of changes in Jujutsu, not “commits” as in Git. Revsets are then expressions in a functional language for selecting a set of revisions. Both the idea and the terminology are borrowed directly from Mercurial, though the implementation is totally new. (Many things about Jujutsu borrow from Mercurial — a decision which makes me quite happy.) The vast majority of Jujutsu commands take a --revision/-r command to select a revision. So far that might not sound particularly different from Git’s notion of commits and commit ranges, and they are indeed similar at a surface level. However, the differences start showing up pretty quickly, both in terms of working with revisions and in terms of how revisions are a different notion of change than a Git commit.

\n

The first place you are likely to experience how revisions and revsets are different — and neat! — is with the log command, since looking at the commit log is likely to be something you do pretty early in using a new version control tool. (Certainly it was for me.) When you clone a repo and initialize Jujutsu in it and then run jj log, you will see something rather different from what git log would show you — indeed, rather different from anything I even know how to get git log to show you. For example, here’s what I see today when running jj log on the Jujutsu repository, limiting it to show just the last 10 revisions:

\n
> jj log --limit 10\n@  ukvtttmt [email protected] 2024-02-03 09:37:24.000 -07:00 1a0b8773\n│  (empty) (no description set)\nâ—‰  qppsqonm [email protected] 2024-02-03 15:06:09.000 +00:00 main* HEAD@git bcdb9beb\n·  cli: Move git_init() from init.rs to git.rs\n· â—‰  rzwovrll [email protected] 2024-02-01 14:25:17.000 -08:00\n┌─┘  ig/contributing@origin 01e0739d\n│    Update contributing.md\nâ—‰  nxskksop 49699333+dependabot[bot]@users.noreply.github.com 2024-02-01 08:56:08.000\n·  -08:00 fb6c834f\n·  cargo: bump the cargo-dependencies group with 3 updates\n· â—‰  tlsouwqs [email protected] 2024-02-02 21:26:23.000 -08:00\n· │  jt/missingop@origin missingop@origin 347817c6\n· │  workspace: recover from missing operation\n· â—‰  zpkmktoy [email protected] 2024-02-02 21:16:32.000 -08:00 2d0a444e\n· │  workspace: inline is_stale()\n· â—‰  qkxullnx [email protected] 2024-02-02 20:58:21.000 -08:00 7abf1689\n┌─┘  workspace: refactor for_stale_working_copy\nâ—‰  yyqlyqtq [email protected] 2024-01-31 09:40:52.000 +09:00 976b8012\n·  index: on reinit(), delete all segment files to save disk space\n· â—‰  oqnvqzzq [email protected] 2024-01-23 10:34:16.000 -08:00\n┌─┘  push-oznkpsskqyyw@origin 54bd70ad\n│    working_copy: make reset() take a commit instead of a tree\nâ—‰  rrxuwsqp [email protected] 2024-01-23 08:59:43.000 -08:00 57d5abab\n·  cli: display which file's conflicts are being resolved\n
\n

Here’s the output for the same basic command in Git — note that I am not trying to get a similar output from Git, just asking what it shows by default (and warning: wall of log output!):

\n
> git log -10\ncommit: bcdb9beb6ce5ba625ae73d4839e4574db3d9e559     HEAD -> main, origin/main\ndate:   Mon, 15 Jan 2024 22:31:33 +0000\nauthor: Essien Ita Essien \n\n    cli: Move git_init() from init.rs to git.rs\n\n    * Move git_init() to cli/src/commands/git.rs and call it from there.\n    * Move print_trackable_remote_branches into cli_util since it's not git specific,\n      but would apply to any backend that supports remote branches.\n    * A no-op change. A follow up PR will make use of this.\n\ncommit: 31e4061bab6cfc835e8ac65d263c29e99c937abf\ndate:   Mon, 8 Jan 2024 10:41:07 +0000\nauthor: Essien Ita Essien \n\n    cli: Refactor out git_init() to encapsulate all git related work.\n\n    * Create a git_init() function in cli/src/commands/init.rs where all git related work is done.\n      This function will be moved to cli/src/commands/git.rs in a subsequent PR.\n\ncommit: 8423c63a0465ada99c81f87e06f833568a22cb48\ndate:   Mon, 8 Jan 2024 10:41:07 +0000\nauthor: Essien Ita Essien \n\n    cli: Refactor workspace root directory creation\n\n    * Add file_util::create_or_reuse_dir() which is needed by all init\n      functionality regardless of the backend.\n\ncommit: b3c47953e807bef202d632c4e309b9a8eb814fde\ndate:   Wed, 31 Jan 2024 20:53:23 -0800\nauthor: Ilya Grigoriev \n\n    config.md docs: document `jj config edit` and `jj config path`\n\n    This changes the intro section to recommend using `jj config edit` to\n    edit the config instead of looking for the files manually.\n\ncommit: e9c482c0176d5f0c0c28436f78bd6002aa23a5e2\ndate:   Wed, 31 Jan 2024 20:53:23 -0800\nauthor: Ilya Grigoriev \n\n    docs: mention in `jj help config edit` that the command can create a file\n\n\ncommit: 98948554f72d4dc2d5f406da36452acb2868e6d7\ndate:   Wed, 31 Jan 2024 20:53:23 -0800\nauthor: Ilya Grigoriev \n\n    cli `jj config`: add `jj config path` command\n\n\ncommit: 8a4b3966a6ff6b9cc1005c575d71bfc7771bced1\ndate:   Fri, 2 Feb 2024 22:08:00 -0800\nauthor: Ilya Grigoriev \n\n    test_global_opts: make test_version just a bit nicer when it fails\n\n\ncommit: 42e61327718553fae6b98d7d96dd786b1f050e4c\ndate:   Fri, 2 Feb 2024 22:03:26 -0800\nauthor: Ilya Grigoriev \n\n    test_global_opts: extract --version to its own test\n\n\ncommit: 42c85b33c7481efbfec01d68c0a3b1ea857196e0\ndate:   Fri, 2 Feb 2024 15:23:56 +0000\nauthor: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>\n\n    cargo: bump the cargo-dependencies group with 1 update\n\n    Bumps the cargo-dependencies group with 1 update: [tokio](https://github.com/tokio-rs/tokio).\n\n\n    Updates `tokio` from 1.35.1 to 1.36.0\n    - [Release notes](https://github.com/tokio-rs/tokio/releases)\n    - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.35.1...tokio-1.36.0)\n\n    ---\n    updated-dependencies:\n    - dependency-name: tokio\n      dependency-type: direct:production\n      update-type: version-update:semver-minor\n      dependency-group: cargo-dependencies\n    ...\n\n    Signed-off-by: dependabot[bot] \ncommit: 32c6406e5f04d2ecb6642433b0faae2c6592c151\ndate:   Fri, 2 Feb 2024 15:22:21 +0000\nauthor: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>\n\n    github: bump the github-dependencies group with 1 update\n\n    Bumps the github-dependencies group with 1 update: [DeterminateSystems/magic-nix-cache-action](https://github.com/determinatesystems/magic-nix-cache-action).\n\n\n    Updates `DeterminateSystems/magic-nix-cache-action` from 1402a2dd8f56a6a6306c015089c5086f5e1ca3ef to eeabdb06718ac63a7021c6132129679a8e22d0c7\n    - [Release notes](https://github.com/determinatesystems/magic-nix-cache-action/releases)\n    - [Commits](https://github.com/determinatesystems/magic-nix-cache-action/compare/1402a2dd8f56a6a6306c015089c5086f5e1ca3ef...eeabdb06718ac63a7021c6132129679a8e22d0c7)\n\n    ---\n    updated-dependencies:\n    - dependency-name: DeterminateSystems/magic-nix-cache-action\n      dependency-type: direct:production\n      dependency-group: github-dependencies\n    ...\n\n    Signed-off-by: dependabot[bot] \n41898282+github-actions[bot]@users.noreply.github.com>41898282+github-actions[bot]@users.noreply.github.com>
\n

What’s happening in the Jujutsu log output? Per the tutorial’s note on the log command specifically:

\n
\n

By default, jj log lists your local commits, with some remote commits added for context. The ~ indicates that the commit has parents that are not included in the graph. We can use the -r flag to select a different set of revisions to list.

\n
\n

What jj log does show by default was still a bit non-obvious to me, even after that. Which remote commits added for context, and why? The answer is in the help output for jj log’s -r/--revisions option:

\n
\n

Which revisions to show. Defaults to the ui.default-revset setting, or @ | ancestors(immutable_heads().., 2) | heads(immutable_heads()) if it is not set

\n
\n

I will come back to this revset in a moment to explain it in detail. First, though, this shows a couple other interesting features of Jujutsu’s approach to revsets and thus the log command. First, it treats some of these operations as functions (ancestors(), immutable_heads(), etc.). There is a whole list of these functions! This is not a surprise if you think about what “expressions in a functional language” implies… but it was a surprise to me because I had not yet read that bit of documentation. Second, it makes “operators” a first-class idea. Git has operators, but this goes a fair bit further:

\n\n

Now, I used <id> here, but throughout these actually operate on revsets, so you could use them with any revset. For example, ..tags() will give you the ancestors of all tags. This strikes me as extremely interesting: I think it will dodge a lot of pain in dealing with Git histories, because it lets you ask questions about the history in a compositional way using normal set logic. To make that concrete: back in October, Jujutsu contributor @aseipp pointed out how easy it is to use this to get a log which excludes gh-pages. (Anyone who has worked on a repo with a gh-pages branch knows how annoying it is to have it cluttering up your view of the rest of your Git history!) First, you define an alias for the revset that only includes the gh-pages branch: 'gh-pages' = 'remote_branches(exact:"gh-pages")'. Then you can exclude it from other queries with the ~ negation operator: jj log -r "all() ~ ancestors(gh-pages)" would give you a log view for every revision with all() and then exclude every ancestor of the gh-pages branch.

\n

Jujutsu also provides a really capable templating system, which uses “a functional language to customize output of commands”. That functional language is built on top of the functional language that the whole language uses for describing revisions (described in brief above!), so you can use the same kinds of operators in templates for output as you do for navigating and manipulating the repository. The template format is still evolving, but you can use it to customize the output today… while being aware that you may have to update it in the future. Keywords include things like description and change_id, and these can be customized in Jujutsu’s config. For example, I made this tweak to mine, overriding the built-in format_short_id alias:

\n
[template-aliases]\n'format_short_id(id)' = 'id.shortest()'\n
\n

This gives me super short names for changes and commits, which makes for a much nicer experience when reading and working with both in the log output: Jujutsu will give me the shortest unique identifier for a given change or commit, which I can then use with commands like jj new. Additionally, there are a number of built-in templates. For example, to see the equivalent of Git’s log --pretty you can use Jujutsu’s log -T builtin_log_detailed (-T for “template”; you can also use the long from --template). You can define your own templates in a [templates] section, or add your own [template-aliases] block, using the template language and any combination of further functions you define yourself.

\n

That’s all well and good, but even with reading the docs for the revset language and the templating language, it still took me a bit to actually quite make sense out of the default output, much less to get a handle on how to customize the output. Right now, the docs have a bit of a flavor of explanations for people who already have a pretty good handle on version control systems, and the description of what you get from jj log is a good example of that. As the project gains momentum, it will need other kinds of more-introductory material, but the current status is totally fair and reasonable for the stage the project is at. And, to be fair to Jujutsu, both the revset language and the templating language are incredibly easier to understand and work with than the corresponding Git materials.

\n

Returning to the difference between the default output from jj log and git log, the key is that unless you pass -r, Jujutsu uses the ui.default-revset selector to provide a much more informative view than git log does. Again, the default is @ | ancestors(immutable_heads().., 2) | heads(immutable_heads()). Walking through that:

\n\n

When you put those all together, your log view will always show your current head change, all the open branches which have not been merged into your trunk branch, and whatever you have configured to be immutable — out of the box, trunk and all tags. That is vastly more informative than git log’s default output, even if it is a bit surprising the first time you see it. Nor is it particularly possible to get that in a single git log command. By contrast, getting the equivalent of git log is trivial.

\n

To show the full history for a given change, you can use the :: ancestors operator. Since jj log always gives you the identifier for a revision, you can follow it up with jj log --revision ::<change id>, or jj log -r ::<change id> for short. For example, in one repo where I am trying this, the most recent commit identifier starts with mwoq (Jujutsu helpfully highlights the segment of the change identifier you need to use), so I could write jj log -r ::mwoq, and this will show all the ancestors of mwoq, or jj log -r ..mwoq to get all the ancestors of the commit except the root. (The root is uninteresting.) Net, the equivalent command for “show me all the history for this commit” is:

\n
$ jj log -r ..@\n
\n

Revsets are very powerful, very flexible, and yet much easier to use than Git’s operators. That is in part because of the language used to express them. It is also in part because revsets build on a fundamentally different view of the world than Git commits: Jujutsu’s idea of changes.

\n

Changes

\n

In Git, as in Subversion and Mercurial and other version control systems before them, when you finish with a change, you commit it. In Jujutsu, there is no first-class notion of “committing” code. This took me a fair bit to wrap my head around! Instead, Jujutsu has two discrete operations: describe and new. jj describe lets you provide a descriptive message for any change. jj new starts a new change. You can think of git commit --message "something I did" as being equivalent to jj describe --message "some I did" && jj new. This falls out of the fact that jj describe and jj new are orthogonal, and much more capable than git commit as a result.

\n

The describe command works on any commit. It defaults to the commit that is the current working copy. If you want to rewrite a message earlier in your commit history, though, that is not a special operation like it is in Git, where you have to perform an interactive rebase to do it. You just call jj describe with a --revision (or -r for short, as everywhere in Jujutsu) argument. For example:

\n
# long version\n$ jj describe --revision abcd --message "An updated message."\n\n# short version\n$ jj describe -r abcd -m "An updated message."\n
\n

That’s it. How you choose to integrate that into your workflow is a matter for you and your team to decide, of course. Jujutsu understands that some branches should not have their history rewritten this way, though, and lets you specify what the “immutable heads” revset should be accordingly. This actually makes it safer than Git, where the tool itself does not understand that kind of immutability and we rely on forges to protect certain branches from being targeted by a force push.

\n

The new command is the core of creating any new change, and it does not require there to be only a single parent. You can create a new change with as many parents as is appropriate! Is a given change logically the child of four other changes, with identifiers a, b, c, and d? jj new a b c d. That’s it. One neat consequence that falls out of this: a merge in Jujutsu is just jj new with the requirement that it have at least two parents. (“At least two parents” because having multiple parents for a merge is not a special case as with Git’s “octopus” merges.) Likewise, you do not need a commit command, because you can describe a given change at any time with describe, and you can create a new change at any time with new. If you already know the next thing you are going to do, you can even describe it by passing -m/--message to new when creating the new change!6

\n
\n\n
A demo of using jj new to create a three-parent merge
\n
\n

Most of the time with Git, I am doing one of two things when I go to commit a change:

\n\n

In the first case, Jujutsu’s choice to skip Git’s “index” looks like a very good one. In the second case, I was initially skeptical. Once I got the hang of working this way, though, I started to come around. My workflow with Fork looks an awful lot like the workflow that Jujutsu pushes you toward with actually using a diff tool. With Jujutsu, though, any diff tool can work. Want to use Vim? Go for it.

\n

What is more, Jujutsu’s approach to the working copy results in a really interesting shift. In every version control system I have worked with previously (including CVS, PVCS, SVN), the workflow has been some variation on:

\n\n

With both Mercurial and Git, it also became possible to rewrite history in various ways. I use Git’s rebase --interactive command extensively when working on large sets of changes. (I did the same with Mercurial’s history rewriting when I was using it a decade ago.) That expanded the list of common operations to include two more:

\n\n

Jujutsu flips all of that on its head. A change, not a commit, is the fundamental element of the mental and working model. That means that you can describe a change that is still “in progress” as it were. I discovered this while working on a little example code for a blog post I plan to publish later this month: you can describe the change you are working on and then keep working on it. The act of describing the change is distinct from the act of “committing” and thus starting a new change. This falls out naturally from the fact that the working copy state is something you can operate on directly: akin to Git’s index, but without its many pitfalls. (This simplification affects a lot of things, as I will discuss further below; but it is especially important for new learners. Getting my head around the index was one of those things I found quite challenging initially with Git a decade ago.)

\n

When you are ready to start a new change, you use either jj commit to “finalize” this commit with a message, or jj new to “Create a new, empty change and edit it in the working copy”. Implied: jj commit is just a convenience for jj describe followed by jj new. And a bonus: this means that rewording a message earlier in history does not involve some kind of rebase operation; you just jj describe --revision <target>.

\n

What is more, jj new lets you create a new commit anywhere in the history of your project, trivially:

\n
-A, --insert-after\n      Insert the new change between the target commit(s) and their children\n\n      [aliases: after]\n\n-B, --insert-before\n      Insert the new change between the target commit(s) and their parents\n\n      [aliases: before]\n
\n

You can do this using interactive rebasing with Git (or with history rewriting with Mercurial, though I am afraid my hg is rusty enough that I do not remember the details). What you cannot do in Git specifically is say “Start a new change at point x” unless you are in the middle of a rebase operation, which makes it inherently somewhat fragile. To be extra clear: Git allows you to check out make a new change at any point in your graph, but it creates a branch at that point, and none of the descendants of that original point in your commit graph will come along without explicitly rebasing. Moreover, even once you do an explicit rebase and cherry-pick in the commit, the original commit is still hanging out, so you likely need to delete that branch. With jj new -A <some change ID>, you just insert the change directly into the history. Jujutsu will rebase every child in the history, including any merges if necessary; it “just works”. That does not guarantee you will not have conflicts, of course, but Jujutsu also handles conflicts better — way better — than Git. More on that below.

\n

I never use git reflog so much as when doing interactive rebases. Once I got the hang of Jujutsu’s ability to jj new anywhere, it basically obviates most of the places I have needed Git’s interactive rebase mode, especially when combined with Jujutsu’s aforementioned support for “first-class conflicts”. There is still an escape hatch for mistakes, though: jj op log shows all the operations you have performed on the repo — and frankly, is much more useful and powerful than git reflog, because it logs all the operations, including whenever Jujutsu updates its view of your working copy via jj status, when it fetches new revisions from a remote.

\n

Additionally, Jujutsu allows you to see how any change has evolved over time. This handily solves multiple pain points in Git. For example, if you have made changes in your working copy, and would like to split it into multiple changes, Git only has a binary state to let you tease those apart: staged, or not. As a result, that kind of operation ranges in difficulty from merely painful to outright impossible. With its obslog command,8 Jujutsu allows you to see how a change has evolved over time. Since the working copy is just one more kind of “change”, you can very easily retrieve earlier state — any time you did a jj status check, or any other command which snapshotted the state of the repository (which is most of them). That applies equally to earlier changes. If you just rebased, for example, and realize you moved some changes to code into the wrong revision, you can use the combination of obslog and new and restore (or move) to pull it back apart into the desired sequence of changes. (This one is hard to describe, so I may put up a video of it later!)

\n

Split

\n

This also leads to another significant difference with Git: around breaking up your current set of changes on disk. As I noted above, Jujutsu treats the working copy itself as a commit instead of having an “index” like Git. Git really only lets you break apart a set of changes with the index, using git add --patch. Jujutsu instead has a split command, which launches a diff editor and lets you select what you want to incorporate — rather like git add --patch does. As with all of its commands, though, jj split works exactly the same way on any commit; the working copy commit gets it “for free”.

\n

Philosophically, I really like this. Practically, though, it is a slightly bumpier experience for me than the Git approach at the moment. Recall that I do not use git add --patch directly. Instead, I always stage changes into the Git index using a graphical tool like Fork. That workflow is slightly nicer than editing a diff — at least, as Jujutsu does it today. In Fork (and similar tools), you start with no changes and add what you want to the change set you want. By contrast, jj split launches a diff view with all the changes from a given commit present: splitting the commit involves removing changes from the right side of the diff so that it has only the changes you want to be present in the first of two new commits; whatever is not present in the final version of the right side when you close your diff editor ends up in the second commit.

\n

If this sounds a little complicated, that is because it is — at least for today. That qualifier is important, because a lot of this is down to tooling, and we have about as much dedicated tooling for Jujutsu as Git had in 2007, which is to say: not much. Qualifier notwithstanding, and philosophical elegance notwithstanding, the complexity is still real here in early 2024. There are two big downsides as things stand. First, I find it comes with more cognitive load. It requires thinking in terms of negation rather than addition, and the “second commit” becomes less and less visible over time as you remove it from the first commit. Second, it requires you to repeat the operation when breaking up something into more than two commits. I semi-regularly take a single bucket of changes on disk and chunk it up into many more than just 2 commits, though! That significantly multiplies the cognitive overhead.

\n

Now, since I started working with Jujutsu, the team has switched the default view for working with these kinds of diffs to using scm-diff-editor, a TUI which has a first-class notion of this kind of workflow.9 That TUI works reasonably well, but is much less pleasant to use than something like the nice GUIs of Fork or Tower.

\n

The net is: when I want to break apart changes, at least for the moment I find myself quite tempted to go back to Fork and Git’s index. I do not think this problem is intractable, and I think the idea of jj split is right. It just — “just”! — needs some careful design work. Preferably, the split command would make it straightforward to generate an arbitrary number of commits from one initial commit, and it would allow progressive creation of each commit from a “vs. the previous commit” baseline. This is the upside of the index in Git: it does actually reflect the reality that there are three separate “buckets” in view when splitting apart a change: the baseline before all changes, the set of all the changes, and the set you want to include in the commit. Existing diff tools do not really handle this — other than the integrated index-aware diff tools in Git clients, which then have their own oddities when interacting with Jujutsu, since it ignores the index.

\n

First-class conflicts

\n

Another huge feature of Jujutsu is its support for first-class conflicts. Instead of a conflict resulting in a nightmare that has to be resolved before you can move on, Jujutsu can incorporate both the merge and its resolution (whether manual or automatic) directly into commit history. Just having the conflicts in history does not seem that weird. “Okay, you committed the text conflict markers from git, neat.” But: having the conflict and its resolution in history, especially when Jujutsu figured out how to do that resolution for you, as part of a rebase operation? That is just plain wild.

\n

A while back, I was working on a change to a library I maintain10 and decided to flip the order in which I landed two changes to package.json. Unfortunately, those changes were adjacent to each other in the file and so flipping the order they would land in seemed likely to be painfully difficult. It was actually trivial. First of all, the flow itself was great: instead of launching an editor for interactive rebase, I just explicitly told Jujutsu to do the rebases: jj rebase --revision <source> --destination <target>. I did that for each of the items I wanted to reorder and I was done. (I could also have rebased a whole series of commits; I just did not need to in this case.) Literally, that was it: because Jujutsu had agreed with me that JSON is a terrible format for changes like this and committed a merge conflict, then resolved the merge conflict via the next rebase command, and simply carried on.

\n

At a mechanical level, Jujutsu will add conflict markers to a file, not unlike those Git adds in merge conflicts. However, unlike Git, those are not just markers in a file. They are part of a system which understands what conflicts are semantically, and therefore also what resolving a conflict is semantically. This not only produces nice automatic outcomes like the one I described with my library above; it also means that you have more options for how to accomplish a resolution, and for how to treat a conflict. Git trains you to see a conflict between two branches as a problem. It requires you to solve that problem before moving on. Jujutsu allows you to treat a conflict as a problem which much be resolved, but it does not require it. Resolving conflicts in merges in Git is often quite messy. It is even worse when rebasing. I have spent an incredibly amount of time attempting merges only to give up and git reset --hard <before the merge>, and possibly even more time trying to resolve a conflicting in a rebase only to bail with git rebase --abort. Jujutsu allows you to create a merge, leave the conflict in place, and then introduce a resolution in the next commit, telling the whole story with your change history.

\n
\n\n
Conflict resolution with merges
\n
\n

Likewise with a rebase: depending on whether you require all your intermediate revisions to be able to be built or would rather show a history including conflicts, you could choose to rebase, leave all the intermediate changes conflicted, and resolve it only at the end.

\n
\n\n
Conflict resolution with rebases
\n
\n

Conflicts are inevitable when you have enough people working on a repository. Honestly: conflicts happen when I am working alone in a repository, as suggested by my anecdote above. Having this ability to keep working with the repository even in a conflicted state, as well as to resolve the conflicts in a more interactive and iterative way is something I now find difficult to live without.

\n

Changing changes

\n

There are a few other niceties which fall out of Jujutsu’s distinction between changes and commits, especially when combined with first-class conflicts.

\n

First up, jj squash takes all the changes in a given commit and, well, squashes them into the parent of that commit.11 Given a working copy with a bunch of changes, you can move them straight into the parent by just typing jj squash. If you want to squash some change besides the one you are currently editing, you just pass the -r/--revision flag, as with most Jujutsu commands: jj squash -r abc will squash the change identified by abc into its parent. You can also use the --interactive (-i for short) argument to move just a part of a change into its parent. Using that flag will pop up your configured diff editor just like jj split will and allow you to select which items you want to move into the parent and which you want to keep separate. Or, for an even faster option, if you have specific files to move while leaving others alone, and you do not need to handle subsections of those files, you can pass them as the final arguments to the command, like jj squash ./path/a ./path/c.

\n

As it turns out, this ability to move part of one change into a different change is a really useful thing to be able to do in general. I find it particularly handy when building up a set of changes where I want each one to be coherent — say, for the sake of having a commit history which is easy for others to review. You could do that by doing some combination of jj split and jj new --after <some change ID> and then doing jj rebase to move around the changes… but as usual, Jujutsu has a better way. The squash command is actually just a shortcut for Jujutsu’s move command with some arguments filled in. The move command has --from and --to arguments which let you specify which revisions you want to move between. When you run jj squash with no other arguments, that is the equivalent of jj move --from @ --to @-. When you run jj squash -r abc, that is the equivalent of jj move --from abc --to abc-. Since it takes those arguments explicitly, though, move lets you move changes around between any changes. They do not need to be anywhere near each other in history.

\n
\n\n
A demo of using jj move
\n
\n

This eliminates another entire category of places I have historically had to reach for git rebase --interactive. While there are still a few times where I think Jujutsu could use something akin to Git’s interactive rebase mode, they are legitimately few, and mostly to do with wanting to be able to do batch reordering of commits. To be fair, though, I only want to do that perhaps a few times a year.

\n

Branches

\n

Branches are another of the very significant differences between Jujutsu and Git — another place where Jujutsu acts a bit more like Mercurial, in fact. In Git, everything happens on named branches. You can operate on anonymous branches in Git, but it will yell at you constantly about being on a “detached HEAD”. Jujutsu inverts this. The normal working mode in Jujutsu is just to make a series of changes, which then naturally form “branches” in the change graph, but which do not require a name out of the gate. You can give a branch a name any time, using jj branch create. That name is just a pointer to the change you pointed it at, though; it does not automatically “follow” you as you do jj new to create new changes. (Readers familiar with Mercurial may recognize that this is very similar to its bookmarks), though without the notion of “active” and “inactive” bookmarks.)

\n

To update what a branch name points to, you use the branch set command. To completely get rid of a branch, including removing it from any remotes you have pushed the branch to, you use the branch delete command. Handily, if you want to forget all your local branch operations (though not the changes they apply to), you can use the branch forget command. That can come in useful when your local copy of a branch has diverged from what is on the remote and you don’t want to reconcile the changes and just want to get back to whatever is on the remote for that branch. No need for git reset --hard origin/<branch name>, just jj branch forget <branch name> and then the next time you pull from the remote, you will get back its view of the branch!

\n
\n
\n\n
\n
It’s not just me who wants this!
\n
\n

Jujutsu’s defaulting to anonymous branches took me a bit to get used to, after a decade of doing all of my work in Git and of necessity having to do my work on named branches. As with so many things about Jujutsu, though, I have very much come to appreciate this default. In particular,I find this approach makes really good sense for all the steps where I am not yet sharing a set of changes with others. Even once I am sharing the changes with others, Git’s requirement of a branch name can start to feel kind of silly at times. Especially for the case where I am making some small and self-contained change, the name of a given branch is often just some short, snake-case-ified version of the commit message. The default log template shows me the current set of branches, and their commit messages are usually sufficiently informative that I do not need anything else.

\n

However, there are some downsides to this approach in practice, at least given today’s ecosystem. First, the lack of a “current branch” makes for some extra friction when working with tools like GitHub, GitLab, Gitea, and so on. The GitHub model (which other tools have copied) treats branches as the basis for all work. GitHub displays warning messages about commits which are not on a branch, and will not allow you to create a pull request from an anonymous branch. In many ways, this is simply because Git itself treats branches as special and important. GitHub is just following Git’s example of loud warnings about being on a “detached HEAD” commit, after all.

\n

What this means in practice, though, is that there is an extra operation required any time you want to push your changes to GitHub or a similar forge. With Git, you simply git push after making your changes. (More on Git interop below.) Since Git keeps the current branch pointing at the current HEAD, Git aliases git push with no arguments to git push <configured remote for current branch> <current branch>. Jujutsu does not do this, and given how its branching model works today, cannot do this, because named branches do not “follow” your operations. Instead, you must first explicitly set the branch to the commit you want to push. In the most common case, where you are pushing your latest set of changes, that is just jj branch set <branch name>; it takes the current change automatically. Only then can you run jj git push to actually get an update. This is only a paper cut, but it is a paper cut. It is one extra command every single time you go to push a change to share with others, or even just to get it off of your machine.12 That might not seem like a lot, but it adds up.

\n

There is a real tension in the design space here, though. On the one hand, the main time I use branches in Jujutsu at this point is for pushing to a Git forge like GitHub. I rarely feel the need for them for just working on a set of changes, where jj log and jj new <some revision> give me everything I need. In that sense, it seems like having the branch “follow along” with my work would be natural: if I have gone to the trouble of creating a name for a branch and pushing it to some remote, then it is very likely I want to keep it up to date as I add changes to the branch I named. On the other hand, there is a big upside to not doing that automatically: pushing changes becomes an intentional act. I cannot count the number of times I have been working on what is essentially just an experiment in a Git repo, forgotten to change from the foo-feature to a new foo-feature-experiment branch, and then done a git push. Especially if I am collaborating with others on foo-feature, now I have to force push back to the previous to reset things, and let others know to wait for that, etc. That never happens with the Jujutsu model. Since updating a named branch is always an intentional act, you can experiment to your heart’s content, and know you will never accidentally push changes to a branch that way. I go back and forth: Maybe the little bit of extra friction when you do want to push a branch is worth it for all the times you do not have to consciously move a branch backwards to avoid pushing changes you are not yet ready to share.

\n

(As you might expect, the default of anonymous branches has some knock-on effects for how it interacts with Git tooling in general; I say more on this below.)

\n

Jujutsu also has a handy little feature for when you have done a bunch of work on an anonymous branch and are ready to push it to a Git forge. The jj git push subcommand takes an optional --change/-c flag, which creates a branch based on your current change ID. It works really well when you only have a single change you are going to push and then continually work on, or any time you are content that your current change will remain the tip of the branch. It works a little less well when you are going to add further changes later, because you need to then actually use the branch name with jj branch set push/<change ID> -r <revision>.

\n

Taking a step back, though, working with branches in Jujutsu is great overall. The branch command is a particularly good lens for seeing what a well-designed CLI is like and how it can make your work easier. Notice that the various commands there are all of the form jj branch <do something>. There are a handful of other branch subcommands not mentioned so far: list, rename, track, and untrack. Git has slowly improved its design here over the past few years, but still lacks the straightforward coherence of Jujutsu’s design. For one thing, all of these are subcommands in Jujutsu, not like Git’s mishmash of flags which can be combined in some cases but not others, and have different meanings depending on where they are deployed. For another, as with the rest of Jujutsu’s CLI structure, they use the same options to mean the same things. If you want to list all the branches which point to a given set of revisions, you use the -r/--revisions flag, exactly like you do with any other command involving revisions in Jujutsu. In general, Jujutsu has a very strong and careful distinction between commands (including subcommands) and options. Git does not. The track and untrack subcommands are a perfect example. In Jujutsu, you track a remote branch by running a command like jj branch track <branch>@<remote>. The corresponding Git command is git branch --set-upstream-to <remote>/<branch>. But to list and filter branches in Git, you also pass flags, e.g. git branch --all is the equivalent of jj branch list --all. The Git one is shorter, but also notably less coherent; there is no way to build a mental model for it. With Jujutsu, the mental model is obvious and consistent: jj <command> <options> or jj <context> <command> <options>, where <context> is something like branch or workspace or op (for operation).

\n

Git interop

\n

Jujutsu’s native backend exists, and every feature has to work with it, so it will some day be a real feature of the VCS. Today, though, the Git backend is the only one you should use. So much so that if you try to run jj init without passing --git, Jujutsu won’t let you by default:

\n
> jj init\nError: The native backend is disallowed by default.\nHint: Did you mean to pass `--git`?\nSet `ui.allow-init-native` to allow initializing a repo with the native backend.\n
\n

In practice, you are going to be using the Git backend. In practice, I have been using the Git backend for the last seven months, full time, on every one of my personal repositories and all the open source projects I have contributed to. With the sole exception of someone watching me while we pair, no one has noticed, because the Git integration is that solid and robust. This interop means that adoption can be very low friction. Any individual can simply run jj git init --git-repo . in a given Git repository, and start doing their work with Jujutsu instead of Git, and all that work gets translated directly into operations on the Git repository.

\n

Interoperating with Git also means that there is a two way-street between Jujutsu and Git. You can do a bunch of work with jj commands, and then if you hit something you don’t know how to do with Jujutsu yet, you can flip over and do it the way you already know with a git command. When you next run a jj command, like jj status, it will (very quickly!) import the updates from Git and go back about its normal business. The same thing happens when you run commands like jj git fetch to get the latest updates from a Git remote. All the explicit Git interop commands live under a git subcommand: jj git push, jj git fetch, etc. There are a handful of these, including the ability to explicitly ask to synchronize with the Git repository, but the only ones I use on a day to day basis are jj git push and jj git fetch. Notably, there is no jj git pull, because Jujutsu keeps a distinction between getting the latest changes from the server and changing your local copy’s state. I have not missed git pull at all.

\n

This clean interop does not mean that Git sees everything Jujutsu sees, though. Initializing a Jujutsu repo adds a .jj directory to your project, which is where it stores its extra metadata. This, for example, is where Jujutsu keeps track of its own representation of changes, including how any given change has evolved, in terms of the underlying revisions. In the case of a Git repository, those revisions just are the Git commits, and although you rarely need to work with or name them directly, they have the same SHAs, so any time you would name a specific Git commit, you can reference it directly as a Jujutsu revision as well. (This is particularly handy when bouncing between jj commands and Git-aware tools which know nothing of Jujutsu’s change identifiers.) The .jj directory also includes the operation log, and in the case of a fresh Jujutsu repo (not one created from an existing Git repository), is where the backing Git repo lives.

\n

This Git integration currently runs on libgit2, so there is effectively no risk of breaking your repo because of a Jujutsu – Git interop issue. To be sure, there can be bugs in Jujutsu itself, and you can do things using Jujutsu that will leave you in a bit of a mess, but the same is true of any tool which works on your Git repository. The risk might be very slightly higher here than with your average GUI Git client, since Jujutsu is mapping different semantics onto the repository, but I have extremely high confidence in the project at this point, and I think you can too.

\n

Is it ready?

\n

Unsurprisingly, given the scale of the problem domain, there are still some rough edges and gaps. For example: commit signing with GPG or SSH does not yet work. There is an open PR for the basics of the feature with GPG support, and SSH support will be straightforward to add once the basics, but landed it has not.13 The list of actual gaps or missing features is getting short, though. When I started using Jujutsu back in July 2023, there was not yet any support for sparse checkouts or for workspaces (analogous to Git worktrees). Both of those landed in the interval, and there is consistent forward motion from both Google and non-Google contributors. In fact, the biggest gap I see as a regular user in Jujutsu itself is the lack of the kinds of capabilities that will hopefully come once work starts in earnest on the native backend.

\n

The real gaps and rough edges at this point are down to the lack of an ecosystem of tools around Jujutsu, and the ways that existing Git tools interact with Jujutsu’s design for Git interop. The lack of tooling is obvious: no one has built the equivalent of Fork or Tower, and there is no native integration in IDEs like IntelliJ or Visual Studio or in editors like VS Code or Vim. Since Jujutsu currently works primarily in terms of Git, you will get some useful feedback. All of those tools expect to be working in terms of Git’s index and not in terms of a Jujutsu-style working copy, though. Moreover, most of them (unsurprisingly!) share Git’s own confusion about why you are working on a detached HEAD nearly all the time. On the upside, viewing the history of a repo generally works well, with the exception that some tools will not show anonymous branches/detached HEADs other than one you have actively checked out. Detached heads also tend to confuse tools like GitHub’s gh; you will often need to do a bit of extra manual argument-passing to get them to work. (gh pr create --web --head <name> is has been showing up in my history a lot for exactly this reason.)

\n

Some of Jujutsu’s very nice features also make other parts of working on mainstream Git forges a bit wonky. For example, notice what each of these operations has in common:

\n\n

They are all changes to history. If you have pushed a branch to a remote, doing any of these operations with changes on that branch and pushing to a remote again will be a force push. Most mainstream Git forges handle force pushing pretty badly. In particular, GitHub has some support for showing diffs between force pushes, but it is very basic and loses all conversational context. As a result, any workflow which makes heavy use of force pushes will be bumpy. Jujutsu is not to blame for the gaps in those tools, but it certainly does expose them.14 Nor do I not blame GitHub for the quirks in interop, though. It is not JujutsuLab after all, and Jujutsu is doing things which do not perfectly map onto the Git model. Since most open source software development happens on forges like GitHub and GitLab, though, these things do regularly come up and cause some friction.

\n

The biggest place I feel this today is in the lack of tools designed to work with Jujutsu around splitting, moving, and otherwise interactively editing changes. Other than @arxanas’ excellent scm-diff-editor, the TUI, which Jujutsu bundles for editing diffs on the command line, there are zero good tools for those operations. I mean it when I say scm-diff-editor is excellent, but I also do not love working in a TUI for this kind of thing, so I have cajoled both Kaleidoscope and BBEdit into working to some degree. As I noted when describing how jj split works, though, it is not a particularly good experience. These tools are simply not designed for this workflow. They understand an index, and they do not understand splitting apart changes. Net, we are going to want new tooling which actually understands Jujutsu.

\n

There are opportunities here beyond implementing the same kinds of capabilities that many editors, IDEs, and dedicated VCS viewers provide today for Git. Given a tool which makes rebasing, merging, re-describing changes, etc. are all normal and easy operations, GUI tools could make all of those much easier. Any number of the Git GUIs have tried, but Git’s underlying model simply makes it clunky. That does not have to be the case with Jujutsu. Likewise, surfacing things like Jujutsu’s operation and change evolution logs should be much easier than surfacing the Git reflog, and provide easier ways to recover lost work or simply to change one’s mind.

\n

Conclusion

\n

Jujutsu has become my version control tool of choice since I picked it up over the summer. The rough edges and gaps I described throughout this write-up notwithstanding, I much prefer it to working with Git directly. I do not hesitate to recommend that you try it out on personal or open source projects. Indeed, I actively recommend it! I have used Jujutsu almost exclusively for the past seven months, and I am not sure what would make me go back to using Git other than Jujutsu being abandoned entirely. Given its apparently-bright future at Google, that seems unlikely.15 Moreover, because using it in existing Git repositories is transparent, there is no inherent reason individual developers or teams cannot use it today. (Your corporate security policy might have be a different story.)

\n

Is Jujutsu ready for you to roll out at your Fortune 500 company? Probably not. While it is improving at a steady clip — most of the rough edges I hit in mid-2023 are long since fixed — it is still undergoing breaking changes in design here and there, and there is effectively no material out there about how to use it yet. (This essay exists, in part, as an attempt to change that!) Beyond Jujutsu itself, there is a lot of work to be done to build an ecosystem around it. Most of the remaining rough edges are squarely to do with the lack of understanding from other tools. The project is marching steadily toward a 1.0 release… someday. As for when that might be, there are as far as I know no plans: there is still too much to do. Above all, I am very eager to see what a native Jujutsu backend would look like. Today, it is “just” a much better model for working with Git repos. A world where the same level of smarts being applied to the front end goes into the backend too is a world well worth looking forward to.

\n

Thoughts, comments, or questions? Discuss:

\n\n
\n

Appendix: Kaleidoscope setup and tips

\n

As alluded to above, I have done my best to make it possible to use Kaleidoscope, my beloved diff-and-merge tool, with Jujutsu. I have had only mixed success. The appropriate setup that gives the best results so far:

\n
    \n
  1. \n

    Add the following to your Jujutsu config (jj config edit --user) to configure Kaleidoscope for the various diff and merge operations:

    \n
    [ui]\ndiff-editor = ["ksdiff", "--wait", "$left", "--no-snapshot", "$right", "--no-snapshot"]\nmerge-editor = ["ksdiff", "--merge", "--output", "$output", "--base", "$base", "--", "$left", "--snapshot", "$right", "--snapshot"]\n
    \n

    I will note, however, that I have still not been 100% successful using Kaleidoscope this way. In particular, jj split does not give me the desired results; it often ends up reporting “Nothing changed” when I close Kaleidoscope.

    \n
  2. \n
  3. \n

    When opening a file diff, you must Option⎇-double-click, not do a normal double-click, so that it will preserve the --no-snapshot behavior. That --no-snapshot argument to ksdiff is what makes the resulting diff editable, which is what Jujutsu needs for its just-edit-a-diff workflow. I have been in touch with the Kaleidoscope folks about this, which is how I even know about this workaround; they are evaluating whether it is possible to make the normal double-click flow preserve the --no-snapshot in this case so you do not have to do the workaround.

    \n
  4. \n
\n
\n
\n

Notes

    \n
  1. Yes, it is written in Rust, and it is pretty darn fast. But Git is written in C, and is also pretty darn fast. There are of course some safety upsides to using Rust here, but Rust is not particularly core to Jujutsu’s “branding”. It was just a fairly obvious choice for a project like this at this point — which is exactly what I have long hoped Rust would become! ↩︎

    \n
  2. \n
  3. Pro tip for Mac users: add .DS_Store to your ~/.gitignore_global and live a much less annoyed life — whether using Git or Jujutsu. ↩︎

    \n
  4. \n
  5. I did have one odd hiccup along the way due to a bug (already fixed, though not in a released version) in how Jujutsu handles a failure when initializing in a directory. While confusing, the problem was fixed in the next release… and this is what I expected of still-relatively-early software. ↩︎

    \n
  6. \n
  7. The plain jj init command is reserved for initializing with the native backend… which is currently turned off. This is absolutely the right call for now, until the native backend is ready, but it is a mild bit of extra friction (and makes the title of this essay a bit amusing until the native backend comes online…). ↩︎

    \n
  8. \n
  9. This is not quite the same as Git’s HEAD or as Mercurial’s “tip” — there is only one of either of those, and they are not the same as each other! ↩︎

    \n
  10. \n
  11. If you look at the jj help output today, you will notice that Jujutsu has checkout, merge, and commit commands. Each is just an alias for a behavior using new, describe, or both, though:

    \n
      \n
    • checkout is just an alias for new
    • \n
    • commit is just a shortcut for jj describe -m "<some message>" && jj new
    • \n
    • merge is just jj new with an implicit @ as the first argument.
    • \n
    \n

    All of these are going to go away in the medium term with both documentation and output from the CLI that teach people to use new instead. ↩︎

    \n
  12. \n
  13. Actually it is normally git ci -am "<message>" with -a for “all” (--all) and -m for the message, and smashed together to avoid any needless extra typing. ↩︎

    \n
  14. \n
  15. The name is from Mercurial’s evolution feature, where it refers to changes which have become obsolescent, thus obslog is the “obsolescent changes log”. I recently suggested to the Jujutsu maintainers that renaming this might be helpful, because it took me six months of daily use to discover this incredibly helpful tool. ↩︎

    \n
  16. \n
  17. They also enabled support for a three-pane view in Meld, which allegedly makes it somewhat better. However, Meld is pretty janky on macOS (as GTK apps basically always are), and it has a terrible startup time for reasons that are unclear at this point, which means this was not a great experience in the first place… and Meld crashes on launch on the current version of macOS. ↩︎

    \n
  18. \n
  19. Yes, this is what I do for fun on my time off. At least: partially. ↩︎

    \n
  20. \n
  21. For people coming from Git, there is also an amend alias, so you can use jj amend instead, but it does the same thing as squash and in fact the help text for jj amend makes it clear that it just is squash. ↩︎

    \n
  22. \n
  23. If that sounds like paranoia, well, you only have to lose everything on your machine once due to someone spilling a whole cup of water on it at a coffee shop to learn to be a bit paranoid about having off-machine backups of everything. I git push all the time. ↩︎

    \n
  24. \n
  25. I care about about this feature and have some hopes of helping get it across the line myself here in February 2024, but we will see! ↩︎

    \n
  26. \n
  27. There are plenty of interesting arguments out there about the GitHub collaboration design, alternatives represented by the Phabricator or Gerrit review models, and so on. This piece is long enough without them! ↩︎

    \n
  28. \n
  29. Google is famous for killing products, but less so developer tools. ↩︎

    \n
  30. \n
\n
\n

Thanks: Waleed Khan (@arxanas), Joy Reynolds (@joyously), and Isabella Basso (@isinyaaa) all took time to read and comment on earlier drafts of this mammoth essay, and it is substantially better for their feedback!\n


Thoughts, comments, or questions? Shoot me an email, or leave a comment on Hacker News or lobste.rs.

","summary":"Jujutsu (`jj`) is a new version control system from a software developer at Google. I have been using it full time for 6 months. Here’s why you should switch, too.\n","date_modified":"2024-09-09T08:15:00.000-06:00","image":"https://cdn.chriskrycho.com/images/unlearn.jpg","tags":["software development","tools","version control","Jujutsu","Git"],"banner_image":"https://cdn.chriskrycho.com/images/unlearn.jpg"},{"id":"https://v5.chriskrycho.com/essays/the-leica-q2/","author":{"name":"Chris Krycho","url":"https://v5.chriskrycho.com/"},"title":"[essays] The Leica Q2","url":"https://v5.chriskrycho.com/essays/the-leica-q2/","date_published":"2022-12-28T21:05:00.000-07:00","content_html":"

Mulling on a remarkable camera (that isn’t for me).\n

Assumed audience: People who care about photography specifically or the technologies and tools we use more generally—this essay touches on both.\n

A bit of context: I’ve been having some problems with my Sony α7R IV’s sensor for the last month or so. (it’s intermittently dropping out all but the red or green channels on the sensor, which makes for not great photos), so I rented [disclosure: mutual discount link] a Leica Q2 to have camera coverage for the immediate and extended family Christmases. (I take my role as family photographer seriously!) This essay grew out of that experience.\n


Leica has a reputation. Depending on which way you squint, that reputation might be one of quality that earns an extraordinary price tag, or one of overpriced hype. You will find no argument, though, that Leica is expensive. The Q and Q2 compact, fixed-lens, mirrorless cameras are Leica’s most affordable and approachable entry point to the Leica system of cameras. The Q system is not, in absolute terms, anything but profoundly expensive; and it brings along a slightly higher learning curve than many of the other mirrorless cameras out there — but it is affordable and approachable by comparison with the rest of their line-up, affordable and approachable enough that they have developed a robust following over the past half decade.

\n

I remember distinctly my two introductions to the Leica Q: from Shawn Blanc and from Craig Mod. Both are people whose taste and opinions I have long appreciated. Both gushed with unadulterated enthusiasm about the camera. When I read those essays, I was still in the long winter of photography that was most of my seminary years: no camera bigger or more capable than whatever a smartphone offered. Still, the raves left their mark. They impressed on me that the mirrorless revolution had changed the scene enormously, and they also left a lingering sense that Leica might actually be something special.

\n

When I first came out of that long hiatus, I started out by testing myself: was I really interested in photography again, or just in the cool new gear? I started taking the mechanics seriously enough to read up and actually interalize how photography works, for one thing. For another, I made myself just shoot with my decade-old Canon DSLR for a few months. Frustrating as that camera was, with its APS-C sensor and a profoundly mediocre kit lens, I did enjoy it, and the hobby did indeed stick.

\n

I thought — a lot — about getting a Leica Q or its then-recently-released Q2 successor. I decided, though, that I wanted greater flexibility in lens choice, so I ultimately bought Sony’s α7R IV instead. It was the right choice: a conclusion I had reinforced by renting a Leica Q2 and using it extensively for a week around Christmastime this year. I loved the Q2, yet it’s not for me.

\n
\n

It is difficult not to think that Leica is — must be — overrated: most brands are. Even when Craig Mod ends up gushing about Leica cameras — more than once! — it doesn’t really feel like the brand could possibly live up to the reputation, could possibly justify the exorbitant(-seeming?) prices of its cameras and lenses.

\n

It was only a little under a week, but I think, astonishingly, that Leica actually does live up to its reputation, and even more astonishingly, perhaps even justifies its prices.

\n
\n

I said to my friend Tim Hopper the day the Q2 arrived, after just a few hours of off-and-on use:

\n
\n

I think… I think I have a problem on my hands.… I like this camera a LOT.

\n

The handling is great, and the lens is just… dang.

\n

Also their phone app isn’t incredible or anything but it beats the PANTS off of Sony’s.

\n
\n

That quickly-jotted series of text messages aptly characterized my experience. From a basic mechanics-of-shooting and quality-of-photos perspective, I love the Q2.

\n

First of all, the fixed-mount Leica 28mm 𝑓/1.7 Summicron ASPH lens is phenomenal. I have a handful of Sony’s prime lenses, including its very solid 35mm, 55mm, and 85mm 𝑓/1.8 “base” lenses and the 20mm 𝑓/1.8 G lens. The prime boasted by the Q2 is noticeably better than any of my Sony lenses. It’s incredibly sharp corner to corner, has no vignetting to speak of, and has impeccable clarity.

\n
see it full size here
\n

Operating the lens is a joy, too. The clicks for changes in aperture are crisp and satisfying and reliable. The fact that the focus ring not only has a linear throw, but a fixed linear throw, was a game-changer. The Sony lenses I am familiar with often have nicely linear focus rings, but none of them have a fixed throw. Combined with the distinctive Leica finger rest for the focus ring, you can learn, even in the short span of a week, what focusing in a given range feels like. I expect to miss that little nub and its fixed start and end points when I come back to shooting with my Sony primes.

\n

Last but not least: the shutter is so quiet that I thought at first something was wrong. I first had to convince myself that I had correctly toggled off the electronic shutter. Then I spent a few minutes double-checking after each snap that I had actually taken a picture. The quiet of the shutter is wonderful. It was just disconcerting. I remember being pleasantly surprised at how quiet Sony’s shutters were compared to my old Canon DSLR lenses; this was far more startling a decrease in volume.

\n

I’ve spent the past few years fairly skeptical of Leica’s claims to fame on this front; color me a convert.

\n

The body of the Q2 is genuinely great, too. Its smaller size is really nice, and the simple grip works well in a way that surprised me. On the Sony cameras I have used, I have always wanted a somewhat larger grip — in part because the distribution of weight in it is quite different there from the Q2, and the more so as you add in heftier lenses. The Q2 is solid, even dense, but its grip shape and size felt really good, and its weight is incredibly well-distributed. As a consequence, while the metal body is less textured, and certainly less contoured, it actually feels better to hold than the Sony. The cumulative effect is that of a really solid tool in the hand.

\n

The only issue I had with it — and the only place it really felt less good than the α7R IV — was in the ISO dial. It requires, somewhat oddly, that you click the button on top of it, then use the dial to cycle through the range of options. The Sony body just lets you cycle directly through ISO settings without the additional clicks. In practice this isn’t a huge issue, because I find that letting modern cameras choose ISO settings from a predefined range and adjusting aperture and shutter speed makes for a good default workflow anyway. It was odd, though, given how well-considered the rest of the physical controls on the device are.

\n

Finally, the Fotos app is be nothing to write home about in terms of innovation or design, but it does its job well and gets out of the way. That might sound like a bare minimum, but is actually quite praiseworthy. For one thing, neither “doing the job well” nor “getting out of the way” is not a given — Sony’s horrible phone and desktop software being the number one piece of evidence here. For another, that is exactly what an app like this should do. It is a companion app, not the star of the show. This does not mean its design does not matter — again: Sony’s apps are loud evidence to the contrary. Rather, it means that the app’s design should be focused on reliably supporting and enabling the things you want to do with the camera. Fotos does just that — from its (blissfully notification-free!) GPS tagging to its ability to pull off either small previews or full-sized raw images via WiFi. As with most WiFi-powered connections, the latter process is a little bit slow, but it is reliable and straightforward to use.

\n

This is a good place to say that exactly the same things hold for the software and menu system of the camera itself. They are easy to navigate and easy to make choices in. They are, in fact, easier to work with not only than my α7R IV’s menus (Sony’s infamously difficult previous menu system) but even than the current, much-improved menu system Sony has shipped on more recent models like the α7 IV and α7R V.

\n
see it full size here
\n

The Q2’s body and lens come together to make for a viscerally pleasant experience — caveat about its ISO dial notwithstanding. Every twist of the focus or aperture rings, every click of the shutter, felt good: irrepressibly smiling at it good, exclaiming aloud over it good. Shooting with the it was nice, in a way that very few things are nice.

\n
\n

The Q2 is not perfect, of course; it has a few significant drawbacks. More than that, though, it makes some strong choices — choices which may cash out as strengths or weaknesses depending on your preferences.

\n

First and foremost among these: The lens is a trade-off — dare I say, a compromise. That might come as a surprise given my raving about it above. I stand by those raves, but the choice of 28mm does not suit me. for all its glories, this particular fixed-length choice is a non-starter for me. I can imagine a different fixed length lens suiting me quite well, but 28mm is too wide for me: I very much prefer 35mm – 55mm for a “walkabout” or day-to-day lens.1

\n

Having such a wide angle as the baseline means you will only rarely feel like you wish things were wider. At the same time, you can crop in significantly from 28mm to get a still-usable shot with the effective viewing angle of a 35mm or 55mm or 75mm lens (to name just the three crops which the Q2 will show you on screen). It would not be possible to do the inverse: given a 55mm lens, there is no “cropping out” to 35mm. You can shoot quite wide architecture or landscape views with this lens that you simply cannot get with a 50mm. This one, for example (shared via my photos feed the other day):

\n
original post: cold (2)
\n

I therefore understand and respect the choice. It’s still just too wide for me for a go-to camera, for three reasons.

\n

One: for all that the lens is incredibly sharp edge to edge, it’s still a wide lens. That means that there is real, if subtle distortion out at the edges, and that particularly shows up in portraiture. In some cases, it doesn’t matter. A landscape or a couch cushion will not suffer noticeably. An architecture shot will show it up a little more. Faces, though, can be trouble: the human visual system is hyper-attuned to how people are supposed to look. The distortion is not terrible, and you can manage it by keeping human subjects away from the edges of the frame. Unfortunately, in practice, that meant I noticed it in many of my photos. Groups are hard to keep entirely in the middle of the frame.

\n

Two: cropping in to a 55mm field of view from a 28mm lens is not actually the same as shooting with a 55mm lens. The (lack of) compression in the depth of field is significantly different between the two, and that shows up most of all in portraiture, which is one of the main uses to which I put my camera — the contents of my Glass feed notwithstanding!2 As a result, this lens does not easily produce flattering portraits. That isn’t really a surprise: going wider than 35mm for portraiture requires you to really know what you’re doing. I don’t doubt there are many photographers who can still flatter their subjects with this lens, but it’s beyond my skill to do it consistently.

\n
The best portrait I managed with the Q2… was of myself (see it full size here).
\n

Three: I rely heavily on the ability to crop in even when using a longer lens (a point I noted when doing my mini-review of the α7 IV as well). Especially at family gatherings, I will lean on the high resolution of the α7R IV to get a useful image from across the room with my go-to 55mm. The wider your field of view is, the harder that is, because that wider field of view inherently means a subject across the room occupies fewer of the pixels on the sensor. You can still crop in, but you will have much less to work with.

\n

There’s a (likely totally-unfounded, alas) rumor that the forthcoming Q3 might have a 50mm variant, which would significantly change the calculus for me. If they shipped a 40mm variant of the Q3, I would be hard pressed not to empty my pockets instantly. (“Shut up and take my money,” as the meme goes.) As things stand, though, I cannot imagine using this camera on an everyday basis, despite how often I found myself saying things like “Wow, I really love this camera” and “I don’t want to send it back.” My wife can attest: both of those phrases came out of my mouth repeatedly.

\n

I said above that the Q2 makes choices, and nowhere is this more clear than in committing to this specific focal length. Choosing 28mm gave the camera a very specific personality. If that suits you, you will love it. If it does not suit you, as it does not suit me, you will feel — constantly — that choice, rubbing up against your own photographic instincts.

\n
\n

Another weakness: auto-focus. The Sony α7R IV I’ve been using for the past few years is of the same vintage as the Q2 (the Q2 came out March 2019, the α7R IV in July 2019), but the difference between the two in auto-focus capabilities is night and day.

\n

Of the Leica M10, Craig Mod wrote:

\n
\n

…there’s a fascinating, if somewhat artificial joy of mastering range finder focus. I’ve only fumbled a few shots  —  amazingly  —  because of missed focus at f/1.4.

\n
\n

That “somewhat artificial joy” is a real dynamic with the Q2 as well. I found it consistently more enjoyable to use in manual focus mode than with auto-focus. In fact, I found I ended up using manual focus when taking a lot of our extended family Christmas photos — because it was easier to make sure I actually got the shot I wanted in manual than with auto-focus. Consider: that was true with small children running around.

\n

The Q2’s auto-focus performance reminded me of nothing so much as the Canon DSLR the α7R IV replaced; that camera was an entry-level DSLR in 2009. Not good!

\n

In fairness, it is not a surprise, either. The Q2 is clearly a spiritual descendant of Leica’s M-series of range-finder cameras, which are by definition only manual-focus.3 The fact that the Q and Q2 have auto-focus at all is, like the EVF and live view back panel, a huge shift for Leica in this form factor. Too, it is a much smaller body and a much smaller lens than Sony I am comparing to; Leica’s SL2 is far more directly comparable to Sony’s α7 line.

\n
coffee accoutrements (see it full size here)
\n

When I picked up my α7R IV and snapped a few pictures of the family before heading out to drop it off for repairs, I was immediately grateful for how easy it made it to nail focus. While there is a certain pleasure to nailing focus with the Q2 in manual mode, there is a totally different kind of pleasure to the effortless focusing that is part and parcel of shooting with any recent Sony camera and lens.4 What is more, that effortlessness extends to one of the key factors in nailing focus: eye tracking. It is one thing to find the right focus for a single shot; it is something else to be able to keep it up as a subject moves around. With the Q2, I was always fighting to keep up, whether in auto or manual focus mode. Sure, I could have stopped down, but that comes with its own downsides, especially in dim rooms.

\n

At the same time, that work to focus was enjoyable. Increasing mastery of a fine-tuned machine — a bicycle, a camera, etc. — has its own very real pleasures. There is a place for both effortlessness and effortfulness in our use of technologies. Some of the very best technologies require just enough effort from us to make their bionic rewards truly satisfying: you can feel the boost they give you, because you can feel how your own effort translates into something much greater. By the same token, though: no effort, no sense of boost.

\n

The Q2 delivers that sense of boost in spades, because its focus ring is so very usable. That does not make up for its failings of auto-focus, though, so much as it does give you something else to think about instead.

\n
\n

A dream camera: Sony’s auto-focus tech married to Leica’s sense of style and sheer quality of lens-making.

\n

It would be impossibly expensive, of course. Sony hardware isn’t cheap in the first place, and Leica makes Sony look like a bargain.

\n

It also isn’t clear how such a thing would happen. Rare is the company that has the trifecta of great software chops (menus and operating systems), great firmware chops (auto-focus and friends), and great hardware chops (lens and sensor performance). Not just rare: surpassingly rare. I cannot think of any company with all three. Sony has mediocre software, great firmware, and good hardware. Leica has good software, mediocre firmware, and great hardware. It is not impossible to imagine Leica closing the gap on auto-focus and the like, but those capabilities simply are not in the company’s very traditional wheelhouse. Likewise, one can imagine Sony getting its act together on its menus and doubling down on its lens-craft, but it would be a significant change, and the return on investment doesn’t seem like it would be that high, so the question would simply be, “Why?”

\n

So: a dream camera that is fated, I think, to remain a dream. It’s too bad, because a camera with Q2 (or M10/M11) sensibilities and style married to Sony’s auto-focus system would be an unbelievably compelling piece of hardware.

\n
\n

In closing, now, a hard right turn — riffing on two notes from Mod and some thoughts I have had bouncing around my head for a while.

\n

Here’s Mod writing on the M10 (in the same piece quoted above):

\n
\n

In the end, though, it’s an affront to all that is good in the world that Leica makes you wait in line for months to plop down such a large sum of cash for a camera body. But you have to think of the company as it is: A fairly tiny artisanal maker of highly precise yet capricious image tools; William Gibsonian to the max. Framing it as such makes the tax they exact a little less difficult to swallow. Thankfully, the machine delivers the goods, feels great in hand, and is more fun to shoot with than an iPhone.

\n
\n

Here he is again, this time on the M11:

\n
\n

Leica Ms are still the simplest, smallest, most optically performant full-frame cameras on the market. That’s a crazy trio: Simplest, smallest, most optically performant. Of course that’s not going to be cheap. And to boot, they’re assembled by well-paid humans in Germany. Also rare. I am happy to pay to support that.

\n
\n

Even with the Q2, these same points hold. A new Q2 is $5,795. Granted that that comes with the best lens I’ve ever used, full stop. Granted that it feels amazing to shoot with. Granted that as a piece of hardware is it not only functional but beautiful. Granted that the trio of size and weight and performance is best-in-class and it isn’t even close. Granted, as Mod notes, that the people who make it are well compensated. Granted: all of it. It’s still almost $6,000 for a camera with a single, fixed-focal-length lens.

\n

Is that reasonable for them to make? Is it reasonable for me to buy? Is it ever reasonable for anyone to buy? In particular: for a Christian who is serious about the very real dangers of materialism to his soul, who would take heed of the warning that the desire to be rich is the root of all kinds of evil, who would earnestly aim to store up treasures in heaven rather than here on earth: Does it, can it, ever make sense to buy something like this, for the sheer goodness of the thing?

\n

I have been mulling on this dynamic a lot lately.5 I have reservations (significant and serious) about the allure of expensive things, including some of those I already own, including the camera I already own. Love of this world is antithetical to love of God. Whoever would keep his life must lose it. The rich are in danger of hell if they do not give generously of what they have to the poor.

\n

Being an incredibly well-compensated senior engineer at LinkedIn has given me occasion to mull at greater length and in greater detail on these concerns than I could have imagined half a decade ago. Money can be a gift, yet it also comes with burdens: of conscience, of time spent managing it, of temptation to use it on ourselves, of responsibility to use it wisely and well and for the good of others. In this case, it comes with the burden of considering whether, and if so when and how, it can be appropriate to spend money on something like a camera. The same concern goes, mutatis mutandis, for a watch or a car or many other such items: When is it good or right to spend money on a particularly nice thing for your own pleasure, rather than to give that money away?

\n

An easy answer to the question is simply “It is never good or right, never buy good or nice things.” Put plainly, I think that cannot be right. If it were, there would be no room in this world for the kinds of beauty and craftwork which can only come into existence through long labors, which in turn can be afforded only through generous recompense. That simply flies in the face of what I take to be one of the chief human vocations God has given us: to sub-create.

\n

There is no easy answer here.

\n

Is Leica “worth it”? If you are a professional, it might well be. Those are different considerations. If you are a hobbyist, a person picking it up for the sheer love of the art, I think the question is more complicated. The question is more complicated all the way down, though, not specific to Leica or the Q2. This particular example just shows it up more clearly because it sits so strongly on the high end of both price and quality: The Leica Q2 is a phenomenal camera — and yes, Leica earns its price point.

\n

It is not for me, though. Maybe for multiple reasons.

\n
\n
\n

Notes

    \n
  1. I’ve had similar annoyances with my iPhone cameras for years. ↩︎

    \n
  2. \n
  3. There’s a reason for this: my portraiture is almost entirely of my family, immediate and extended, and therefore includes many photos of my and my siblings’ small children. Those photos are wonderful for us to have; but they aren’t especially appropriate to share to the world at large. My daughters don’t need their faces plastered all over the internet; nor do my nieces or nephews. As they get old enough to have well-formed opinions, I will ask them — as I do my wife when I post the occasional portrait of her — whether I can have their permission to share a photo from time to time. For now, though, those all stay private. ↩︎

    \n
  4. \n
  5. I played with an M11 at the Leica shop in NYC back in October and it is an incredibly appealing device. I can see why people love it! ↩︎

    \n
  6. \n
  7. The same is reportedly true of recent Canon and Nikon mirrorless cameras, but I have no experience with them and so cannot comment! ↩︎

    \n
  8. \n
  9. I hope to come back to it in some expanded form — another essay here or in another venue. But I also might not, given my track record of essay-publishing, and this seemed a good place to air out some of these thoughts. ↩︎

    \n
  10. \n
\n
\n

Thanks: Craig Mod’s essay on the Leica Q is—obviously—a massive influence on this essay.\n


Thoughts, comments, or questions? Shoot me an email!

","summary":"The Leica Q2 is a phenomenal camera—and yes, Leica earns its price point. It is not for me, though.\n","image":"https://cdn.chriskrycho.com/images/2022/12-21%20well%2C%20dang%20thumb.jpg","tags":["photography","Leica Q2","Leica"],"banner_image":"https://cdn.chriskrycho.com/images/2022/12-21%20well%2C%20dang%20thumb.jpg"},{"id":"https://v5.chriskrycho.com/journal/private-chat-and-dms-are-good-actually/","author":{"name":"Chris Krycho","url":"https://v5.chriskrycho.com/"},"title":"[essays] Private Chat and DMs Are Good, Actually\n","url":"https://v5.chriskrycho.com/essays/private-chat-and-dms-are-good-actually/","date_published":"2020-11-30T08:00:00.000-07:00","content_html":"

Don’t forget the lessons of physical offices when thinking about chat.\n

Assumed audience: Anyone considering social dynamics in team chat, but especially leaders with authority to shape team norms.\n


It’s common in some circles to treat private rooms or DM-heavy Slack teams as a sign and more importantly a cause of dysfunction, particularly in terms of team trust. In particular, folks are sometimes tempted to use the metrics Slack provides to measure the health of their organizations: a high value of the private-to-public messages ratio is seen as a fairly direct measure of mistrust. The corresponding assumption is that it also causes teams to silo from each other and mistrust each other in the first place. I disagree firmly with both parts of this, and always have.

\n

On Privacy

\n

First, a heavy use of private rooms and DMs might be a reflection of existing mistrust, but only a reflection, not a cause. If you ask around, you’ll find that many organizations with widely varying degrees of siloing, trust, collaboration, etc. have very similar ratios of private and public conversation.1 If two organizations with polar opposite degrees of trust and collaboration have the same ratio of private-to-public conversation, then siloing and mistrust can’t be caused by private chat. Second, this also means that the ratio of private to public chat isn’t even a reliable signal of whether an organization is healthy or not: it lacks any predictive power.

\n

This is exactly what we should expect based on how people interact with each other in the analog world. Our offices have meeting rooms because having spaces for private conversation helps everyone do better in the public spaces. For one thing, even in the healthiest organization, there are going to be times when you’re frustrated with another person or team. When that happens, the most helpful thing is often to go talk to someone else you trust. That gives you space to blow off steam, to work it through mentally or emotionally so you can handle it well publicly.

\n

For another, there are plenty of totally “positive” conversations which benefit from privacy.2 Sometimes you need to help someone avoid tripping over a political or personal landmine in a conversation. Sometimes you’re coaching someone through a difficult personal situation, or through their career considerations. Sometimes you’re working on a thorny problem, where having more input from people with less context would just be a distraction (even if just by way of having to give people enough context to contribute meaningfully). Sometimes you’re processing with your closest colleagues about changes to a company policy which affect you. Sometimes you’re responding privately to a public question about an HR policy based on your own experience. Sometimes you’re just hanging out with your team, talking about your weekend.

\n

The list of things which should not be in public rooms in a healthy organization is long. Public rooms are good for public discussions, but we must be able to distinguish which discussions should be public. Most of us develop a reasonably good sense for what that looks like in an office, because it maps fairly directly to our existing social norms. Many fewer of us have had time to develop the same sense around chat, because we have used it far less. But in the same way as meeting rooms in an office, private chats serve a vital function in keeping public chat healthy, independent of the healthy trust level of an organization.

\n

“Public” on chat is also very different from “public” in an office. If a group of four or five people are sitting around a table in a public area in an office, it’s true that anybody could happen along and join the conversation. There is no log of the conversation for someone to browse through, though, and the people at the table can modulate the conversation however is appropriate for the newcomer. A public conversation around a table is therefore much more like a chat room with no persistent logging — just the opposite of Slack.

\n

Now, this doesn’t mean that there aren’t dangers around private chat. It simply means that the issues aren’t fundamentally matters of organizational trust, or the goodness or badness of private chat in a general sense. Rather, they come down to decision-making and inertia.

\n

On Decision-Making

\n

While there are real challenges for teams around decision-making, these challenges are much the same with chat as with in-office interactions. For example, people particularly worry about whether the chat history is available to others later for context around decisions: in a private chat, the answer is no, whereas in a public chat, the answer is at least in principle yes. If you’re reliant on a chat archive to let you know whether the decision was made well, though, you’ve discovered a problem with your decision-making process in general.

\n

What matters is not that you have a record of every word spoken in a meeting but that the decision-making process is transparent enough that people can trust it, and open enough that if something got missed it can be flagged and reevaluated. Public chat doesn’t solve this very well, though. For one thing, public chat in a room which key people aren’t in can be nearly as much a problem as private chat. For another, a chat log is a terrible way to understand a decision, just as a video recording of a meeting around a table would be.

\n

Accordingly, I recommend away from making significant decisions over chat at all. In place of ad hoc conversations for important decisions — chat or otherwise — use a clear decision-making framework (including clarifying the parties involved via RAPID, RACI, etc.) and use good meeting minutes for synchronous conversations (whether via video or chat or in person) and comment history for asynchronous decision-making (e.g. via RFCs). The resulting decisions will clearer and the document trail behind them will be far more accessible and useful over time.3

\n

Another legitimate problem is people abusing private conversation as a way to do an end-around on the official channels. This is not specific to chat, either, though: people use private meetings as a way of getting around formal decision-making approaches in the office, too. Again, the key is that the reasons for decisions are clearly articulated and open to feedback from all relevant parties. If a decision suddenly changes and there isn’t a clearly-visible reason why, there is an organizational problem… but one that would have existed regardless of whether chat was involved at all.

\n

On Inertia

\n

The other real reason to encourage people to “default to public” is that private chat has inertia on its side. If a team defaults to private, even conversations which would benefit from being public are likely to end up remaining private. There isn’t a one-size-fits-all solution here, though, for all the reasons that private chat can be good. The best we can do here is have a habit — especially as leaders — of moving conversations to public whenever there is no reason for it to be private.

\n

For example: I’m the lead for a large migration on the app I support, so people regularly DM me with questions about the migration. I have built a habit of moving those conversations to public forums with a gentle nudge: “Hey, do you mind reposting that question in <the dedicated Slack room for the migration>? That way everyone can benefit from our discussion!” What’s more, I try to direct it to the most public forum possible. That might be the Slack room for the migration, which I intentionally set up to support all teams working on the migration. It might be our internal Q&A board. It might be public Stack Overflow. The key is that to default to the most public channel possible. Leaders’ actions implicitly set norms just as much or more than explicit statements about what people should do.

\n

Beyond that, I think we simply have to acknowledge that the challenge of getting the right degree of public-vs.-private is an “unsolved problem” — and that it’s a problem which is not amenable to technical solutions. The fundamental challenges are in human nature: both our tendency to manipulate systems to our own good, and our inability to judge these questions perfectly.

\n

Finally, watch out for people weaponizing these dynamics: “Why wasn’t this handled privately?” and “Shouldn’t we be having this conversation in public?” are legitimate questions at times, but I have also seen people use them to manipulate instead. In particular, be wary of individuals who push loudly for decisions to be made in public, but themselves regularly use private channels for decision-making. That move is just abusing the stated norm for their own power and advantage. If you have the institutional clout, confront it directly when you see it happen. If you don’t have the clout to pull that off, try to raise it with someone who does. If it’s a pattern with multiple leaders around you, such that you don’t have anyone to raise it with safely, find another team as quickly as you can.

\n

Conclusion

\n

The legitimate problems that people worry about with private chat are not specific to private chat or indeed to chat at all. The other concerns people have about private chat — especially worries that private chat causes or even is a meaningful signal of organizational mistrust or dysfunction — are not only unfounded, but in fact misunderstand the deep human need for a whole range of privacy levels, from totally-private one-on-one conversation through semi-public group conversations to totally-public forums.

\n

If you’re worried about mistrust between teams, dig into why you have that distrust in the first place. If you’re worried about decision-making, fix your organization’s decision-making process. Build good norms which allow room for the the whole range of private to public conversations. But don’t let people weaponize those norms, and don’t use chat statistics as a measure of organizational health.

\n
\n

Thoughts? You can email me or discuss this on Hacker News.

\n
\n
\n
\n

Notes

    \n
  1. That’s exactly what came out in the conversation that prompted this post, in fact. ↩︎

    \n
  2. \n
  3. I scare-quote “positive” here because I think dealing with difficult situations in a healthy way is positive — but since it’s often in response to a negative or difficult circumstance, I think the distinction is still useful. ↩︎

    \n
  4. \n
  5. That does not mean you need a heavy process here. Sometimes all you need is a 15-minute sync or a 1-day RFC cycle. Again: the key is the trustworthiness and openness of the process. Make it as lightweight as you possibly can! ↩︎

    \n
  6. \n
\n
\n

Thanks: Thanks to Keita Kobayashi, Mike Hostetler, Will Larson, Stephen Carradini, and Jeremy Sherman for taking the time to read, comment on, and discuss earlier drafts of this material.\nCredit to Austin Distel for the illustrative photo.\n


Thoughts, comments, or questions? Shoot me an email!

","summary":"Some leaders are tempted to treat private chat as a signal or even a cause of team dysfunction—but there is no such correlation, and indeed people need private chats for healthy social dynamics.\n","date_modified":"2020-12-27T22:21:00.000-07:00","image":"https://images.unsplash.com/photo-1556155092-8707de31f9c4?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=600&q=80","tags":["leadership","decision-making","chat","Slack"],"banner_image":"https://images.unsplash.com/photo-1556155092-8707de31f9c4?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=600&q=80"}]}