jj Documentation

A Git-compatible VCS that is both simple and powerful

32,059 stars Rust Rust 1 file · ~127,368 tokens
RAW Doc

Technical Documentation: jj-vcs/jj

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (`README.md`)

Introduction

Jujutsu is a powerful version control system
for software projects. You use it to get a copy of your code, track changes
to the code, and finally publish those changes for others to see and use.
It is designed from the ground up to be easy to use—whether you're new or
experienced, working on brand new projects alone, or large scale software
projects with large histories and teams.

Jujutsu is unlike most other systems, because internally it abstracts the user
interface and version control algorithms from the storage systems used to
serve your content. This allows it to serve as a VCS with many possible physical
backends, that may have their own data or networking models—like Mercurial or
Breezy, or hybrid systems like Google's cloud-based design, Piper/CitC.

Today, we use Git repositories as a storage layer to serve and track content,
making it compatible with many of your favorite Git-based tools, right now!
However, note that only commits and files are stored in Git; bookmarks
(branches) and other higher-level metadata are stored in custom storage outside
of Git. All core developers use Jujutsu to develop Jujutsu, right here on
GitHub. But it should hopefully work with your favorite Git forges, too.

We combine many distinct design choices and concepts from other version control
systems into a single tool. Some of those sources of inspiration include:

  • Git: We make an effort to be fast—with a snappy UX, efficient
    algorithms, correct data structures, and good-old-fashioned attention to
    detail. The default storage backend uses Git repositories for "physical
    storage", for wide interoperability and ease of onboarding.

  • Mercurial & Sapling: There are many Mercurial-inspired features, such as
    the revset language to select commits. There is no explicit index
    or staging area. Branches are "anonymous" like Mercurial, so you don't need
    to make up a name for each small change. Primitives for rewriting history are
    powerful and simple. Formatting output is done with a robust template language
    that can be configured by the user.

  • Darcs: Jujutsu keeps track of conflicts as first-class
    objects
    in its model; they are first-class in the same way commits
    are, while alternatives like Git simply think of conflicts as textual diffs.
    While not as rigorous as systems like Darcs (which is based on a formalized
    theory of patches, as opposed to snapshots), the effect is that many forms of
    conflict resolution can be performed and propagated automatically.

And it adds several innovative, useful features of its own:

  • Working-copy-as-a-commit: Changes to files are recorded automatically
    as normal commits, and amended on every subsequent change. This "snapshot"
    design simplifies the user-facing data model (commits are the only visible
    object), simplifies internal algorithms, and completely subsumes features like
    Git's stashes or the index/staging-area.

  • Operation log & undo: Jujutsu records every operation that is performed on the
    repository, from commits, to pulls, to pushes. This makes debugging problems like
    "what just happened?" or "how did I end up here?" easier, especially when
    you're helping your coworker answer those questions about their repository!
    And because everything is recorded, you can undo that mistake you just made
    with ease. Version control has finally entered the 1960s!

  • Automatic rebase and conflict resolution: When you modify a commit, every
    descendent is automatically rebased on top of the freshly-modified one. This
    makes "patch-based" workflows a breeze. If you resolve a conflict in a commit,
    the resolution of that conflict is also propagated through descendants as
    well. In effect, this is a completely transparent version of git rebase --update-refs combined with git rerere, supported by design.

[!WARNING]
The following features are available for use, but experimental; they may have
bugs, backwards incompatible storage changes, and user-interface changes!

  • Safe, concurrent replication: Have you ever wanted to store your version
    controlled repositories inside a Dropbox folder? Or continuously backup
    repositories to S3? No? Well, now you can!

    The fundamental problem with using filesystems like Dropbox and backup tools
    like rsync on your typical Git/Mercurial repositories is that they rely
    on local filesystem operations being atomic, serialized, and non-concurrent
    with respect to other reads and writes—which is not true when operating on
    distributed file systems, or when operations like concurrent file copies (for
    backup) happen while lock files are being held.

    Jujutsu is instead designed to be safe under concurrent scenarios;
    simply using rsync or Dropbox and then using that resulting repository
    should never result in a repository in a corrupt state. The worst that
    should happen is that it will expose conflicts between the local and remote
    state, leaving you to resolve them.

The command-line tool is called jj for now because it's easy to type and easy
to replace (rare in English). The project is called "Jujutsu" because it matches
"jj".

Jujutsu is relatively young, with lots of work to still be done. If you have any
questions, or want to talk about future plans, please join us on Discord
,
start a GitHub Discussion, or
send an IRC message to #jujutsu on Libera
Chat
. The developers monitor all of
these channels[^bridge].

[^bridge]: To be more precise, the #jujutsu Libera IRC channel is bridged to
one of the channels on jj's Discord. Some of the developers stay on Discord and
use the bridge to follow IRC.

News and Updates 📣

  • December 2024: The jj Repository has moved to the jj-vcs GitHub
    organization.
  • November 2024: Version 0.24 is released which adds jj file annotate,
    which is equivalent to git blame or hg annotate.
  • September 2024: Martin gave a presentation about Jujutsu at
    Git Merge 2024.
  • Feb 2024: Version 0.14 is released, which deprecates "jj checkout" and "jj merge",
    as well as jj init --git, which is now just called jj git init.
  • Oct 2023: Version 0.10.0 is released! Now includes a bundled merge and
    diff editor for all platforms, "immutable revsets" to avoid accidentally
    edit-ing the wrong revisions, and lots of polish.
  • Jan 2023: Martin gave a presentation about Google's plans for Jujutsu at
    Git Merge 2022!
    See the slides or the recording.

The wiki also contains a more extensive list of media references.

Getting started

[!IMPORTANT]
Jujutsu is an experimental version control system. While Git compatibility
is stable, and most developers use it daily for all their needs, there may
still be work-in-progress features, suboptimal UX, and workflow gaps that make
it unusable for your particular use.

Follow the installation
instructions
to
obtain and configure jj.

The best way to get started is probably to go through the
tutorial
. Also see the Git
comparison
, which
includes a table of jj vs. git commands.

As you become more familiar with Jujutsu, the following resources may be helpful:

  • The FAQ.
  • The Glossary.
  • The jj help command (e.g. jj help rebase).
  • The jj help -k <keyword> command (e.g. jj help -k config). Use jj help --help
    to see what keywords are available.

If you are using a prerelease version of jj, you would want to consult
the docs for the prerelease (main branch)
version
. You can also get there
from the docs for the latest release by using the website's version switcher. The version switcher is visible in
the header of the website when you scroll to the top of any page.

Features

Compatible with Git

Jujutsu is designed so that the underlying data and storage model is abstract.
Today, only the Git backend is production-ready. The Git backend uses the
gitoxide Rust library.

The Git backend is fully featured and maintained, and allows you to use Jujutsu
with any Git remote. The commits you create will look like regular Git commits.
You can fetch branches from a regular Git remote and push branches to the
remote. You can always switch back to Git.

Here is how you can explore a GitHub repository with jj.

You can even have a colocated local
workspace

where you can use both jj and git commands interchangeably.

The working copy is automatically committed

Jujutsu uses a real commit to represent the working copy. Checking out a commit
results in a new working-copy commit on top of the target commit. Almost all
commands automatically amend the working-copy commit.

The working-copy being a commit means that commands never fail because the
working copy is dirty (no "error: Your local changes to the following
files..."), and there is no need for git stash. Also, because the working copy
is a commit, commands work the same way on the working-copy commit as on any
other commit, so you can set the commit message before you're done with the
changes.

The repo is the source of truth

With Jujutsu, the working copy plays a smaller role than with Git. Commands
snapshot the working copy before they start, then they update the repo, and then
the working copy is updated (if the working-copy commit was modified). Almost
all commands (even checkout!) operate on the commits in the repo, leaving the
common functionality of snapshotting and updating of the working copy to
centralized code. For example, jj restore (similar to git restore) can
restore from any commit and into any commit, and jj describe can set the
commit message of any commit (defaults to the working-copy commit).

Entire repo is under version control

All operations you perform in the repo are recorded, along with a snapshot of
the repo state after the operation. This means that you can easily restore to
an earlier repo state, simply undo your operations one-by-one or even revert a
particular operation which does not have to be the most recent one.

Conflicts can be recorded in commits

If an operation results in
conflicts,
information about those conflicts will be recorded in the commit(s). The
operation will succeed. You can then resolve the conflicts later. One
consequence of this design is that there's no need to continue interrupted
operations. Instead, you get a single workflow for resolving conflicts,
regardless of which command caused them. This design also lets Jujutsu rebase
merge commits correctly (unlike both Git and Mercurial).

Basic conflict resolution:

Juggling conflicts:

Automatic rebase

Whenever you modify a commit, any descendants of the old commit will be rebased
onto the new commit. Thanks to the conflict design described above, that can be
done even if there are conflicts. Bookmarks pointing to rebased commits will be
updated. So will the working copy if it points to a rebased commit.

Comprehensive support for rewriting history

Besides the usual rebase command, there's jj describe for editing the
description (commit message) of an arbitrary commit. There's also jj diffedit,
which lets you edit the changes in a commit without checking it out. To split
a commit into two, use jj split. You can even move part of the changes in a
commit to any other commit using jj squash -i --from X --into Y.

Status

The tool is fairly feature-complete, but some important features like support
for Git submodules are not yet completed. There
are also several performance bugs. It's likely that workflows and setups
different from what the core developers use are not well supported, e.g. there
is no native support for email-based workflows.

Today, all core developers use jj to work on jj. I (Martin von Zweigbergk)
have almost exclusively used jj to develop the project itself since early
January 2021. I haven't had to re-clone from source (I don't think I've even had
to restore from backup).

There will be changes to workflows and backward-incompatible changes to the
on-disk formats before version 1.0.0. For any format changes, we'll try to
implement transparent upgrades (as we've done with recent changes), or provide
upgrade commands or scripts if requested.

There are several tools trying to solve similar problems as Jujutsu. See
related work for details.

Contributing

We welcome outside contributions, and there's plenty of things to do, so
don't be shy. Please ask if you want a pointer on something you can help with,
and hopefully we can all figure something out.

We do have a few policies and
suggestions

for contributors. The broad TL;DR:

  • Bug reports are very welcome!
  • Every commit that lands in the main branch is code reviewed.
  • Please behave yourself, and obey the Community Guidelines.
  • There is a mandatory CLA you must agree to. Importantly, it does not
    transfer copyright ownership to Google or anyone else; it simply gives us the
    right to safely redistribute and use your changes.

Mandatory Google Disclaimer

I (Martin von Zweigbergk, [email protected]) started Jujutsu as a hobby
project in late 2019, and it has evolved into my full-time project at Google,
with several other Googlers (now) assisting development in various capacities.
That said, while Google is one of the main contributors, this is not a
supported Google product
, i.e. support is provided by the community.

License

Jujutsu is available as Open Source Software, under the Apache 2.0 license. See
LICENSE for details about copyright and redistribution.

The jj logo was contributed by J. Jennings and is licensed under a Creative
Commons License, see docs/images/LICENSE.


Overview (`demos/README.md`)

Screenshots with demos and scripts to generate them

The demo_*.sh scripts in this directory demo
various features of jj.

The run_scripts.sh script can be used to:

  • Run them inside a standardized environment instead of the user's local
    environment.
  • Generate SVG and PNG images for the scripts.

The PNG images in the repo may be slightly older, as they take up more space in
the repo.

The SVG images have human-readable diffs, but may look different on different
computers with different fonts installed.

Running `run_scripts.sh`

This requires ImageMagick and term-transcript-cli to be installed. See
run_scripts.sh's error messages for some more details. On Debian Linux, it
also seems helpful to sudo apt install inkscape; ImageMagick seems to use
either Inkscape itself or some dependency of it.

One way to make all the images and check the output is:

shell
cd demos
./run_scripts.sh demo_*.sh |less

A note on fonts

The exact PNG output depends on the fonts you have installed on your system.

The screenshots are usually generated on a Debian Linux system and use the "Fira
Code" font. It can be installed with sudo apt install fonts-firacode. It seems
to include all relevant Unicode symbols and be a little bolder and thus more
readable than the "Liberation Mono" font, which is used if Fira Code is not
installed. That font also works OK. See the CSS font specification in
run_scripts.sh for other fonts tried (especially when viewing SVGs on the
web). If none apply, the default monospace font will be used.

convert -list Fonts will list the fonts ImageMagick is aware of.


Overview (`web/docs/README.md`)

docs.jj-vcs.dev

Built withStarlight

This application powers https://docs.jj-vcs.dev.

🧞 Commands

All commands are run from this directory:

Command Action
npm install Installs dependencies
npm run dev Starts local dev server at localhost:4321
npm run build Build your production site to ./dist/
npm run preview Preview your build locally, before deploying
npm run astro ... Run CLI commands like astro add, astro check
npm run astro -- --help Get help using the Astro CLI

👀 Want to learn more?

Check out Starlight’s docs, read the Astro documentation, or jump into the Astro Discord server.


Overview (`cli/src/config/README.md`)

These files are compiled into the binary; they are not installed to the user's
system.


Overview (`lib/src/config/README.md`)

These files are compiled into the binary; they are not installed to the user's
system.


Index (`docs/index.md`)

Jujutsu—a version control system

Welcome to `jj`'s documentation website!

The complete list of the available documentation pages is located in
the sidebar on the left of the page. The sidebar may be hidden; if so,
you can open it either by widening your browser window or by clicking
on the hamburger menu that appears in this situation.

Additional help is available using the jj help command if you have
jj installed.

You may want to jump to:


Web Docs Src Content Docs Index (`web/docs/src/content/docs/index.md`)


title: "Jujutsu—a version control system"

Welcome to `jj`'s documentation website!

The complete list of the available documentation pages is located in
the sidebar on the left of the page. The sidebar may be hidden; if so,
you can open it either by widening your browser window or by clicking
on the hamburger menu that appears in this situation.

Additional help is available using the jj help command if you have
jj installed.

You may want to jump to:


FAQ (`docs/FAQ.md`)

Frequently asked questions

Why does my bookmark not move to the new commit after `jj new/commit`?

If you're familiar with Git, you might expect the current bookmark to move forward
when you commit. However, Jujutsu does not have a concept of a "current bookmark".

To move bookmarks, use jj bookmark move.

I made a commit and `jj git push --all` says "Nothing changed" instead of pushing it. What do I do?

jj git push --all pushes all bookmarks, not all revisions. You have two
options:

  • Using jj git push --change will automatically create a bookmark and push it.
  • Using jj bookmark commands to create or move a bookmark to either the commit
    you want to push or a descendant of it. Unlike Git, Jujutsu doesn't do this
    automatically (see previous question).

Where is my commit, why is it not visible in `jj log`?

Is your commit visible with jj log -r 'all()'?

If yes, you should be aware that jj log only shows a subset of the commits in
the repo by default. Most commits that exist on a remote are not shown. Local
commits and their immediate parents (for context) are shown. The thinking is
that you are more likely to interact with this set of commits. You can configure
the set of revisions to show by default by overriding revsets.log as described
in config.

If not, the revision may have been abandoned (e.g. because you
used jj abandon, or because it's an obsolete version that's been rewritten
with jj rebase, jj describe, etc). In that case, jj log -r commit_id
should show the revision as "hidden". jj new commit_id should make the
revision visible again.

See revsets and templates for further guidance.

What are elided revisions in the output of `jj log`? How can I display them?

"Elided revisions" appears in the log when one revision descends from another,
both are in the revset, but the revisions connecting them are not in the
revset.

For example, suppose you log the revset tyl|mus which contains exactly two
revisions:

sh
$  jj log -r 'tyl|mus'
○  musnqzvt [email protected] 1 minute ago 9a09f8a5
│  Revision C
~  (elided revisions)
○  tylynnzk [email protected] 1 minute ago f26967c8
│  Revision A

Only the two revisions in the revset are displayed. The text "(elided
revisions)" is shown to indicate that musnqzvt descends from tylynnzk, but
the nodes connecting them are not in the revset.

To view the elided revisions, change the revset expression so it
includes the connecting revisions. The connected() revset function does
exactly this:

sh
$ jj log -r 'connected(tyl|mus)'
○  musnqzvt [email protected] 43 seconds ago 9a09f8a5
│  Revision C
○  rsvnrznr [email protected] 43 seconds ago 5b490f30
│  Revision B
○  tylynnzk [email protected] 43 seconds ago f26967c8
│  Revision A

How can I get `jj log` to show me what `git log` would show me?

Use jj log -r ... The .. operator lists all visible commits in the repo, excluding the root (which is never interesting and is shared by all repos).

Can I monitor how `jj log` evolves?

The simplest way to monitor how the history as shown by jj log evolves is by using the watch(1) command (or hwatch or viddy).
For example:

sh
watch --color jj --ignore-working-copy log --color=always

This will continuously update the (colored) log output in the terminal.
The --ignore-working-copy option avoids conflicts with manual operations during the creation of snapshots.
Martin used watch in a tmux pane during his presentation Jujutsu - A Git-compatible VCS.

A similar result can be achieved with watchexec,
which triggers on op log changes instead of using a two-second interval:

sh
watchexec --quiet --clear --restart --watch=.jj/repo/op_heads/heads --ignore-nothing --wrap-process=none -- jj --ignore-working-copy log

Alternatively, you can use jj-fzf, where the central piece is the jj log view and common operations can be carried out via key bindings while the log view updates.

The wiki lists additional TUIs and GUIs beyond the terminal: GUI-and-TUI

How do I convert an existing Git repo into a Jujutsu repo?

From inside the Git repository, run jj git init. This creates a .jj/
directory next to .git/ and imports the existing history (a colocated
workspace).

If you instead want a separate Jujutsu working copy that shares commits with the
Git repo (similar to a Git worktree),
then run jj git init --git-repo=path/to/git-repo path/to/new/jj-repo.

See Creating a repo backed by an existing Git
repo
.

Should I colocate my repository?

Colocating a Jujutsu repository allows you to use both Jujutsu and Git in the
same working copy. The benefits of doing so are:

  • You can use Git commands when you're not sure how to do something with
    Jujutsu, Jujutsu hasn't yet implemented a feature (e.g., tagging), or you
    simply prefer Git in some situations.

  • Tooling that expects a Git repository still works (IDEs, build tooling, etc.)

The colocation documentation describes the
drawbacks
but the most
important ones are:

  • Interleaving git and jj commands may create confusing bookmark conflicts
    or divergent changes.

  • If the working copy commit or its parent contain any conflicted files, tools
    expecting a Git repo may interpret the commit contents or its diff in a wrong
    and confusing way. You should avoid doing mutating operations with Git tools
    and ignore the confusing information such tools present for conflicted commits
    (unless you are curious about the details of how jj stores
    conflicts
    ). See
    #3979 for plans to improve
    this situation.

  • Jujutsu commands may be a little slower in very large repositories due to
    importing and exporting changes to Git. Most repositories are not noticeably
    affected by this.

If you primarily use Jujutsu to modify the repository, the drawbacks are
unlikely to affect you. Try colocating while you learn Jujutsu, then switch if
you find a specific reason not to colocate.

`jj` is said to record the working copy after `jj log` and every other command. Where can I see these automatic "saves"?

Indeed, every jj command updates the current "working-copy" revision, marked
with @ in jj log. You can notice this by how the commit ID of the
working copy revision changes when it's updated. Note that, unless you move to
another revision (with jj new or jj edit, for example), the change ID will
not change.

If you expected to see a historical view of your working copy changes in the
parent-child relationships between commits you can see in jj log, this is
simply not what they mean. What you can see in jj log is that after the
working copy commit gets amended (after any edit), the commit ID changes.

You can see the actual history of working copy changes using jj evolog. This
will show the history of the commits that were previously the "working-copy
commit", since the last time the change id of the working copy commit changed.
The obsolete changes will be marked as "hidden". They are still accessible with
any jj command (jj diff, for example), but you will need to use the commit
id to refer to hidden commits.

You can also use jj evolog -r on revisions that were previously the
working-copy revisions (or on any other revisions). Use jj evolog -p as an
easy way to see the evolution of the commit's contents.

Can I prevent Jujutsu from recording my unfinished work? I'm not ready to commit it.

Jujutsu automatically records new files in the current working-copy commit and
doesn't provide a way to prevent that.

However, you can easily record intermediate drafts of your work. If you think
you might want to go back to the current state of the working-copy commit,
simply use jj new. There's no need for the commit to be "finished" or even
have a description.

Then future edits will go into a new working-copy commit on top of the now
former working-copy commit. Whenever you are happy with another set of edits,
use jj squash to amend the previous commit.

If you have changes you never want to put in a public commit, see: How can I
keep my scratch files in the repository without committing
them?

For more options see the next question.

Can I interactively create a new commit from only some of the changes in the working copy, like `git add -p && git commit` or `hg commit -i`?

Since the changes are already in the working-copy commit, the equivalent to
git add -p && git commit/git commit -p/hg commit -i is to split the
working-copy commit with jj split -i (or the practically identical
jj commit -i).

For the equivalent of git commit --amend -p/hg amend -i, use jj squash -i.

Is there something like `git rebase --interactive` or `hg histedit`?

It is often sufficient to use some form of jj rebase with -A/-B. For
example, if you have a linear chain of revisions A through C and want to
move C before B, use jj rebase -r C -B B. See jj help rebase for more
examples.

For more complicated cases, try jj arrange. It is still under development,
and will gain more features over time.

To squash or split commits, use jj squash and jj split.

How can I keep my scratch files in the repository without committing them?

You can set snapshot.auto-track to only start tracking new files matching the
configured pattern (e.g. "none()"). Changes to already tracked files will
still be snapshotted by every command. Files not matching the pattern can be
tracked with jj file track.

You can keep your notes and other scratch files in the repository, if you add
a wildcard pattern to either the repo's gitignore or your global gitignore.
Something like *.scratch or *.scratchpad should do, after that rename the
files you want to keep around to match the pattern.

If you keep your scratch files in their own directory with no tracked files, you
can create a .gitignore file in that directory containing only *. This will
ignore everything in the directory including the .gitignore file itself.

If $EDITOR integration is important, something like scratchpad.* may be more
helpful, as you can keep the filename extension intact (it matches
scratchpad.md, scratchpad.rs and more). Another option is to add a directory
which would store all your temporary files and notes to your configured global
gitignore file. For example, you could add scratch/ to it and then store
arbitrary files in <your-git-repo>/scratch/.

You can find more details on gitignore files here.

How can I avoid committing my local-only changes to tracked files?

Suppose your repository tracks a file like secret_config.json, and you make
some changes to that file to work locally. Since Jujutsu automatically commits
the working copy, there's no way to prevent Jujutsu from committing changes to
the file. But, you never want to push those changes to the remote repository.

One solution is to keep these changes in a separate commit branched from the
trunk. To use those changes in your working copy, merge the private commit
into your branch.

Suppose you have a commit "Add new feature":

shell
$ jj log
@  xxxxxxxx [email protected] 2024-08-21 11:13:21 ef612875
│  Add new feature
◆  yyyyyyyy [email protected] 2024-08-21 11:13:09 main b624cf12
│  Existing work
~

First, create a new commit branched from main and add your private changes:

shell
$ jj new main -m "private: my credentials"
Working copy  (@) now at: wwwwwwww 861de9eb (empty) private: my credentials
Parent commit (@-)      : yyyyyyyy b624cf12 main | Existing work
Added 0 files, modified 1 files, removed 0 files

$ echo '{ "password": "p@ssw0rd1" }' > secret_config.json

Now create a merge commit with the branch you're working on and the private
commit:

shell
$ jj new xxxxxxxx wwwwwwww
Working copy  (@) now at: vvvvvvvv ac4d9fbe (empty) (no description set)
Parent commit (@-)      : xxxxxxxx ef612875 Add new feature
Parent commit (@-)      : wwwwwwww 2106921e private: my credentials
Added 0 files, modified 1 files, removed 0 files

$ jj log
@    vvvvvvvv [email protected] 2024-08-22 08:57:40 ac4d9fbe
├─╮  (empty) (no description set)
│ ○  wwwwwwww [email protected] 2024-08-22 08:57:40 2106921e
│ │  private: my credentials
○ │  xxxxxxxx [email protected] 2024-08-21 11:13:21 ef612875
├─╯  Add new feature
◆  yyyyyyyy [email protected] 2024-08-21 11:13:09 main b624cf12
│  Existing work
~

Now you're ready to work:

  • Your work in progress xxxxxxxx is the first parent of the merge commit.
  • The private commit wwwwwwww is the second parent of the merge commit.
  • The working copy (vvvvvvvv) contains changes from both.

As you work, squash your changes using jj squash --into xxxxxxxx.

If you need a new empty commit on top of xxxxxxxx you can use the
--insert-after and --insert-before options (-A and -B for short):

shell
# Insert a new commit after xxxxxxxx
$ jj new --no-edit -A xxxxxxxx -m "Another feature"
Working copy  (@) now at: uuuuuuuu 1c3cff09 (empty) Another feature
Parent commit (@-)      : xxxxxxxx ef612875 Add new feature

# Insert a new commit between yyyyyyyy and vvvvvvvv
$ jj new --no-edit -A yyyyyyyy -B vvvvvvvv -m "Yet another feature"
Working copy  (@) now at: tttttttt 938ab831 (empty) Yet another feature
Parent commit (@-)      : yyyyyyyy b624cf12 Existing work

To avoid pushing change wwwwwwww by mistake, use the configuration
git.private-commits:

shell
jj config set --user git.private-commits "'''description('private:*')'''"

I accidentally changed files in the wrong commit, how do I move the recent changes into another commit?

Let's say we are editing a commit for "featureA", and we forgot to run jj new or jj commit before doing some work that belongs in a new commit:

console
$ jj log
@  lnvvtrzo [email protected] 2025-02-28 21:01:10 31a347e0
│  featureA
◆  zzzzzzzz root() 00000000
$ cat file  # Oh no, the work on "feature B" should be in a separate commit!
Done with feature A
Working on feature B

The goal is to restore change lnvvtrzo to its previous state and split the
difference into a new child commit.

Step 1: Find the commit ID for the "last good version"

If you pushed lnvvtrzo already, then the version you probably want to restore
is the version on the remote. For example, if the bookmark is feature-a, then
the commit you want to restore is feature-a@origin.

Otherwise, you can find all the past versions of the working copy revision that
jj has saved
by running jj evolog, perhaps with the --patch
option. The obsolete versions will be marked as "hidden" and will have the same
change ID, but will have different commit IDs. This represents the change
evolving over time.

For example, this is what the evolog might look like after you made two edits to
the same change:

console
$ # Note the word "hidden", the commit IDs on the right,
$ # and the unchanging change ID on the left.
$ jj evolog
@  lnvvtrzo [email protected] 2025-02-28 21:01:10 31a347e0
│  featureA
│  -- operation 3cb7392c092c snapshot working copy
○  lnvvtrzo/1 [email protected] 2025-02-28 21:00:51 b8004ab8 (hidden)
│  featureA
│  -- operation 1280bfaec893 snapshot working copy
○  lnvvtrzo/2 [email protected] 2025-02-28 20:50:05 e4d831d (hidden)
   (no description set)
   -- operation 0418a5aa94b5 snapshot working copy

Since commit b800 is hidden, it is considered obsolete and jj log (without
arguments) will not show it. However, most jj operations work normally on
such commits if you refer to them by their commit ID. Hidden commits can also
be referred to by their change ID, but they require a
change offset to distinguish them (e.g. b800 can
also be referred to as lnv/1, as shown in the evolog).

To find out which of these versions is the last time before we started working
on feature B (the point where we should have created a new change, but failed to
do so), we can look at the actual changes between the evolog commits by
running jj evolog --patch:

console
$ # When was the last saved point before we started working on feature B?
$ jj evolog --patch --git  # We use `--git` to make diffs clear without colors
@  lnvvtrzo [email protected] 2025-02-28 21:01:10 31a347e0
│  featureA
│  -- operation 3cb7392c092c snapshot working copy
│  diff --git a/file b/file
│  index 2b455c4207..2a7e05a01a 100644
│  --- a/file
│  +++ b/file
│  @@ -1,1 +1,2 @@
│   Done with feature A
│  +Working on feature B
○  lnvvtrzo/1 [email protected] 2025-02-28 21:00:51 b8004ab8 (hidden)
│  featureA
│  -- operation 1280bfaec893 snapshot working copy
│  diff --git a/file b/file
│  index cb61245109..2b455c4207
│  --- a/file
│  +++ b/file
│  @@ -1,1 +1,1 @@
│  -Working on feature A
│  +Done with feature A
○  lnvvtrzo/2 [email protected] 2025-02-28 20:50:05 e4d831d (hidden)
   (no description set)
   -- operation 0418a5aa94b5 snapshot working copy
   diff --git a/file b/file
   index 0000000000..cb61245109
   --- /dev/null
   +++ b/file
   @@ 0,0 +1,1 @@
   +Working on feature A

In this example, the version of the change when we were actually done with
feature A is when we edited the file to say "Done with feature A". This state
was saved in the commit with ID b80 (the second one in the list). The
following edit (commit 31a) belongs in a new change.

Step 2: Create a new change on top of the original revision

The "featureA" change is currently at commit 31a:

console
$ jj log
@  lnvvtrzo [email protected] 2025-02-28 21:01:10 31a347e0
│  featureA
◆  zzzzzzzz root() 00000000

We'd like to create a new "featureB" change with the contents of the current
commit 31a, and we'd like the "featureA" change to be reverted to its former
state at commit b80 (see step 1 above for how we found that commit ID).

First, we create a new empty child commit on top of b80:

console
$ jj new b80 -m "featureB"
Working copy  (@) now at: pvnrkl 47171aa (empty) featureB
Parent commit (@-)      : lnvvtr/1 b8004ab (divergent) featureA

There are now two visible commits with change ID lnvvtr (commit b8004ab
and 31a347e0), so we call these divergent. Similarly
to hidden commits, divergent commits also require a
change offset when using the change ID to refer to
them, so you can see that b8004ab is still shown as lnvvtr/1 in the output.
This temporary divergence is okay and will be resolved in the next steps.

Next, restore the contents of 31a347e0 into the working copy:

console
$ jj restore --from 31a347e0
Working copy  (@) now at: pvnrkl 468104c featureB
Parent commit (@-)      : lnvvtr/1 b8004ea (divergent) featureA
$ cat file
Done with feature A
Working on feature B

Step 3: Move any bookmarks to the original revision

console
$ jj bookmark move --from 31a347e0 --to b8004ea8

Step 4: Abandon the unwanted revision

console
$ jj abandon 31a347e0

Now, we have achieved the exact state we desired:

text
$ jj log -p --git
@  pvnrklkn [email protected] 2025-02-28 21:39:29 468104c2
│  featureB
│  diff --git a/file b/file
│  index 2b455c4207..2a7e05a01a 100644
│  --- a/file
│  +++ b/file
│  @@ -1,1 +1,2 @@
│   Done with feature A
│  +Working on feature B
○  lnvvtrzo [email protected] 2025-02-28 21:00:51 b8004ab8
│  featureA
│  diff --git a/file b/file
│  new file mode 100644
│  index 0000000000..2b455c4207
│  --- /dev/null
│  +++ b/file
│  @@ -0,0 +1,1 @@
│  +Done with feature A
◆  zzzzzzzz root() 00000000
$ jj diff --from b80 --to @- # No output means these are identical
$ jj diff --from 31a --to @  # No output means these are identical

How do I resume working on an existing change?

There are two ways to resume working on an earlier change: jj new then jj squash,
and jj edit. The first is generally recommended, but jj edit can be useful. When
you use jj edit, the revision is directly amended with your new changes, making it
difficult to tell what exactly you change. You should avoid using jj edit when the
revision has a conflict, as you may accidentally break the plain-text annotations on
your state without realizing.

To start, use jj new <rev> to create a change based on that earlier revision. Make
your edits, then use jj squash to update the earlier revision with those edits.
For when you would use git stashing, use jj edit <rev> for expected behavior.
Other workflows may prefer jj edit as well.

Why are most merge commits marked as "(empty)"?

Jujutsu, like Git, is a snapshot-based VCS. That means that each commit
logically records the state of all current files in the repo. The changes in a
commit are not recorded but are instead calculated when needed by comparing the
commit's state to the parent commit's state. Jujutsu defines the changes in a
commit to be relative to the auto-merged parents (if there's only one parent,
then that merge is trivial - it's the parent commit's state). As a result, a
merge commit that was a clean merge (no conflict resolution, no additional
changes) is considered empty. Conversely, if the merge commit contains conflict
resolutions or additional changes, then it will be considered non-empty.

This definition of the changes in a commit is used throughout Jujutsu. It's
used by jj diff -r and jj log -p to show the changes in a commit. It's used
by jj rebase to rebase the changes in a commit. It's used in jj log to
indicate which commits are empty. It's used in the files() revset function
(and by jj log ) to find commits that modify a certain path. And so on.

How do I revert a merge commit? `jj revert -r ` does nothing

Jujutsu defines the changes in a merge commit (and non-merge commits) as the
changes made compared to the auto-merged parents. That means that merge commits
are often empty. As a result, jj revert or a merge commit often results in an
empty commit. To revert the changes merged in from the second parent, instead
use jj restore --from <first parent> .

Example:

text
@
|
C
| \
B D
|/
A

To revert the merge in C, create a new commit with jj new C,
then jj restore --from B, and then describe the message
with something like jj desc -m "Revert the merge of D into B. Now, commit @
undoes the merge of D into B. If necessary, you can now rebase it
elsewhere, e.g. jj rebase -r @ -o main.

How do I deal with divergent changes?

See: Handling divergent commits.

How do I deal with conflicted bookmarks ('??' after bookmark name)?

A conflicted bookmark is a bookmark that refers to multiple
different commits because jj couldn't fully resolve its desired position.
Resolving conflicted bookmarks is usually done by setting the bookmark to the
correct commit using jj bookmark move <name> --to <commit ID>.

Usually, the different commits associated with the conflicted bookmark should all
appear in the log, but if they don't you can use jj bookmark listto show all the
commits associated with it.

I'm experiencing `jj` command issues in a Vite/Vitest project, how do I fix this?

When using Vite or Vitest in a Jujutsu repository, you may experience:

  • Very slow vitest startup times
  • Timeout errors in jj terminal commands
  • Errors with 3rd party visual tools like jjk or visual-jj
  • Corrupted working_copy.lock files

This happens because Vite watches the .jj directory where Jujutsu stores its internal state.
This creates unnecessary overhead as Vite processes Jujutsu's frequent internal file changes,
which can slow down both tools and occasionally cause file access conflicts.

Solution: Configure Vite to ignore the .jj directory by adding it to the
server.watch.ignored array inside your Vite configuration, for example:

js
// vite.config.js
export default defineConfig({
  // ... other config like plugins, test setup, etc.
  server: {
    watch: {
      ignored: [
        "**/.jj/**",
      ]
    }
  },
})

Note: There was a request to include .jj
in the default ignore list, but manual configuration remains the recommended approach.

How can I manually trigger (e.g., periodic) snapshots?

Most jj commands will automatically snapshot the working copy at the start of
the command if needed. However, if you'd like to manually trigger a snapshot for
whatever reason, such as for scripting, prompt info, or periodic snapshots in
parallel with something like a
watch command, you can run
jj util snapshot. By default this command will print whether a snapshot was
taken, which you can silence with the global --quiet flag. This command is
likely most useful for scripting rather than for running on the command line by
a human.

If you want to see the ID of the current operation after this command, it would
be simpler to run jj operation log --limit 1 directly, since that command also
takes a snapshot if needed.

I want to write a tool which integrates with Jujutsu. Should I use the library or parse the CLI?

There are some trade-offs and there is no definitive answer yet.

  • Using jj-lib avoids parsing command output and makes error handling easier.
  • jj-lib is not a stable API, so you may have to make changes to your tool
    when the API changes.
  • The CLI is not stable either, so you may need to make your tool detect the
    different versions and call the right command.
  • Using the CLI means that your tool will work with custom-built jj binaries,
    like the one at Google (if you're using the library, you will not be able to
    detect custom backends and more).

Why is Jujutsu a separate project? Why were the features not contributed to Git instead?

The project started as an experiment with the idea of representing the working
copy by a regular commit. I (@martinvonz) considered how this feature would
impact the Git CLI if it were added to Git. My conclusion was that it would
effectively result in deprecating most existing Git commands and flags in favor
of new commands and flags, especially considering I wanted to also support
revsets. This seemed unlikely to be accepted by the Git project.


Bookmarks (`docs/bookmarks.md`)

Bookmarks

Introduction

Bookmarks are named pointers to revisions (just like branches are in Git). You
can move them without affecting the target revision's identity. Bookmarks
automatically move when revisions are rewritten (e.g. by jj rebase). You can
pass a bookmark's name to commands that want a revision as argument. For example,
jj new main will create a new revision on top of the main bookmark. Use
jj bookmark list to list bookmarks and jj bookmark <subcommand> to create,
move, or delete bookmarks. There is currently no concept of an
active/current/checked-out bookmark.

Mapping to Git branches

Jujutsu maps its bookmarks to Git branches when interacting with Git repos. For
example, jj git push --bookmark foo will push the state of the foo bookmark
to the foo branch on the Git remote. Similarly, if you create a bar branch
in the backing Git repo, then a subsequent jj git import will create a bar
bookmark (reminder: that import happens automatically in
colocated workspaces).

Remotes and tracked bookmarks

Jujutsu records the last seen position of a bookmark on each remote (just like
Git's remote-tracking branches). This record is updated on every jj git fetch
and jj git push of the bookmark. You can refer to the remembered remote bookmark
positions with @<remote name>, such as jj new main@origin. jj
does not provide a way to manually edit these recorded positions.

A remote bookmark can be associated with a local bookmark of the same name. This
is called a tracked remote bookmark (which maps to a Git remote branch when
using the Git backend). When you pull a tracked bookmark from a remote, any
changes compared to the current record of the remote's state will be propagated
to the corresponding local bookmark, which will be created if it doesn't exist
already.

!!! note "Details: how fetch pulls bookmarks"

text
Let's say you run `jj git fetch --remote origin` and, during the fetch, `jj`
determines that the remote's `main` bookmark has been moved so that its target is
now ahead of the local record in `main@origin`.

`jj` will then update `main@origin` to the new target. If `main@origin` is
**tracked**, `jj` will also apply the change to the local bookmark `main`. If the
local target has also been moved compared to `main@origin` (probably because you
ran `jj bookmark set main`), then the two updates will be merged. If one is ahead
of the other, then that target will become the new target. Otherwise, the local
bookmark will become conflicted (see the ["Conflicts" section](#conflicts) below
for details).

Most commands don't show the tracked remote bookmark if it has the same target as
the local bookmark. The local bookmark (without @<remote name>) is considered the
bookmark's desired target. Consequently, if you want to update a bookmark on a
remote, you first update the bookmark locally and then push the update to the
remote. If a local bookmark also exists on some remote but points to a different
target there, jj log will show the bookmark name with an asterisk suffix (e.g.
main*). That is meant to remind you that you may want to push the bookmark to
some remote.

If you want to know the internals of bookmark tracking, consult the
Design Doc.

Terminology summary

  • A remote bookmark is a bookmark ref on the remote. jj can find out its
    actual state only when it's actively communicating with the remote. However,
    jj does store the last-seen position of the remote bookmark; this is the
    commit jj show @<remote name> would show. This notion is
    completely analogous to Git's "remote-tracking branches".
  • A tracked (remote) bookmark is defined above. You can make a remote bookmark
    tracked with the jj bookmark track command, for
    example.
  • A tracking (local) bookmark is the local bookmark that jj tries to keep
    in sync with the tracked remote bookmark. For example, after jj bookmark track mybookmark --remote=origin, there will be a local bookmark mybookmark
    that's tracking the remote mybookmark@origin bookmark. A local bookmark can
    track a bookmark of the same name on 0 or more remotes.

The notion of tracked bookmarks serves a similar function to the Git notion of an
"upstream branch". Unlike Git, a single local bookmark can be tracking remote
bookmarks on multiple remotes, and the names of the local and remote bookmarks
must match.

Manually tracking a bookmark

To track a bookmark permanently use jj bookmark track --remote=<remote name>. It will now be imported as a local bookmark until you
untrack it or it is deleted on the remote.

Example:

sh
$ # List all available bookmarks, as we want our colleague's bookmark.
$ jj bookmark list --all
$ # Find the bookmark.
$ # [...]
$ # Actually track the bookmark.
$ jj bookmark track <bookmark name> --remote=<remote name> # Example: jj bookmark track my-feature --remote=origin
$ # From this point on, <bookmark name> will be imported when fetching from <remote name>.
$ jj git fetch --remote <remote name>
$ # A local bookmark <bookmark name> should have been created or updated while fetching.
$ jj new <bookmark name> # Do some local testing, etc.

Untracking a bookmark

To stop following a remote bookmark, you can jj bookmark untrack it. After that,
subsequent fetches of that remote will no longer move the local bookmark to match
the position of the remote bookmark.

Example:

sh
$ # List all local and remote bookmarks.
$ jj bookmark list --all
$ # Find the bookmark we no longer want to track.
$ # [...]
# # Actually untrack it.
$ jj bookmark untrack <bookmark name> --remote=<remote name> # Example: jj bookmark untrack stuff --remote=origin
$ # From this point on, this remote bookmark won't be imported anymore.
$ # The local bookmark (e.g. stuff) is unaffected. It may or may not still
$ # be tracking bookmarks on other remotes (e.g. stuff@upstream).

Listing tracked bookmarks

To list tracked bookmarks, you can jj bookmark list --tracked or jj bookmark list -t.
This command omits local Git-tracking bookmarks by default.

You can see if a specific bookmark is tracked with jj bookmark list --tracked .

Automatic tracking of bookmarks & `auto-track-bookmarks` option

There are two situations where jj tracks bookmarks automatically. jj git clone automatically sets up the default remote bookmark (e.g. main@origin) as
tracked. When you push a local bookmark, the newly created bookmark on the remote is
marked as tracked.

By default, every other remote bookmark is marked as "not tracked" when it's
fetched. If desired, you need to manually jj bookmark track them. This works
well for repositories where multiple people work on a large number of bookmarks.

The default can be changed by setting the config remotes.<name>.auto-track-bookmarks = "*".
Then, jj git fetch tracks every newly fetched bookmark with a local bookmark.
Branches that already existed before the jj git fetch are not affected. This
is similar to Mercurial, which fetches all its bookmarks (equivalent to Git's
branches) by default. Similarly, all newly created local bookmarks
will be marked as "tracked", preparing them to be pushed with
the next jj git push command. See "Automatic tracking of
bookmarks"
for details.

Bookmark updates

Currently Jujutsu automatically updates local bookmarks when these conditions are
met:

  • When a commit has been rewritten (e.g, when you rebase) bookmarks and the
    working-copy will move along with it.
  • When a commit has been abandoned, all associated bookmarks will be deleted.

You could describe the updates as following along the change-id of the
current bookmark commit, even if it isn't entirely accurate.

Pushing bookmarks: Safety checks

Before jj git push actually moves, creates, or deletes a remote bookmark, it
makes several safety checks.

  1. jj will contact the remote and check that the actual state of the remote
    bookmark matches jj's record of its last known position. If there is a
    conflict, jj will refuse to push the bookmark. In this case, you need to run
    jj git fetch --remote <remote name> and resolve the resulting bookmark
    conflict. Then, you can try jj git push again.

    If you are familiar with Git, this makes jj git push similar to git push --force-with-lease.

    There are a few cases where jj git push will succeed even though the remote
    bookmark is in an unexpected location. These are the cases where jj git fetch
    would not create a bookmark conflict and would not move the local bookmark, e.g.
    if the unexpected location is identical to the local position of the bookmark.

  2. The local bookmark must not be conflicted. If it is, you would
    need to use jj bookmark move, for example, to resolve the conflict.

    This makes jj git push safe even if jj git fetch is performed on a timer
    in the background (this situation is a known issue[^known-issue] with some
    forms of git push --force-with-lease). If the bookmark moves on a remote in a
    problematic way, jj git fetch will create a conflict. This should ensure
    that the user becomes aware of the conflict before they can jj git push and
    override the bookmark on the remote.

  3. If the remote bookmark already exists on the remote, it must be
    tracked.

[^known-issue]: See "A general note on safety" in
https://git-scm.com/docs/git-push#Documentation/git-push.txt---no-force-with-lease

Conflicts

Bookmarks can end up in a conflicted state. When that happens, jj status will
include information about the conflicted bookmarks (and instructions for how to
mitigate it). jj bookmark list will have details. jj log will show the bookmark
name with a double question mark suffix (e.g. main??) on each of the
conflicted bookmark's potential target revisions. Using the bookmark name to look up
a revision will resolve to all potential targets. That means that jj new main
will error out, complaining that the revset resolved to multiple revisions.

Both local bookmarks (e.g. main) and the remote bookmark (e.g. main@origin) can
have conflicts. Both can end up in that state if concurrent operations were run
in the repo. The local bookmark more typically becomes conflicted because it was
updated both locally and on a remote.

To resolve a conflicted state in a local bookmark (e.g. main), you can move the
bookmark to the desired target with jj bookmark move. You may want to first either
merge the conflicted targets with jj new (e.g. jj new main), or you may want to
rebase one side on top of the other with jj rebase.

To resolve a conflicted state in a remote bookmark (e.g. main@origin), simply
pull from the remote (e.g. jj git fetch). The conflict resolution will also
propagate to the local bookmark (which was presumably also conflicted).

Ease of use

The use of bookmarks is frequent in some workflows, for example, when
interacting with Git repositories containing branches. To this end,
one-letter shortcuts have been implemented, both for the jj bookmark
command itself through an alias (as jj b), and for its subcommands.
For example, jj bookmark create BOOKMARK-NAME -r@ can be abbreviated as
jj b c BOOKMARK-NAME -r@.


Changelog (`docs/changelog.md`)

{%
include-markdown "../CHANGELOG.md"
rewrite-relative-urls=true
comments=true
%}


Cli Reference (`docs/cli-reference.md`)

!!! warning

text
This CLI reference is experimental. It is automatically generated, but
does not match the `jj help` output exactly.

Run `jj help <COMMAND>` for more authoritative documentation.

If you see a significant difference, feel free to file a bug, or a PR to note the difference here.

{%
include-markdown "../cli/tests/[email protected]"
rewrite-relative-urls=false
start=""
comments=true
%}


Code Of Conduct (`docs/code-of-conduct.md`)

Contributor Covenant Code of Conduct

Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socioeconomic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

  • Demonstrating empathy and kindness toward other people
  • Being respectful of differing opinions, viewpoints, and experiences
  • Giving and gracefully accepting constructive feedback
  • Accepting responsibility and apologizing to those affected by our mistakes,
    and learning from the experience
  • Focusing on what is best not just for us as individuals, but for the overall
    community

Examples of unacceptable behavior include:

  • The use of sexualized language or imagery, and sexual attention or advances of
    any kind
  • Trolling, insulting or derogatory comments, and personal or political attacks
  • Public or private harassment
  • Publishing others' private information, such as a physical or email address,
    without their explicit permission
  • Other conduct which could reasonably be considered inappropriate in a
    professional setting

Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may
be reported to the community leaders responsible for enforcement at two or
more of [email protected], [email protected], [email protected], and
[email protected]. All complaints will be reviewed and investigated promptly
and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

1. Correction

Community Impact: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

Consequence: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

2. Warning

Community Impact: A violation through a single incident or series of
actions.

Consequence: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.

3. Temporary Ban

Community Impact: A serious violation of community standards, including
sustained inappropriate behavior.

Consequence: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

4. Permanent Ban

Community Impact: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

Consequence: A permanent ban from any sort of public interaction within the
community.

Attribution

This Code of Conduct is adapted from the Contributor Covenant,
version 2.1, available at
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.

Community Impact Guidelines were inspired by
Mozilla's code of conduct enforcement ladder.

For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.


Community Tools (`docs/community_tools.md`)

Community-built tools around Jujutsu

Many of these tools are not complete yet, just like Jujutsu itself. But they
already simplify many workflows and can improve your experience.

!!! warning
The listed tools are community‑maintained; the Jujutsu project does not
review, endorse, or guarantee their quality or security.

Diffedit3

Diffedit3 is a web-based alternate to Meld, as it no longer is packaged and
available for all Distros. Its creator is also a frequent contributor.

Find it here

GG - GUI for JJ

GG is a cross platform GUI for Jujutsu which makes all graph manipulating
workflows quite easy. Take a look at its README.md as it quite descriptive.

Find it here.

Hunk.nvim

Hunk.nvim is a Neovim based diff-editor for Jujutsu which can be used as an
alternative to the default :builtin diff-editor.

Find it here.

JayJay

JayJay is a native macOS GUI for Jujutsu built with Rust and SwiftUI. It features
a DAG graph view, Myers diff with syntax highlighting, side-by-side diffs, conflict
resolution, bookmark manager, command palette, and AI commit messages. The diff
engine is available as a standalone Rust crate (jj-diff).

Find it here.

JJ-FZF

Centered around the jj log graph view, jj-fzf provides previews of diffs, the
evolution-log, browses the op log and offers a large number of key bindings for
commonly used jj operations from rebase to undo, and helps with divergent commits.

Find it here.

jj-hunk

jj-hunk is a command-line tool for programmatic hunk selection. It can split,
commit, and squash selected hunks without opening an interactive diff editor,
which is useful for scripts and AI coding agents.

Find it here.

JJ TUI

This is TUI for Jujutsu built in Ocaml, it is unopiniated and its creator is
open to feedback.

Find it here.

Jujutsu Kaizen

Jujutsu Kaizen is a plugin for Visual Studio Code. The goal of this extension is to bring the great UX of Jujutsu into the VS Code UI.
Its developers are currently focused on achieving parity for commonly used features of VS Code's built-in Git extension, such as the various operations possible via the Source Control view.

Find it here.

LazyJJ

lazyjj is a lazygit inspired TUI for Jujutsu.

Find it here.

Visual Jujutsu

VJJ is a fzf (fuzzy finder) wrapper for Jujutsu, which is meant to be used
interactively in the terminal.

Find it here.

VisualJJ

VisualJJ is a plugin for Visual Studio Code which provides native integration
for Jujutsu, not relying on Git colocation. Unlike other tools on this page,
VisualJJ is not open-source.

Find it here.

Jujutsu UI

jjui is a terminal user interface for working with Jujutsu version control system.

Find it here.

Selvejj

Selvejj is a JetBrains plugin for integrating Jujutsu as a first-class VCS within the IDE.

Find it here and here (Marketplace).

Jujutsu plugin for IntelliJ IDEA

Native IntelliJ integration for Jujutsu.

Find it here and here (Marketplace).

PSCompletions

PSCompletions is a completion manager for a better and simpler tab-completion experience in PowerShell.

It can provide completions via psc add jj, or offer a better completion menu for the official completions.

Find it here.

LightJJ

A fast, keyboard-driven browser UI for Jujutsu.

Find it here.

JJ View

JJ View is an open-source Visual Studio Code extension that provides a rich, native graphical interface for Jujutsu. It features a Git-style Source Control (SCM) view for easy change management, an interactive visual commit history graph, and built-in commands for common jj operations like squashing, abandoning, and absorbing modifications directly from the editor. It also includes native Gerrit integration for streamlined code review workflows.

Find it here.

jj-stack

jj-stack turns a linear series of local jj changes into a stack of GitHub pull requests.
Rewrite, split, squash, or reorder your changes with jj, then run jj-stack submit to update
GitHub. Existing PRs follow their change IDs, keeping comments and review history together.

Find it here.

Finding other integrations

You can find other community contributed tools and integrations in our
Wiki.


Config (`docs/config.md`)

Configuration

These are the config settings available to jj/Jujutsu.

Config files and TOML

jj loads several types of config settings:

  • The built-in settings. These cannot be edited. They can be viewed in the
    cli/src/config/ directory in jj's source repo.

  • The user settings. These can be edited with jj config edit --user. User
    settings are located in the user config files, which can be found with
    jj config path --user.

  • The repo settings. These can be edited with jj config edit --repo, or found
    with jj config path --repo. For security reasons, they are not located
    inside the repo.

  • The workspace settings. These can be edited with jj config edit --workspace,
    or found with jj config path --workspace. For security reasons, they are not
    located inside the workspace.

  • Settings from environment variable overrides
    (for example $JJ_USER / $JJ_EMAIL).

  • Settings specified on the command-line.

These are listed in the order they are loaded; the settings from earlier items
in the list are overridden by the settings from later items if they disagree.
Every type of config except for the built-in settings is optional.

Individual config files can also be targeted with
jj config {edit,set,unset} --file .

You can enable JSON Schema validation in your editor by adding a #:schema
reference at the top of your TOML config files. See JSON Schema Support for
details.

See the TOML site and the syntax guide for a detailed description of the
syntax. We cover some of the basics below.

The first thing to remember is that the value of a setting (the part to the
right of the = sign) should be surrounded in quotes if it's a string.

Dotted style and headings

In TOML, anything under a heading can be dotted instead. For example,
user.name = "YOUR NAME" is equivalent to:

toml
[user]
name = "YOUR NAME"

For future reference, here are a couple of more complicated examples,

toml
# Dotted style
template-aliases."format_short_id(id)" = "id.shortest(12)"
colors."commit_id prefix".bold = true

# is equivalent to:
[template-aliases]
"format_short_id(id)" = "id.shortest(12)"

[colors]
"commit_id prefix" = { bold = true }

The docs below refer to keys in text using dotted notation, but example blocks
will use heading notation to be unambiguous. If you are confident with TOML then
use whichever suits you in your config. If you mix dotted keys and headings,
you must put the dotted keys before the first heading.

Escapes in strings

There are two (or four) types of strings in TOML: basic and literal, with
multi-line versions of each. Basic strings use double quotes (") and support
escape sequences, such as "\n", "\"", "\\", etc. Literal strings use
single quotes (') and do not support escape sequences. Using 3 double quotes
or 3 single quotes to wrap a string makes it multi-line, and then you can use an
unescaped double quote or single quote respectively without ending the string.

A string in your config may be interpreted twice, first by the TOML parser and
then by a Jujutsu parser. To avoid putting strings in your config that the TOML
parser processes before it gets to the Jujutsu parser, you may want to use
literal strings or literal multi-line strings in your config.toml files. For
example:

toml
[template-aliases]
# basic_template = "'hello' ++ \"\n\""
literal_template = '"hello" ++ "\n"'

basic_template is actually equivalent to this string:

text
'hello' ++ "
"

because the TOML parser interprets the \n character as part of the string.
This causes the Jujutsu parser to receive a different string than what you
literally see in the TOML file, which may result in seemingly confusing parse
error messages (such as referencing a line that you think shouldn't exist).

On the other hand, literal_template is equivalent to:

text
"hello" ++ "\n"

because the TOML parser does not escape anything in the literal string.

The same rules of basic and literal strings apply to custom Jujutsu languages:
double quotes (") allow escapes whereas single quotes (') do not. So
template-aliases.my_template = "'hello\\n'" would probably not be what you
want.

User settings

toml
[user]
name = "YOUR NAME"
email = "[email protected]"

Don't forget to change these to your own details!

These can also be set with environment variables (handy with tools like
direnv):

bash
export JJ_USER="Your Name"
export JJ_EMAIL="[email protected]"

$JJ_USER overrides user.name and $JJ_EMAIL overrides user.email. They
take precedence over values in config files (but not over --config /
--config-file). See Environment variable overrides
for the full list. For directory-based switching without env vars, use
conditional variables.

UI settings

Colorizing output

Possible values are always, never, debug and auto (default: auto).
auto will use color only when writing to a terminal. debug will print the
active labels alongside the regular colorized output.

This setting overrides the NO_COLOR environment variable (if set).

toml
[ui]
color = "never" # Turn off color

Custom colors and styles

You can customize the colors used for various elements of the UI. For example:

toml
[colors]
commit_id = "green"

The following colors are available:

  • black
  • red
  • green
  • yellow
  • blue
  • magenta
  • cyan
  • white
  • default

All of them but "default" come in a bright version too, e.g. "bright red". The
"default" color can be used to override a color defined by a parent style
(explained below).

You can also use a 6-digit hex code for more control over the exact color used:

toml
[colors]
change_id = "#ff1525"

jj also supports colors from the ANSI 256-color palette as ansi-color-<N>,
where <N> is a number between 0 and 255:

toml
[colors]
commit_id = "ansi-color-81"

If you use a string value for a color, as in the examples above, it will be used
for the foreground color. You can also set the background color, reverse colors
(swap foreground and background), or make the text bold, dim, italic,
underlined, or crossed-out. For that, you need to use a table:

toml
[colors]
commit_id = { fg = "green", bg = "#ff1525", bold = true, underline = true }
change_id = { reverse = true, dim = false, italic = true }

The key names are called "labels". The above used commit_id as label. You can
also create rules combining multiple labels. The rules work a bit like CSS
selectors. For example, if you want to color commit IDs green in general but
make the commit ID of the working-copy commit also be underlined, you can do
this:

toml
[colors]
commit_id = "green"
"working_copy commit_id" = { underline = true }

Parts of the style that are not overridden - such as the foreground color in the
example above - are inherited from the style of the parent label.

Which elements can be colored is not yet documented, but see the
default color configuration for some examples of what's possible.

Default command

When jj is run with no explicit subcommand, the value of the
ui.default-command setting will be used instead. Possible values are any valid
subcommand name, subcommand alias, or user-defined alias (defaults to "log").

toml
[ui]
default-command = ["log", "--reversed"]

Default description

The editor content of a commit description can be populated by the
draft_commit_description template. self is a Commit
object
.

toml
[templates]
draft_commit_description = '''
concat(
  builtin_draft_commit_description,
  "\nJJ: ignore-rest\n",
  diff.git(),
)
'''

You can override only the default_commit_description value if you like, e.g.:

toml
[template-aliases]
default_commit_description = '''
"

Closes #NNNN
"
'''

Duplicate commit description

By default, jj duplicate copies the descriptions from the original commits.
You can customize this behavior by specifying the duplicate_description
template, which is given a Commit object of the
original commit.

toml
[templates]
duplicate_description = '''
concat(
  description.trim_end(),
  "\n\n(cherry picked from commit ",
  commit_id,
  ")\n"
)
'''

Note that description usually ends with a \n if it is not blank. Use
.trim_end() to remove the \n.

New commit description

When jj new creates a commit without an explicit -m message, it evaluates
the new_description template to populate the description. By default, this
template is empty.

You can customize this to automatically generate descriptions for new commits.
For example, to auto-generate merge commit messages:

toml
[templates]
new_description = '''
if(parents.len() > 1,
  "Merge " ++ parents.skip(1).map(|p| try(
    p.bookmarks().first().name(),
    p.change_id().shortest(8)
  )).join(", ") ++ " into " ++ try(
    parents.first().bookmarks().first().name(),
    parents.first().change_id().shortest(8)
  ) ++ "\n",
  ""
)
'''

Bookmark / tag listing order

By default, jj bookmark list and jj tag list display bookmarks and tags
sorted alphabetically by name. You can customize this sorting behavior by
specifying sort keys in your config file:

toml
[ui]
bookmark-list-sort-keys = ["name"]
tag-list-sort-keys = ["name"]

The configuration works identically to using the --sort option. The following
sort keys are supported: name, author-name, author-email, author-date,
committer-name, committer-email, committer-date. Suffix the key with -
to sort in descending order. Multiple keys can be supplied here, the first key
is the most significant.

When the --sort option is used, the configuration is ignored.

Bookmark advance default targets

The jj bookmark advance command moves bookmarks forward in the graph. The
default values for what bookmarks to advance and where to advance them to are:

toml
[revsets]
bookmark-advance-from = 'heads(::to & bookmarks())'  # The closest bookmarks
bookmark-advance-to = '@'  # To the current working copy

The default from is likely to be a good fit for almost everyone. The from
revset has access to to, allowing it to find bookmarks relative to the
destination revision.

The default to is largely up to your preference and workflow. One simple
alternative which fits squash-heavy workflows is @-, while a more involved
and versatile alternative that advances to the closest "pushable" revision is:

toml
[revsets]
bookmark-advance-to = 'closest_pushable(@)'

[revset-aliases]
# Closest revision that is mutable, described and either non-empty or a merge
'closest_pushable(to)' = '''
  heads(::to & mutable() & ~description(exact:"") & (~empty() | merges()))
'''

Commit trailers

You can configure automatic addition of one or more trailers to commit
descriptions using the commit_trailers template.

Each line of the template is an individual trailer, usually in Key: Value
format.

Trailers defined in this template are deduplicated with the existing
added again. To deduplicate based only on the trailer key, use the
trailers.contains_key(key) method within the template.

toml
[templates]
commit_trailers = '''
format_signed_off_by_trailer(self)
++ if(!trailers.contains_key("Change-Id"), format_gerrit_change_id_trailer(self))'''

Some ready-to-use trailer templates are available for frequently used trailers:

  • format_signed_off_by_trailer(commit) creates a "Signed-off-by" trailer
    using the committer info.
  • format_gerrit_change_id_trailer(commit) creates a "Change-Id" trailer
    suitable to be used with Gerrit. It is based Jujutsu's change id.

Existing trailers are also accessible via commit.trailers().

Diff colors and styles

In color-words and git diffs, word-level hunks are rendered with underline. You
can override the default style with the following keys:

toml
[colors]
# Highlight hunks with background
"diff removed token" = { bg = "#221111", underline = false }
"diff added token" = { bg = "#002200", underline = false }
# Alternatively, swap colors
"diff token" = { reverse = true, underline = false }

In color-words diffs, line numbers of context lines are rendered with decreased
intensity to highlight changed lines. You can also decrease intensity of context
contents.

toml
[colors]
# Dim context line numbers (default)
"diff context line_number" = { dim = true }
# Dim context line numbers and contents (applies to color-words and git diffs)
"diff context" = { dim = true }

Colors and styles can be configured differently for each diff format:

toml
[colors]
# Dim unchanged parts of changed lines in color-words diffs
"diff color_words removed" = { dim = true }
"diff color_words added" = { dim = true }
"diff color_words removed token" = { dim = false }
"diff color_words added token" = { dim = false }

Diff format

toml
[ui]
# Builtin formats: ":color-words" (default), ":git",
#                  ":summary", ":stat", ":types", ":name-only"
# or external command name and arguments (see below)
diff-formatter = ":git"

Color-words diff options

In color-words diffs, changed words are displayed inline by default when color
is enabled. When the formatter cannot emit color, changed words are always shown
on separate lines because inline changes would not be distinguishable.

Because it's difficult to read a diff line with many removed/added words, there's
a threshold to switch to traditional separate-line format in colorized output.
You can also change the default number of lines of context shown.

  • max-inline-alternation: Maximum number of removed/added word alternation to
    inline. For example, <added> ... <added> sequence has 1 alternation, so the
    line will be inline if max-inline-alternation >= 1. <added> ... <removed> ... <added> sequence has 3 alternation.

    • 0: disable inlining, making --color-words more similar to --git
    • 1: inline removes-only or adds-only lines
    • 2, 3, ..: inline up to 2, 3, .. alternation
    • -1: inline all lines

    The default is 3.

    This parameter is experimental. The definition is subject to change.

  • conflict: How conflicts are processed and displayed.

    • "materialize": compare materialized contents (default)
    • "pair": compare individual pairs

    This parameter is experimental.

  • context: Number of lines of context to show in the diff. The default is 3.

toml
[diff.color-words]
max-inline-alternation = 3
context = 3

Git diff options

In git diffs you can change the default number of lines of context shown.

  • context: Number of lines of context to show in the diff. The default is 3.

  • show-path-prefix: Whether to show the a/ and b/ path prefixes in
    diff --git output. The default is true.

toml
[diff.git]
context = 3
show-path-prefix = true

Diff stat options

When using a subcommand that displays the diff stat menu like jj show --stat,
the bar portion that renders as ++-- can be limited to a maximum width in
characters.

toml
[diff.stat]
max-bar-width = 10

By default without this option set, the bar width is unbounded and will use the
remaining available space, with a minimum of 30% of the total width.

Generating diffs by external command

If ui.diff-formatter is not a builtin format, the specified diff command will
be called.

toml
[ui]
# Use Difftastic by default
diff-formatter = ["difft", "--color=always", "$left", "$right"]
# Use tool named "<name>" (see below)
diff-formatter = "<name>"

The external diff tool can also be enabled by diff --tool <name> argument.
For the tool named <name>, command arguments can be configured as follows.

toml
[merge-tools.<name>]
# program = "<name>"  # Defaults to the name of the tool if not specified
diff-args = ["--color=always", "$left", "$right"]
  • $left and $right are replaced with the paths to the left and right
    directories to diff respectively.

  • $width is replaced with the number of terminal columns available to the diff
    content.

  • If diff-args is not specified, ["$left", "$right"] will be used by default.

  • If diff-args = [], jj will refuse to use this tool for diff formatting.
    This is a way to explicitly state that a certain tool (e.g. mergiraf) does
    not work for viewing diffs.

By default jj will invoke external tools with a directory containing the left
and right sides. The diff-invocation-mode config can change this to file by file
invocations as follows:

toml
[ui]
diff-formatter = "vimdiff"

[merge-tools.vimdiff]
diff-invocation-mode = "file-by-file"

By default jj will display a warning when the command exits with a non-success
error code. The diff-expected-exit-codes config can suppress this warning
message for specific exit codes:

toml
[merge-tools.delta]
diff-expected-exit-codes = [0, 1]

Conflict marker style

You can configure which style of conflict markers to use when materializing
conflicts:

toml
[ui]
# Shows a single snapshot and one or more diffs to apply to it
conflict-marker-style = "diff"
# Shows a snapshot for each side and base of the conflict
conflict-marker-style = "snapshot"
# Uses Git's "diff3" conflict markers to support tools that depend on it
conflict-marker-style = "git"

For more details about these conflict marker styles, see the conflicts
page
.

Set of immutable commits

You can configure the set of immutable commits via
revset-aliases."immutable_heads()". The default set of immutable heads is
builtin_immutable_heads(), which in turn is defined as trunk() | tags() | untracked_remote_bookmarks() | untracked_remote_tags(). For example, to also
consider the release@origin bookmark immutable:

toml
[revset-aliases]
"immutable_heads()" = "builtin_immutable_heads() | release@origin"

To prevent rewriting commits authored by other users:

toml
# The `trunk().. &` bit is an optimization to scan for non-`mine()` commits
# only among commits that are not in `trunk()`.
[revset-aliases]
"immutable_heads()" = "builtin_immutable_heads() | (trunk().. & ~mine())"

Ancestors of the configured set are also immutable. The root commit is always
immutable even if the set is empty.

Immutable commits (other than the root commit) can be rewritten using the
--ignore-immutable CLI flag.

!!! warning

text
Using `--ignore-immutable` will allow you to rewrite any commit in the
history, and all descendants, without warning. Use this power wisely, and
remember `jj undo`.

Behavior of prev and next commands

If you prefer using an "edit-based" workflow, rather than squashing
modifications into parent changes, you may find yourself using the prev and
next commands with their --edit flag often to move between your changes. You
can avoid having to type the --edit flag every time you need it by actually
making it the default:

toml
[ui.movement]
edit = true

You can pass the --no-edit flag to prev and next if you find yourself
needing the original behavior.

List

Default Template

You can configure the template used when no -T is specified.

  • templates.config_list for jj config list
  • templates.workspace_list for jj workspace list

If you want to see the config variable origin (type and path) when you do jj config list
you can add this to your config:

toml
[templates]
config_list = "builtin_config_list_detailed"

Log

Default revisions

You can configure the revisions jj log would show when neither -r nor any paths are specified.

toml
[revsets]
# Show commits that are not in `main@origin`
log = "main@origin.."

The default value for revsets.log is builtin_log(), which is defined as
'present(@) | ancestors(immutable_heads().., 2) | trunk()'. You can use
builtin_log() as a starting point for your own custom log revset.

!!! warning

text
When `revsets.short-prefixes` is not specified, it defaults to the value of `revsets.log`. This
affects how change and commit id references are resolved when multiple commits share a common
prefix. Changing `revsets.log` without also setting `revsets.short-prefixes` might increase the
length of prefixes needed on the command-line, especially in large repositories.

Default revisions for operation diffs

You can configure the set of revisions that are considered "interesting" when
showing the difference between two operations (e.g. in jj op show,
jj op diff, or jj op log -p). Revisions that are not in this set are
elided into a summary count.

toml
[revsets]
# Only show mutable revisions and the trunk
op-diff-changes-in = "mutable() | trunk()"

The default value for revsets.op-diff-changes-in is
'mutable() | immutable_heads()'.

Prioritize Revsets in the Log over @

In some situations the default graph can be hard to read, for example when working with big merges.
To improve this behavior you can configure which revset in the jj log graph is displayed on
the left instead of @.

The following example will prioritize the change with the description "megamerge" with a fallback
to trunk() in case no such change exists:

toml
[revsets]
log-graph-prioritize = 'coalesce(description("megamerge\n"), trunk())'

Default Template

You can configure the template used when no -T is specified.

  • templates.evolog for jj evolog
  • templates.log for jj log
  • templates.show for jj show
  • templates.op_log for jj op log
  • templates.op_show for jj op show
toml
[templates]
# Use builtin evolog template
evolog = "builtin_evolog_compact"
# Use builtin log template
log = "builtin_log_compact"
# Use builtin show template
show = "builtin_log_detailed"
# Use builtin op log template
op_log = "builtin_op_log_compact"
# Use builtin op log template
op_show = "builtin_op_log_compact"

If you want to see the full description when you do jj log you can add this to
your config:

toml
[templates]
log = "builtin_log_compact_full_description"

Graph style

toml
[ui]
# Possible values: "curved" (default), "square", "ascii", "ascii-large"
graph.style = "square"

Node style

The symbols used to represent commits or operations can be customized via
templates.

  • templates.log_node for commits (with Option<Commit> keywords)
  • templates.op_log_node for operations (with Operation keywords)

For example:

toml
[templates]
log_node = '''
coalesce(
  if(!self, "🮀"),
  if(current_working_copy, "@"),
  if(root, "┴"),
  if(immutable, "●", "○"),
)
'''
op_log_node = 'if(current_operation, "@", "○")'

Wrap log content

If enabled, log/evolog/op log content will be wrapped based on
the terminal width.

toml
[ui]
log-word-wrap = true

Display of commit and change ids

Can be customized by the format_short_id() template alias.

toml
[template-aliases]
# Highlight unique prefix and show at least 12 characters (default)
'format_short_id(id)' = 'id.shortest(12)'
# Just the shortest possible unique prefix
'format_short_id(id)' = 'id.shortest()'
# Show unique prefix and the rest surrounded by brackets
'format_short_id(id)' = 'id.shortest(12).prefix() ++ "[" ++ id.shortest(12).rest() ++ "]"'
# Always show 12 characters
'format_short_id(id)' = 'id.short(12)'

To customize these separately, use the format_short_commit_id() and
format_short_change_id() aliases:

toml
[template-aliases]
# Uppercase change ids. `jj` treats change and commit ids as case-insensitive.
'format_short_change_id(id)' = 'format_short_id(id).upper()'

Operation ids can be customized by the format_short_operation_id() alias:

toml
[template-aliases]
# Always show 12 characters
'format_short_operation_id(id)' = 'id.short(12)'

To get shorter prefixes for certain revisions, set revsets.short-prefixes:

toml
[revsets]
# Prioritize the current bookmark
short-prefixes = "(main..@)::"

Relative timestamps

Can be customized by the format_timestamp() template alias.

toml
[template-aliases]
# Full timestamp in ISO 8601 format
'format_timestamp(timestamp)' = 'timestamp'
# Relative timestamp rendered as "x days/hours/seconds ago"
'format_timestamp(timestamp)' = 'timestamp.ago()'

jj op log defaults to relative timestamps. To use absolute timestamps, you
will need to modify the format_time_range() template alias.

toml
[template-aliases]
'format_time_range(time_range)' = 'time_range.start() ++ " - " ++ time_range.end()'

Author format

Can be customized by the format_short_signature() template alias.

toml
[template-aliases]
# Full email address (default)
'format_short_signature(signature)' = 'signature.email()'
# Both name and email address
'format_short_signature(signature)' = 'signature'
# Username part of the email address
'format_short_signature(signature)' = 'signature.email().local()'

Commit timestamp

Commits have both an "author timestamp" and "committer timestamp". By default,
jj displays the committer timestamp, but can be changed to show the author
timestamp instead.

The function must return a timestamp because the return value will likely be
formatted with format_timestamp().

toml
[template-aliases]
'commit_timestamp(commit)' = 'commit.author().timestamp()'

Signature format

Can be enabled with ui.show-cryptographic-signatures, and
customized with format_short_cryptographic_signature(sig) and
format_detailed_cryptographic_signature(sig).

Note that the formatting functions take an Option<CryptographicSignature>.
This allows you to emit a custom message if a signature is not present, but
will raise an error if you try to access methods on a signature that is not
available.

toml
[ui]
# default is false
show-cryptographic-signatures = true

[template-aliases]
'format_short_cryptographic_signature(sig)' = '''
  if(sig,
    sig.status(),
    "(no sig)",
  )
'''

Pager

By default, jj will paginate output that would scroll off the screen. It does
this by passing output through less -FRXK on most platforms (on Windows it uses
the pager that is built-in to jj).

Which pager to use can be customized by setting ui.pager. When choosing a
pager, ensure that it either supports color codes or that you disable color (see
Colorizing output).

The JJ_PAGER environment variable overrides ui.pager, and is useful for
tool-specific environments that should not affect other programs. The generic
PAGER environment variable is ignored.

Examples:

shell
# Pipe output through `less -FRXK` (default on non-Windows platforms)
$ jj config set --user ui.pager "less -FRXK"

# Use the built-in pager (default on Windows)
$ jj config set --user ui.pager :builtin

# Use `$PAGER` environment variable if set (on non-Windows platforms)
$ jj config set --user ui.pager '["sh", "-c", "exec ${PAGER:-less -FRXK}"]'

Additionally, paging behavior can be toggled via ui.paginate like so:

toml
[ui]
# Enable pagination for commands that support it (default)
paginate = "auto"
# Disable all pagination, equivalent to using --no-pager
paginate = "never"

Builtin pager

Our builtin pager is based on
streampager but is configured within
jj's config. It is configured via the ui.streampager table.

Key bindings

The built-in pager supports both navigation via arrows and Vim-style navigation.
Beyond that, here are some useful keybindings for the pager:

Key Action
Ctrl-c or q Quit
h or F1 Show all key bindings
Esc Close help or prompt
\ Toggle line wrapping
# Toggle line numbers
Ctrl-r Toggle the ruler

The built-in pager does not support mouse input.

Wrapping config

Wrapping performed by the pager happens in addition to any
wrapping that jj itself does.

toml
[ui.streampager]
wrapping = "anywhere"  # wrap at screen edge (default)
wrapping = "word"      # wrap on word boundaries
wrapping = "none"      # strip long lines, allow scrolling
                       # left and right like `less -S`

Auto-exit, clearing the screen on startup or exit

You can configure whether the pager clears the screen on startup or exit, and
whether it quits automatically on short inputs. When the pager auto-quits,
features like word-wrapping are disabled.

toml
[ui.streampager]
# Do not clear screen on exit. Use a full-screen interface for long
# output only. Like `less -FX`.
interface = "quit-if-one-page"  # (default).
# Always use a full-screen interface, ask the terminal to clear the
# screen on exit. Like `less -+FX`.
interface = "full-screen-clear-output"
# Use the alternate screen if the input is either long or takes more
# than 2 seconds to finish. Similar but not identical to `less -F -+X`.
interface = "quit-quickly-or-clear-output"

Showing the ruler on startup

toml
[ui.streampager]
# Start with the ruler showing
show-ruler = true # (default)
# Start with the ruler hidden
show-ruler = false

Processing contents to be paged

If you'd like to pass the output through a formatter e.g.
diff-so-fancy before piping it
through a pager you must do it using a subshell as, unlike git or hg, the
command will be executed directly. For example:

toml
[ui]
pager = ["sh", "-c", "diff-so-fancy | less -RFX"]

Some formatters (like delta) require
git style diffs for formatting. You can configure this style of
diff as the default with the ui.diff-formatter setting. For example:

toml
[ui]
pager = "delta"
diff-formatter = ":git"

Aliases

You can define aliases for commands, including their arguments. For example:

toml
[aliases]
# `jj l` shows commits on the working-copy commit's (anonymous) bookmark
# compared to the `main` bookmark
l = ["log", "-r", "(main..@):: | (main..@)-"]

Alias descriptions

Alias descriptions can be surfaced in shell completions by defining the alias
as a table with .doc and .definition properties. For example:

toml
[aliases]
l = {
    definition = ["log", "-r", "(main..@):: | (main..@)-"],
    doc = "Log pending changes"
}

You can also use the dotted key syntax:

toml
[aliases]
l.definition = ["log", "-r", "(main..@):: | (main..@)-"]
l.doc = "Log pending changes"

This alias syntax can only run a single jj command. However, you may want to
execute multiple jj commands with a single alias, or run arbitrary scripts that
complement your version control workflow. This can be done, but be aware of the
danger:

!!! warning

text
The following technique just provides a convenient syntax for running
arbitrary code on your system. Using it irresponsibly may cause damage
ranging from breaking the behavior of `jj undo` to wiping your file system.
Exercise the same amount of caution while writing these aliases as you would
when typing commands into the terminal!

This feature may be removed or replaced by an embedded scripting language in
the future.

The command jj util exec will simply run any command you pass to it as an
argument. Additional arguments are passed through. Here are some examples:

toml
[aliases]
my-script = ["util", "exec", "--", "my-jj-script"]
#                            ^^^^
# This makes sure that flags are passed to your script instead of parsed by jj.
my-inline-script = ["util", "exec", "--", "bash", "-c", """
set -euo pipefail
echo "Look Ma, everything in one file!"
echo "args: $@"
""", ""]
#    ^^
# This last empty string will become "$0" in bash, so your actual arguments
# are all included in "$@" and start at "$1" as expected.

Note: Shebangs (e.g. #!/usr/bin/env) aren't necessary since you're already
explicitly passing your script into the right shell.

Disabling aliases

Sometimes you may want to disable an alias, such as a built-in alias that you
prefer not to use (for example, jj des<tab> may result in jj desc<cursor>
instead of jj describe <cursor> because there are two possible completions,
desc and describe, with the same prefix) or an alias defined in an earlier
config that you want to disable in a later layer (such as in a repo config or
in a conditional variables scope). To do so, set
aliases.<name>.enabled to false:

toml
[aliases]
desc.enabled = false

best-log = {
  enabled = false,  # WIP; don't enable yet
  doc = "The best `jj log`",
  definition = ["log", "-r", "the_perfect_revset()"]
}

Normal alias overriding rules apply, which means another configuration file may
define the alias again later. Disabling aliases only applies to the aliases
table; template-aliases, revset-aliases, and fileset-aliases cannot be
disabled.

Editor

The default editor is set via ui.editor, though there are several places to
set it. The priority is as follows (environment variables are marked with
a $):

$JJ_EDITOR > ui.editor > $VISUAL > $EDITOR

Nano is the default editor (Notepad on Windows) in the absence of any other
setting, but you could set it explicitly too.

toml
[ui]
editor = "pico"

To use NeoVim instead:

toml
[ui]
editor = ["nvim", "+$line"]

For GUI editors you possibly need to use a -w or --wait. Some examples:

toml
[ui]
editor = "code -w"       # VS Code
editor = "code.cmd -w"   # VS Code on Windows
editor = "bbedit -w"     # BBEdit
editor = "subl -n -w"    # Sublime Text
editor = "mate -w"       # TextMate
editor = ["C:/Program Files/Notepad++/notepad++.exe",
    "-multiInst", "-notabbar", "-nosession", "-noPlugin"] # Notepad++
editor = "idea --temp-project --wait"   #IntelliJ

Obviously, you would only set one line, don't copy them all in!

The following variables will be substituted:

  • $path: File path (optional)
  • $line: 1-based line number for cursor positioning

Editing diffs

The ui.diff-editor setting affects the default tool used for editing diffs
(e.g. jj split, jj squash -i). If it is not set, the special value
:builtin is used. It launches a built-in TUI tool (known as scm-diff-editor)
to edit the diff in your terminal.

You can try a different tool temporarily by doing e.g. jj split --tool meld or
you can set the option to change the default. This requires that you have an
appropriate tool installed, see for example the instructions for using
Meld
.

Suggestion: If possible, it is recommended to try an external diff tool like
Meld (see below for some other possibilities)
for splitting commits and other diff editing, in addition to the built-in diff
editor. It is good to know the capabilities of both. The built-in diff editor
does not require external tools to be available, is faster for tasks like
picking hunks, and does not require leaving the terminal. External tools give
you the flexibility of picking out portions of lines from the diff or even
arbitrarily editing the text of the files.

If ui.diff-editor is a string, e.g. "meld", the arguments will be read from
the following config keys.

toml
[merge-tools.meld]
# program = "meld"      # Defaults to the name of the tool if not specified
program = "/path/to/meld" # May be necessary if `meld` is not in the PATH
edit-args = ["--newtab", "$left", "$right"]

jj makes the following substitutions:

  • $left is a directory containing the original contents before any changes.

  • $right is the directory containing the changed contents.
    Edits are read back from here, so this is where tools should make changes.

  • If no edit-args are specified, ["$left", "$right"] are set by default.

  • If edit-args = [], jj will refuse to use this tool for diff editing. This is a way to explicitly state that a certain tool (e.g. mergiraf) does not work for diff editing.

Like diff viewing, diff editors are invoked with a directory containing the left
and right sides by default. The edit-invocation-mode config controls this
independently of diff-invocation-mode, so you can, for example, keep
directory-based diff viewing but launch the diff editor once per changed file:

toml
[merge-tools.my-tool]
diff-invocation-mode = "dir"
edit-invocation-mode = "file-by-file"

Finally, ui.diff-editor can be a list that specifies a command and its arguments.

Some examples:

toml
[ui]
# Use merge-tools.meld.edit-args
diff-editor = "meld"  # Or `kdiff3`, or `diffedit3`, ...
# Specify edit-args inline
diff-editor = ["/path/to/binary", "--be-helpful", "$left", "$right"]
# Equivalent to ["binary", "$left", "$right"] arguments by default
diff-editor = "binary"

Experimental 3-pane diff editing

We offer two special "3-pane" diff editor configs:

  • meld-3, which requires installing Meld, and
  • diffedit3, which requires installing diffedit3.

Meld is a graphical application that is recommended, but can be difficult to
install in some situations. diffedit3 is designed to be easy to install and to
be usable in environments where Meld is difficult to use (e.g. over SSH via port
forwarding). diffedit3 starts a local server that can be accessed via a web
browser, similarly to Jupyter.

There is also the diffedit3-ssh which is similar to diffedit3 but does not
try to open the web browser pointing to the local server (the URL
printed to the terminal) automatically. diffedit3-ssh also always uses ports in between
17376-17380 and fails if they are all busy. This can be useful when working
over SSH. Open the fold below for more details of how to set that up.

Tips for using diffedit3-ssh over SSH

To use diffedit3 over SSH, you need to set up port forwarding. One way to do
this is to start SSH as follows (copy-paste the relevant lines):

shell
ssh -L 17376:localhost:17376 \
    -L 17377:localhost:17377 \
    -L 17378:localhost:17378 \
    -L 17379:localhost:17379 \
    -L 17380:localhost:17380 \
    myhost.example.com

diffedit3-ssh is set up to use these 5 ports by default. Usually, only the
first of them will be used. The rest are used if another program happens to use
one of them, or if you run multiple instances of diffedit3 at the same time.

Another way is to add a snippet to ~/.ssh/config:

ssh-config
Host myhost
    User     myself
    Hostname myhost.example.com
    LocalForward 17376 localhost:17376
    LocalForward 17377 localhost:17377
    LocalForward 17378 localhost:17378
    LocalForward 17379 localhost:17379
    LocalForward 17380 localhost:17380

With that configuration, you should be able to simply ssh myhost.

Setting either ui.diff-editor = "meld-3" or ui.diff-editor = "diffedit3"
will result in the diff editor showing 3 panes: the diff on the left and right,
and an editing pane in the middle. This allow you to see both sides of the
original diff while editing.

If you use ui.diff-editor = "meld-3", note that you can still get the 2-pane
Meld view using jj diff --tool meld. diffedit3 has a button you can use to
switch to a 2-pane view.

To configure other diff editors in this way, you can include $output together
with $left and $right in merge-tools.TOOL.edit-args. jj will replace
$output with the directory where the diff editor will be expected to put the
result of the user's edits. Initially, the contents of $output will be the
same as the contents of $right.

`JJ-INSTRUCTIONS`

When editing a diff, jj will include a synthetic file called JJ-INSTRUCTIONS
in the diff with instructions on how to edit the diff. Any changes you make to
this file will be ignored. To suppress the creation of this file, set
ui.diff-instructions = false.

Using Meld as a diff editor

Meld is a nice and polished free diff editor. It can be
obtained as follows:

  • Linux: use your favorite package manager, e.g. sudo apt install meld.

  • Windows: Meld can be downloaded from https://meldmerge.org/.

  • Mac OS: Install Homebrew and run brew install --cask meld.
    This will install both an app in /Applications/Meld.app and the command-line
    meld command that jj uses. You can read about more details and other
    options
    but,
    as of this writing, this is by far the easiest.

jj has two diff editing configurations that use Meld: meld for a 2-pane view
and meld-3 for a three-pane view.

There is also a meld merge tool
that can be useful, but does not support displaying the merge base while
merging.

Using Vim as a diff editor

Using ui.diff-editor = "vimdiff" is possible but not recommended. For a better
experience, you can follow instructions from the Wiki to configure the
DirDiff Vim plugin and/or the vimtabdiff Python script.

3-way merge tools for conflict resolution

The ui.merge-editor key specifies the tool used for three-way merge tools
by jj resolve. For example:

toml
[ui]
# Use merge-tools.meld.merge-args
merge-editor = "meld"  # Or "vscode" or "vscodium" or "kdiff3" or "vimdiff"
# Specify merge-args inline
merge-editor = ["meld", "$left", "$base", "$right", "-o", "$output"]

The following built-in tools are always available:

  • :builtin launches a TUI tool for 3-way merges (the default).
  • :editor opens the conflicted file with conflict markers in your text editor
    (ui.editor). Unlike the other tools, it supports conflicts with any number
    of sides. Conflict markers left in the file are kept as a partially resolved
    conflict.
  • :ours and :theirs pick side #1 or side #2 of the conflict respectively.

The following tools can be used out of the box, as long as they are installed:

  • "kdiff3"
  • "meld"
  • "mergiraf"
  • "smerge"
  • "vimdiff"
  • "vscode"
  • "vscodium"

Using VS Code as a merge tool works well with VS Code's Remote
Development

functionality, as long as jj is called from VS Code's terminal.

Setting up a custom merge tool

To use a different tool named TOOL, the arguments to pass to the tool MUST be
specified either inline or in the merge-tools.TOOL.merge-args key. As an
example of how to set this key and other tool configuration options, here is
the out-of-the-box configuration of the three default tools. (There is no need
to copy it to your config file verbatim, but you are welcome to customize it.)

toml
[merge-tools.kdiff3]
# program  = "kdiff3"     # Defaults to the name of the tool if not specified
merge-args = ["$base", "$left", "$right", "-o", "$output", "--auto"]
[merge-tools.meld]
merge-args = ["$left", "$base", "$right", "-o", "$output", "--auto-merge"]

[merge-tools.vimdiff]
merge-args = ["-f", "-d", "$output", "-M",
    "$left", "$base", "$right",
    "-c", "wincmd J", "-c", "set modifiable",
    "-c", "set write"]
program = "vim"
merge-tool-edits-conflict-markers = true    # See below for an explanation

jj makes the following substitutions:

  • $output (REQUIRED) is replaced with the name of the file that the merge tool
    should output. jj will read this file after the merge tool exits.

  • $left and $right are replaced with the paths to two files containing the
    content of each side of the conflict.

  • $base is replaced with the path to a file containing the contents of the
    conflicted file in the last common ancestor of the two sides of the conflict.

  • $marker_length is replaced with the length of the conflict markers which
    should be used for the file. This can be useful if the merge tool parses
    and/or generates conflict markers. Usually, jj uses conflict markers of
    length 7, but they can be longer if necessary to make parsing unambiguous.

  • $path is replaced with the path in the repository at which the file
    will be eventually stored. It is relative to the root directory of the
    repository and uses / as separators.

Unlike diff-args or edit-args, there is no default value for merge-args.
If merge-args are not specified, the tool cannot be used for conflict
resolution.

Editing conflict markers with a tool or a text editor

By default, the merge tool starts with an empty output file. If the tool puts
anything into the output file and exits with the 0 exit code,
jj assumes that the conflict is fully resolved, while if the tool exits with
a non-zero exit code, jj assumes that the merge should be canceled.
This is appropriate for most graphical merge tools.

For merge tools which try to automatically resolve conflicts without user input,
this behavior may not be desired. For instance, some automatic merge tools use
an exit code of 1 to indicate that some conflicts were unable to be resolved and
that the output file should contain conflict markers. In that case, you could
set the config option merge-tools.TOOL.merge-conflict-exit-codes = [1] to tell
jj to expect conflict markers in the output file if the exit code is 1. If a
merge tool produces output using Git's "diff3" conflict style, jj should be
able to parse it correctly, so many Git merge drivers should be usable with jj
as well.

Some tools (e.g. vimdiff) can present a multi-way diff but don't resolve
conflict themselves. When using such tools, jj
can help you by populating the output file with conflict markers before starting
the merge tool (instead of leaving the output file empty and letting the merge
tool fill it in). To do that, set the
merge-tools.vimdiff.merge-tool-edits-conflict-markers = true option.

With this option set, if the output file still contains conflict markers after
the conflict is done, jj assumes that the conflict was only partially resolved
and parses the conflict markers to get the new state of the conflict. The
conflict is considered fully resolved when there are no conflict markers left.
The conflict marker style can also be customized per tool using the
merge-tools.TOOL.conflict-marker-style option, which takes the same values as
ui.conflict-marker-style.

Code formatting and other file content transformations

The jj fix command allows you to efficiently rewrite files in complex commit
graphs with no risk of introducing conflicts, using tools like clang-format or
prettier. The tools run as subprocesses that take a file content on standard
input and repeat it, with any desired changes, on standard output.

This enables the command to format many versions of the same file concurrently
and without touching on the working copy. The local files are not and must not
be used.

The file is only rewritten if the subprocess produces a successful exit code.

Configuration

Tools are defined in a table where the keys are arbitrary identifiers and the
values have the following properties:

  • command: The arguments used to run the tool. The first argument is the path
    to an executable file. Arguments can contain these variables that will be
    replaced:

    • $root will be replaced with the workspace root path (the directory
      containing the .jj directory).
    • $path will be replaced with the repo-relative path of the file being
      fixed. It is useful to provide the path to tools that include the path in
      error messages, or behave differently based on the directory or file name.
  • patterns: List of filesets (see: jj help -k filesets), determining which
    files the tool will affect based on their path. If this list is empty, no
    files will be affected by the tool. If there are multiple patterns, the tool
    is applied only once to each file in the union of the patterns.

  • enabled: Enables or disables the tool. If omitted, the tool is enabled. This
    is useful for defining disabled tools in user configuration that can be
    enabled in individual repositories with one config setting.

  • line-range-args: Optional array of template strings specifying how to pass a
    line range to the tool. It may include the variables $first and $last,
    which will be replaced with the 1-based line numbers of the first and last
    lines inside the modified range, respectively. A range containing a single
    line will have equal values for $first and $last. Empty ranges
    representing deletions will be skipped because they cannot be represented this
    way. If the tool does not support formatting specific line ranges, this
    setting should be omitted, and the tool will always process the entire file.

    • Example: line-range-args = ["--lines=$first:$last"]
    • Example: line-range-args = ["--line-start=$first", "--line-end=$last"]
  • run-tool-if-zero-line-ranges: Whether to run the tool invocation for this
    tool on files that had zero line ranges to format in the revision being fixed.
    Defaults to false, meaning the tool is skipped for these files. This default
    behavior serves two main purposes:

    • Avoiding unnecessary executions of tools when no line ranges are modified.
    • Preventing tools configured with line-range-args from making overly broad
      changes (formatting the whole file) when no line ranges are available.

    Setting this to true is useful if the tool should run regardless of diffs
    (e.g., to sort imports, or run with --include-unchanged-files).

jj fix provides the file content anonymously on standard input, but the name
of the file being formatted may be important for include sorting or other output
like error messages. To address this, you can use the $root and $path in
your arguments.

Enforce coding style rules

Suppose you want to use clang-format to format your *.c and *.h files,
as well as sorting their #include directives.

toml
[fix.tools.clang-format]
command = ["/usr/bin/clang-format", "--sort-includes", "--assume-filename=$path"]
patterns = ["glob:'**/*.c'",
            "glob:'**/*.h'"]

Sort and remove duplicate lines from a file

jj fix can also be used with tools that are not considered code formatters.

Suppose you have a list of words in a text file in your repository, and you want
to keep the file sorted alphabetically and remove any duplicate words.

toml
[fix.tools.sort-word-list]
command = ["sort", "-u"]
patterns = ["word_list.txt"]

Enforce code formatting on modified lines

You can configure jj fix to run a formatter only on the modified lines of a
file, rather than formatting the entire file. This is particularly useful for
avoiding unrelated formatting changes in untouched parts of a file.

To enable this, specify the line-range-args configuration to match the line
range argument format expected by your tool. The formatter will be invoked with
these arguments repeated for each modified line range (e.g. --lines=10-20 --lines=30-40).

Additionally, you can use run-tool-if-zero-line-ranges to control the
behavior when a diff results in zero line ranges (which can happen when lines
are only deleted or with --include-unchanged-files). If false (the default),
jj will skip the tool entirely for that file. If true, jj will run the tool
even when there are zero line ranges calculated. This is useful for tools that
perform file-wide operations, such as import sorting.

Note that you can use the --all-lines CLI flag for jj fix to ignore these
line ranges and format the entire modified file instead.

Example of clang-format with line ranges:

toml
[fix.tools.clang-format]
command = ["/usr/bin/clang-format", "--assume-filename=$path"]
patterns = ["glob:'**/*.cc'",
            "glob:'**/*.h'"]
enabled = true
line-range-args = ["--lines=$first:$last"]
run-tool-if-zero-line-ranges = false

Tools stored inside the workspace

Some fix tools may be stored inside the workspace. For example, a binary may be
stored inside node_modules. Use the $root variable to create an absolute
path to such a program:

toml
[fix.tools.biome]
command = ["$root/node_modules/@biomejs/biome/bin/biome", "format", "--stdin-file-path=$path"]
patterns = ["glob:'**/*.ts'", "glob:'**/*.tsx'"]

Execution order of tools

If two or more tools affect the same file, they are executed in the ascending
lexicographical order of their configured names. Any tool, but the first one to
be executed, get as input the content of the file produced by the previous
tool. This will remain as a tie breaker if other ordering mechanisms are
introduced in the future.

If you use numbers in tool names to control execution order, remember to
include enough leading zeros so that, for example, 09 sorts before 10.

Suppose you want to keep only the 10 smallest numbers in a text file that
contains one number on each line. This can be accomplished with sort and
head, but execution order is important.

toml
[fix.tools.1-sort-numbers-file]
command = ["sort", "-n"]
patterns = ["numbers.txt"]

[fix.tools.2-truncate-numbers-file]
command = ["head", "-n", "10"]
patterns = ["numbers.txt"]

Disabling and enabling tools

Tools can be disabled and enabled with the optional enabled config. This
allows you to define tools globally but enable them only for specific
repositories.

In the user configuration, define a disabled tool for running rustfmt:

toml
[fix.tools.rustfmt]
enabled = false
command = ["rustfmt", "--emit", "stdout"]
patterns = ["glob:'**/*.rs'"]

Then to use the tool in a specific repository, set the enabled config:

shell
$ jj config set --repo fix.tools.rustfmt.enabled true

`run`: Running commands across revisions {: #run }

The jj run command executes a command against each revision in a set,
checking out each one into an isolated working copy, running the command, and
amending the revision with any resulting changes.

`run.jobs`: Default parallelism {: #run.jobs }

By default jj run processes one revision at a time. You can increase
parallelism with the run.jobs setting:

toml
[run]
jobs = 8

The value must be a positive integer. The --jobs / -j CLI flag overrides
this setting for a single invocation:

shell
jj run -j 4 -- cargo fmt

Commit Signing

jj can be configured to sign and verify the commits it creates using either
GnuPG or SSH signing keys.

To do this you need to configure a signing backend.

Setting the backend to "none" disables signing.

GnuPG Signing

toml
[signing]
behavior = "own"
backend = "gpg"
## You can set `key` to anything accepted by `gpg -u`
## If not set then defaults to the key associated with `user.email`
# key = "4ED556E9729E000F"
# key = "[email protected]"

By default the gpg backend will look for a gpg binary on your path. If you want
to change the program used or specify a path to gpg explicitly you can set:

toml
[signing]
backends.gpg.program = "gpg2"

Also by default the gpg backend will consider key expiry when verifying commit signatures.
To consider expired keys as valid you can set:

toml
[signing]
backends.gpg.allow-expired-keys = true

PKCS#12 Certificates

PKCS#12 certificates can be used to sign commits using the gpgsm backend.

toml
[signing]
behavior = "own"
backend = "gpgsm"
## You can set `key` to anything accepted by `gpgsm -u`
## If not set then defaults to the key associated with `user.email`
# key = "4ED556E9729E000F"
# key = "[email protected]"

By default the gpgsm backend will look for a gpgsm binary on your path. If you want
to change the program used or specify a path to gpgsm explicitly you can set:

toml
[signing]
backends.gpgsm.program = "gpgsm"

Also by default the gpgsm backend will consider key expiry when verifying commit signatures.
To consider expired keys as valid you can set:

toml
[signing]
backends.gpgsm.allow-expired-keys = true

SSH Signing

toml
[signing]
behavior = "own"
backend = "ssh"
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGj+J6N6SO+4P8dOZqfR1oiay2yxhhHnagH52avUqw5h"
## You can also use a path instead of embedding the key
# key = "~/.ssh/id_for_signing.pub"

By default the ssh backend will look for a ssh-keygen binary on your path. If you want
to change the program used or specify a path to ssh-keygen explicitly you can set:

toml
[signing]
backends.ssh.program = "/path/to/ssh-keygen"

When verifying commit signatures the ssh backend needs to be provided with an allowed-signers
file containing the public keys of authors whose signatures you want to be able to verify.

You can find the format for this file in the
ssh-keygen man page. This can be provided
as follows:

toml
[signing]
backends.ssh.allowed-signers = "/path/to/allowed-signers"

Additionally, an SSH KRL or list of revoked public keys (see
ssh-keygen man page) can be provided
with the revocation-list option. If a public key is found in this file then any signature
relating to it is marked as invalid.

toml
[signing]
backends.ssh.revocation-list = "/path/to/revocation-list"

Manually signing commits

You can use jj sign/jj unsign
to sign/unsign commits manually.

!!! warning

text
`jj sign` always signs commits, even if they are already signed by the
user. While this is cumbersome for users signing via hardware devices, we
cannot reliably check if a commit is already signed without creating a
signature (see [this issue](https://github.com/jj-vcs/jj/issues/5786)).

Automatically signing commits

The signing.behavior configuration option has four different options for what
to do with signing commits on modification of a change (e.g., rebasing or edits).

  • drop: do not automatically sign; if a change was signed before
    modification, drop that signing after modification.
  • keep: if a change was signed before modification, and it was authored by
    you, attempt to sign it again after the modification.
  • own: sign all commits that were authored by you when you modify them.
  • force: sign all commits after modification, always, even if you are not the
    author.

To sign commits only when pushing, set signing.behavior = "drop" and
git.sign-on-push = true:

toml
# Configure signing backend as before, but lazily signing only on push.
[signing]
behavior = "drop"
backend = "ssh"
key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGj+J6N6SO+4P8dOZqfR1oiay2yxhhHnagH52avUqw5h"

[git]
sign-on-push = true

With this setup, commits are not signed when they are created or rewritten. At
push time, git.sign-on-push only signs commits that are not yet signed (that
are mutable and authored by you, behaving like own even though write-time
signing uses drop).

This is useful when the signing backend requires user interaction or is slow,
because the signing work is deferred to push time. If signing.behavior is set
to own or force, commits may already be signed when written, and
git.sign-on-push would have little or nothing left to sign.

The commits are signed in their own transaction which is committed before
the push starts. If the push fails, the commits stay signed and retrying
does not have to sign them again. If nothing needs signing, no extra
transaction is created.

Because signing and pushing are separate operations, jj undo of the push
operation would not undo the commit signatures.

Commit Signature Verification

By default signature verification and display is disabled as it incurs a
performance cost when rendering medium to large change logs. You can enable it
by setting ui.show-cryptographic-signatures to true in your configuration.

If you want to display commit signatures in your templates, you can use
commit.signature() (see Commit type). The
returned CryptographicSignature
Type
provides methods to retrieve
signature details.

Git settings

Default colocation

When creating a Git-backed Jujutsu repository, they use "colocated" mode by
default, which places the .git directory next to the .jj directory.
Colocation allows some amount of two-way interoperability, but it can perform
worse in large repos.

The setting git.colocate is a boolean option that controls whether or not the
jj git init and jj git clone commands should create colocated workspace by
default. Set git.colocate to false to disable it.

See Colocated Jujutsu/Git workspaces
for more information.

Default object hash format

Traditionally, Git used the SHA-1 hash function to compute the identifiers for
objects. Because SHA-1 is not considered cryptographically secure anymore, Git
is in the process of transitioning to a stronger hash function,
namely SHA-256.

Currently, the object hash function can only be set when initializing a new Git
repository. The setting git.object-hash controls the default choice for that
purpose. It can take the values sha1 (default) and sha256.

Note that at the moment there is no interopability between the formats, and not
all code forges support SHA-256 repositories yet.

Default remotes for `jj git fetch` and `jj git push`

By default, if a single remote exists it is used for jj git fetch and jj git push; however if multiple remotes exist, the default remote is assumed to be
named "origin", just like in Git. Sometimes this is undesirable, e.g. when you
want to fetch from a different remote than you push to, such as a GitHub fork.

To change this behavior, you can modify the repository
configuration
variable git.fetch, which can be a
single remote, or a list of remotes to fetch from multiple places:

sh
jj config set --repo git.fetch "upstream"
jj config set --repo git.fetch '["origin", "upstream"]'

By default, the specified pattern matches remote names with glob syntax. You can
also use other string pattern syntax:

sh
jj config set --repo git.fetch "regex:'^(remote|upstream)'"
jj config set --repo git.fetch '["remote*", "upstream*"]'

Similarly, you can also set the variable git.push to cause jj git push to
push to a different remote:

sh
jj config set --repo git.push "github"
jj config set --repo git.push "regex:'^(remote|upstream)'"
jj config set --repo git.push '["remote*", "upstream*"]'

Default bookmarks and tags to fetch

You can configure which bookmarks and tags to fetch by default per remote, using
the remotes.<name>.fetch-bookmarks/fetch-tags config. The value is a string
pattern
that matches the names of the bookmarks
and tags to fetch. If remotes.<name>.fetch-bookmarks is not configured, the
default fetch refspecs for the remotes are read from the Git configuration.

The glob pattern supports only * (other wildcard characters like ? are not
supported). You can combine patterns with logical operators to specify multiple
bookmarks and tags, but only union and negative intersection are supported.

toml
[remotes.origin]
fetch-bookmarks = "~gh-pages"
fetch-tags = "v*"

Automatic tracking of bookmarks

You can configure which bookmarks to track automatically per remote, using the
remotes.<name>.auto-track-bookmarks config. The value is a
string pattern that matches the names of the
bookmarks to track. This setting is applied to all new bookmarks, including ones
created locally and ones fetched from the remote. For example:

toml
[remotes.origin]
auto-track-bookmarks = "*"

This will simply track all bookmarks for the remote "origin". There are various
reasons to restrict which bookmarks to track:

  • When collaborating with other people via the same remote, you may not want to
    track all the bookmarks of your collaborators. Similarly, you may not want to
    push some of your bookmarks to the remote at all; these may only be intended
    for local use. Many people use a "personal prefix" in their bookmark names.
    This marks a bookmark as belonging to one person, making it possible to track
    and push only bookmarks with that prefix. For example, Alice may set her
    configuration like so:

    toml
    [remotes.origin]
    auto-track-bookmarks = "alice/*"

    That way, bookmarks pushed by other people (who probably use a different
    prefix or none at all) are not tracked automatically. At the same time, Alice
    can create bookmarks without the prefix for local-only use. You can also
    configure Jujutsu to use your prefix for generated bookmark names; see the
    section
    "Generated bookmark names on push".

  • Another reason to restrict the bookmarks to track may be that you're not
    collaborating with people using the same remote, but a different one. For
    example, if you make a fork on GitHub, your fork will usually be called
    "origin", while the repository you forked from is usually called "upstream".
    In that case, you may want to track all bookmarks from your fork, but only the
    "main" bookmark from upstream. Here's an example how to achieve that:

    toml
    [remotes.origin]
    auto-track-bookmarks = "*"
    [remotes.upstream]
    auto-track-bookmarks = "main"
  • If you're collaborating with people on the same remote, but your bookmark
    names don't follow a pattern that allows a bookmark's "owner" to be identified
    (e.g. alice/*), you can use auto-track-created-bookmarks instead:

    toml
    [remotes.origin]
    auto-track-created-bookmarks = "*"

    This pattern will only apply to bookmarks you create with jj bookmark create
    and jj bookmark set. The disadvantage of this option is the fact that you
    will have to track your own bookmarks manually if you fetch them from the
    remote. This may happen regularly if you use multiple computers to work on the
    same project.

  • Lastly, you may be working on various projects with different conventions for
    bookmark names. In that case, it can be handy to apply different configuration
    to different (groups of) repositories. Read about how to do that in the
    section "Conditional variables".

Default revisions to push

You can configure the default target revisions to push. By default, any tracking
bookmarks and tags pointing to these revisions are pushed.

The remote symbol expands to the name of the remote (e.g. "origin").

toml
[revsets]
git-push = 'remote_bookmarks(remote=exact:remote)..@'

Automatic local bookmark creation on `jj git clone`

When cloning a new Git repository, jj by default creates a local bookmark
tracking the default remote bookmark (such as main for main@origin.) If you
aren't going to update the main bookmark locally, the tracking bookmark isn't
necessary.

This behavior can be disabled by

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

Abandon commits that became unreachable in Git

By default, when jj imports refs from Git, it will look for commits that used
to be reachable but no longer are reachable. Those commits will
then be abandoned, and any descendant commits will be rebased off of them (as
usual when commits are abandoned). You can disable this behavior and instead
leave the Git-unreachable commits in your repo by setting:

toml
[git]
abandon-unreachable-commits = false

Replicating evolution history for fetched or imported Git commits

By default, when jj imports commits from Git, it attempts to reconstruct their
evolution history using change IDs. If you are importing a large number of
commits, you can temporarily disable this behavior to improve performance:

toml
[git]
record-synthetic-predecessors = false

Generated bookmark names on push

jj git push --change generates bookmark names with a prefix of "push-" by
default. You can pick a different prefix and formatting by setting the
templates.git_push_bookmark template. For example:

toml
[templates]
git_push_bookmark = '"martinvonz/push-" ++ change_id.short()'

This template should include expressions like change_id to generate unique and
stable bookmark.

Set of private commits

You can configure the set of private commits by setting git.private-commits to
a revset. The value is a revset of commits that Jujutsu will refuse to push. If
unset, all commits are eligible to be pushed.

toml
[git]
# Prevent pushing work in progress or anything explicitly labeled "private"
private-commits = "description('wip:*') | description('private:*')"

If a commit is in git.private-commits but is already on the remote, then it is
not considered a private commit. Commits that are immutable are also excluded
from the private set.

Private commits prevent their descendants from being pushed, since doing so
would require pushing the private commit as well.

Git subprocessing behavior

Git remote interactions are handled by spawning a git subprocess.
If git is not on your OS path, or you want to specify a particular binary,
you can:

toml
[git]
executable-path = "/path/to/git"

Gerrit settings

Default remote

To configure the default remote that jj gerrit upload will push to, set
gerrit.default-remote. This can be overridden with the --remote option.

toml
[gerrit]
default-remote = "gerrit"

Default remote branch

To configure the default branch that jj gerrit upload will push to, set
gerrit.default-remote-branch. This can be overridden with the
--remote-branch option.

toml
[gerrit]
default-remote-branch = "main"

Since version 3.3.1 Gerrit supports an alternative to the Change-Id trailer,
using a Link trailer in the format of
<review-url>/id/I<change-id>. To have jj gerrit upload automatically add a
Link trailer instead of a Change-Id trailer, set gerrit.review-url to the
URL preview you'd like to use.

toml
[gerrit]
review-url = "https://review.gerrithub.io/your/project"

Note that if a Change-Id or Link trailer already exists in a change, then
jj gerrit upload will not add any trailer.

Merge settings

Granularity of hunks

jj by default resolves content conflicts by splitting text into line-level
hunks and merge them. This can be configured to split hunks further into
word-level hunks.

  • line: split into line hunks (default)
  • word: split into word hunks
toml
[merge]
hunk-level = "line"

Resolution of same-change conflicts

jj by default resolves conflicts if all sides made the same change. This
matches what Git and Mercurial do (in the 3-way case at least), but not what
Darcs does. See the technical docs on the
same-change rule for background and
trade-offs. It also means that repeated 3-way merging of multiple trees may give
different results depending on the order of merging. To turn it off, set
same-change = "keep".

  • keep: leave same-change conflict unresolved
  • accept: resolve same-change conflict to new value (default)
toml
[merge]
same-change = "accept"

Converge settings

The jj converge command attempts to resolve divergence by replacing two or
more divergent commits with a single commit. The command accepts a
--revisions REVSET argument, and looks for divergence within the commits that
match that revset. The --revisions argument is optional. By default
jj converge uses the revsets.converge revset:

toml
[revsets]
converge = "mutable() & divergent()"

Filesystem monitor

In large repositories, it may be beneficial to use a "filesystem monitor" to
track changes to the working copy. This allows jj to take working copy
snapshots without having to rescan the entire working copy.

This is governed by the fsmonitor.backend option. Currently, the valid values
are "none" or "watchman".

Watchman

To configure the Watchman filesystem monitor, set
fsmonitor.backend = "watchman". Ensure that you have installed the Watchman
executable on your system
.

You can configure jj to use watchman triggers to automatically create
snapshots on filesystem changes by setting
fsmonitor.watchman.register-snapshot-trigger = true.

A full configuration for watchman can look like this:

toml
[fsmonitor]
backend = "watchman"

[fsmonitor.watchman]
register-snapshot-trigger = true

You can check whether Watchman is enabled and whether it is installed correctly
using jj debug watchman status.

Note: watchman heavily uses inotify and sets up a user watch per-file. On
large repositories, this may cause watchman to fail and commands like
jj status to take longer than expected. If you experience this run
jj debug watchman status and tune your inotify limits.

Snapshot settings

Paths to automatically track

All new files in the working copy that don't match the ignore patterns are
tracked by default. You can set the snapshot.auto-track to set which paths
get automatically tracked when they're added to the working copy. See the
fileset documentation for the syntax. Files with paths matching
ignore files are never tracked automatically.

If you set snapshot.auto-track to a non-default value, untracked files can be
tracked with jj file track.

You can use jj file untrack to untrack a file while keeping it in the working
copy. However, first ignore them or remove them
from the snapshot.auto-track patterns; otherwise they will be immediately
tracked again.

Maximum size for new files

By default, as an anti-footgun measure, jj will refuse to add new files to the
snapshot that are larger than a certain size; the default is 1MiB. This can be
changed by setting snapshot.max-new-file-size to a different value. For
example:

toml
[snapshot]
max-new-file-size = "10MiB"
# the following is equivalent
max-new-file-size = 10485760

The value can be specified using a human readable string with typical suffixes;
B, MiB, GB, etc. By default, if no suffix is provided, or the value is a
raw integer literal, the value is interpreted as if it were specified in bytes.

Files that already exist in the working copy are not subject to this limit.

Setting this value to zero will disable the limit entirely.

Automatic update of stale working copies

When a working copy becomes stale (meaning the working copy's recorded commit is
no longer the current commit for that workspace), jj will normally prompt you
to update it with jj workspace update-stale. You can configure jj to
automatically update stale working copies by setting:

toml
[snapshot]
auto-update-stale = true

This is particularly useful if you have multiple workspaces
and want to avoid manually updating each one.

For more information on stale working copies, see the working copy
documentation
.

Working copy settings

EOL conversion setting

This setting serves the same purpose as the core.autocrlf Git
config.

Regardless of this setting, the line endings conversion is skipped on binary
files based on a heuristic. This is similar to Git.

toml
[working-copy]
# No EOL conversion. Similar to `core.autocrlf = false`.
eol-conversion = "none"
# Apply CRLF-to-LF EOL conversion when we check files in to the backend store
# from the local file system, but do not apply LF-to-CRLF EOL conversion when we
# check out the code from the backend store to the local file system. Similar to
# `core.autocrlf = input`.
eol-conversion = "input"
# Apply EOL conversion on both file check-in to and file check-out from the
# backend store, so you have CRLF line endings in your working directory while
# the repository has LF line endings. Similar to `core.autocrlf = true`.
eol-conversion = "input-output"

Binary file detection

To detect if a file is binary, Jujutsu currently checks if there is a NULL byte
in the file, which is different from the algorithm of
gitoxide or Git. Jujutsu doesn't plan
to align the binary detection logic with Git.

Respect or ignore executable bit permission changes

Whether to respect or ignore changes to the executable bit for files on Unix.
This option is unused on Windows.

toml
[working-copy]
exec-bit-change = "respect" | "ignore" | "auto" (default)

On Unix systems, files have a permission bit for whether they are executable as
scripts or binary code. Jujutsu stores this state in the repository and will
update it for files as you operate on a repository. If you set your working
copy to a commit where a file is recorded as executable or not, jj will
adjust the permission of that file. If you change a file's executable bit
through the filesystem, jj will record that change when taking a snapshot.

Setting this to "ignore" will make Jujutsu ignore the executable bit on the
filesystem when updating the state for the repository. In addition, jj will
not attempt to modify a file's executable bit and will add new files as
"not executable." This is already the behavior on Windows, and having the
option to enable this behavior is useful for Unix users dual-booting Windows,
Windows users accessing files from WSL, or anyone experimenting with other
filesystem configurations.

On Unix if "auto" is set (the default), jj will try to detect whether the
filesystem supports changing executable bits to choose between "respect" or
"ignore". On errors, we assume "respect", except if permission was denied
which will assume "ignore".

On Windows, files have no executable bit so this option is unused.

You can always use jj file chmod to update the recorded executable bit for a
file manually. If this option is "respect", jj will also attempt to
propagate that change to the filesystem.

Note that if you modify a file's executable bit before changing this setting
from "ignore" or "auto" to "respect", jj may not update the stored
executable bit until you modify the file's contents or update its modification
time, e.g. with touch.

Ways to specify `jj` config: details

User config files

An easy way to find the user config file/directory is:

bash
jj config path --user

On all platforms, the user's global jj configurations are by default loaded in
the following precedence order (with later configs overriding earlier ones):

  • $HOME/.jjconfig.toml
  • /jj/config.toml (preferred)
  • /jj/conf.d/*.toml

where $HOME represents the user's home directory (%USERPROFILE% on Windows),
and `` represents the platform-specific configuration
directory shown in the table below. The platform-specific location is
recommended for better integration with platform services.

The files in the conf.d directory are loaded in lexicographic order. This
allows configs to be split across multiple files and combines well with
Conditional Variables. Modifying user configuration
with jj config {edit,set,unset} --user targets the primary user config file
(or the first loaded file in conf.d/). Individual files in conf.d/ can be
targeted with --file .

Platform Location of `` dir Example config file location
Linux and macOS $XDG_CONFIG_HOME or $HOME/.config /home/alice/.config/jj/config.toml
Windows {FOLDERID_RoamingAppData} C:\Users\Alice\AppData\Roaming\jj\config.toml

The location of the jj user config files/directories can also be overridden
with the JJ_CONFIG environment variable. If the environment variable is set
(even to an empty string), it will be used instead of any configuration files in
the default locations. It can be a path to a TOML file or a directory of TOML
files, which will be loaded in lexicographic order and merged. Multiple paths
can be specified by separating them with a platform-specific path separator (:
on Unix-like systems, ; on Windows). Note that this variable only affects
user-level and system-level config files; repo and workspace configs are
unaffected and will continue to be loaded from their respective locations.
(There is no environment variable to disable repo or workspace configs.)

For example, the following could be used to run jj without loading any user or
system configs:

bash
# Ignores any settings specified in `{PLATFORM,/etc}/jj/{config.toml,conf.d/*.toml}`,
# but still loads repo and workspace configs.
JJ_CONFIG= jj log

There are also the --config-file and --config <NAME=VALUE>
global options which work with any
jj command.

System config files

On Unix-like platforms, system-wide jj configurations are by default loaded in
the following precedence order (with later configs overriding earlier ones).

  • /etc/jj/config.toml
  • /etc/jj/conf.d/*.toml

These configs can be overridden by the user config files, and will be disabled
in favor of the JJ_CONFIG environment variable if it is set.

Existing system config files loaded by jj can be edited using
jj config {edit,set,unset} --file .

JSON Schema Support

Many popular editors support TOML file syntax highlighting and validation. To
enable schema validation in your editor, add this line at the top of your TOML
config files:

toml
#:schema https://docs.jj-vcs.dev/latest/config-schema.json

This enables features like:

  • Autocomplete for config keys
  • Type checking of values
  • Documentation on hover
  • Validation of settings

Here are some popular editors with TOML schema validation support:

Environment variable overrides

Besides $JJ_CONFIG (which controls where user config files are loaded
from
), these environment variables override individual
config keys. They take precedence over config files and are themselves
overridden by --config / --config-file.

Variable Config key
$JJ_USER user.name
$JJ_EMAIL user.email
$JJ_EDITOR ui.editor
$JJ_PAGER ui.pager
$JJ_OP_HOSTNAME operation.hostname
$JJ_OP_USERNAME operation.username

A few other variables act as fallbacks when no matching config is set (for
example $VISUAL / $EDITOR for the editor, and $NO_COLOR for color), rather
than hard overrides of configured values. See Editor and
Colorizing output.

Specifying config on the command-line

You can use one or more --config/--config-file options on the command line
to specify additional configuration settings. This overrides settings defined in
config files or environment variables. For example,

shell
# Must not have spaces around the `=`
jj --config ui.color=always --config ui.diff-editor=meld split

The config value should be specified as a TOML expression. If it is a string
value and isn't enclosed by any TOML constructs (such as array notation), quotes
can be omitted. Here is an example with more advanced TOML constructs:

shell
# Single quotes and the '\' are interpreted by the shell (assuming a POSIX shell)
# Double quotes are passed to jj and are parsed as TOML syntax
jj log --config \
  'template-aliases."format_timestamp(timestamp)"="""timestamp.format("%Y-%m-%d %H:%M %:::z")"""'

To load an entire TOML document, use --config-file:

shell
jj --config-file=extra-config.toml log

Conditional variables

You can conditionally enable config variables by using --when.

Using `[[--scope]]` tables

Variables defined in [[--scope]] tables are expanded to the root table.
--when specifies the condition to enable the scope table.

If no conditions are specified, the table is always enabled. If multiple
conditions are specified, their intersection is used.

toml
[user]
name = "YOUR NAME"
email = "[email protected]"

# override user.email if the repository is located under ~/oss
[[--scope]]
--when.repositories = ["~/oss"]
[--scope.user]
email = "[email protected]"

# override ui.pager on specific machines
[[--scope]]
--when.hostnames = ["work-laptop", "work-desktop"]
[--scope.ui]
pager = "delta"

# disable pagination for `jj status`, use `delta` for `jj diff` and `jj show`
[[--scope]]
--when.commands = ["status"]
[--scope.ui]
paginate = "never"
[[--scope]]
--when.commands = ["diff", "show"]
[--scope.ui]
pager = "delta"

Using multiple files

--when can also be used on the top level of a TOML file, which is convenient
when splitting your config across multiple files.
The behavior of conditions are the same as when using [[--scope]] tables.

toml
# In $XDG_CONFIG_HOME/jj/config.toml
[user]
name = "YOUR NAME"
email = "[email protected]"
toml
# In $XDG_CONFIG_HOME/jj/conf.d/work.toml
--when.repositories = ["~/the/work/repo"]

[user]
email = "[email protected]"

[revset-aliases]
work = "heads(::@ ~ description(''))::"

[aliases]
wip = ["log", "-r", "work"]

You can modify specific configuration files such as those in
conf.d/ directly using the --file option (the file will be created if it
doesn't already exist):

shell
jj config set --file ~/.config/jj/conf.d/work.toml user.email "[email protected]"
jj config edit --file ~/.config/jj/conf.d/work.toml

Available condition keys

  • --when.repositories: List of paths to match the repository path prefix.

    Paths should be absolute. Each path component (directory or file name, drive
    letter, etc.) is compared case-sensitively on all platforms. A path starting
    with ~ is expanded to the home directory. On Windows, directory separator may
    be either \ or /. (Beware that \ needs escape in double-quoted strings.)

    Use jj root to see the workspace root directory. Note that the repository path
    is in the main workspace if you're using multiple workspaces with jj workspace.

  • --when.workspaces: List of paths to match the workspace path prefix.

    The same concerns about the path as for --when.repositories applies.

    Use jj root to see the workspace root directory.

  • --when.hostnames: List of hostnames to match against the operation.hostname
    config setting.

    Hostnames are compared case-sensitively and must match exactly.

    toml
    --when.hostnames = ["work-laptop"]               # matches only "work-laptop"
    --when.hostnames = ["home-desktop", "laptop"]    # matches "home-desktop" *OR* "laptop"
  • --when.commands: List of subcommands to match.

    Subcommands are space-separated and matched by prefix.

    toml
    --when.commands = ["file"]        # matches `jj file show`, `jj file list`, etc
    --when.commands = ["file show"]   # matches `jj file show` but *NOT* `jj file list`
    --when.commands = ["file", "log"] # matches `jj file` *OR* `jj log` (*OR* subcommand of either)
  • --when.platforms: List of platforms to match.

    The values are defined by both
    std::env::consts::FAMILY
    and
    std::env::consts::OS.

    toml
    --when.platforms = ["windows"]            # matches only Windows
    --when.platforms = ["linux", "freebsd"]   # matches Linux *OR* FreeBSD, but *NOT* macOS
    --when.platforms = ["unix"]               # matches anything in the Unix family (Linux, FreeBSD, macOS, etc.)
  • --when.environments: List of environment variable conditions in
    "NAME=VALUE" format (check if the variable has the value), or NAME to
    check if the variable is set.

    toml
    --when.environments = ["CI=true"]                     # matches when CI is set to "true"
    --when.environments = ["CI"]                          # matches when CI is set to anything
    --when.environments = ["CI=true", "CI=1"]             # matches when CI is "true" *OR* "1"
    --when.environments = ["CI=true", "GITHUB_ACTIONS=1"] # matches when *EITHER* condition holds

Conflicts (`docs/conflicts.md`)

First-class conflicts

Introduction

Conflicts happen when Jujutsu can't figure out how to merge different changes
made to the same file. For instance, this can happen if two people are working
on the same file and make different changes to the same part of the file, and
then their commits are merged together with jj new (or one is rebased onto the
other with jj rebase).

Unlike most other VCSs, Jujutsu can record conflicted states in commits. For
example, if you rebase a commit and it results in a conflict, the conflict will
be recorded in the rebased commit and the rebase operation will succeed. You can
then resolve the conflict whenever you want. Conflicted states can be further
rebased, merged, or backed out. Note that what's stored in the commit is a
logical representation of the conflict, not conflict markers; rebasing a
conflict doesn't result in a nested conflict markers (see
technical doc for how this works).

Advantages

The deeper understanding of conflicts has many advantages:

  • Removes the need for things like
    git rebase/merge/cherry-pick/etc --continue. Instead, you get a single
    workflow for resolving conflicts: check out the conflicted commit, resolve
    conflicts, and amend.
  • Enables the "auto-rebase" feature, where descendants of rewritten commits
    automatically get rewritten. This feature mostly replaces Mercurial's
    Changeset Evolution.
  • Lets us define the change in a merge commit as being compared to the merged
    parents. That way, we can rebase merge commits correctly (unlike both Git and
    Mercurial). That includes conflict resolutions done in the merge commit,
    addressing a common use case for
    git rerere.
    Since the changes in a merge commit are displayed and rebased as expected,
    evil merges
    are arguably not as evil anymore.
  • Allows you to postpone conflict resolution until you're ready for it. You
    can easily keep all your work-in-progress commits rebased onto upstream's head
    if you like.
  • Criss-cross merges
    and octopus merges
    become trivial (implementation-wise); some cases that Git can't currently
    handle, or that would result in nested conflict markers, can be automatically
    resolved.
  • Enables collaborative conflict resolution. (This assumes that you can share
    the conflicts with others, which you probably shouldn't do if some people
    interact with your project using Git.)

For information about how conflicts are handled in the working copy, see
here.

Conflict markers

Conflicts are "materialized" using conflict markers in various contexts. For
example, when you run jj new or jj edit on a commit with a conflict, it will
be materialized in the working copy. Conflicts are also materialized when they
are part of diff output (e.g. jj show on a commit that introduces or resolves
a conflict).

As an example, imagine that you have a file which contains the following text,
all in lowercase:

text
apple
grape
orange

One person replaces the word "grape" with "grapefruit" in commit A, while
another person changes every line to uppercase in commit B. If you merge the
changes together with jj new A B, the resulting commit will have a conflict
since Jujutsu can't figure out how to combine these changes. Therefore, Jujutsu
will materialize the conflict in the working copy using conflict markers, which
would look like this:

text
<<<<<<< conflict 1 of 1
%%%%%%% diff from: vpxusssl 38d49363 "merge base"
\\\\\\\        to: rtsqusxu 2768b0b9 "commit A"
 apple
-grape
+grapefruit
 orange
+++++++ ysrnknol 7a20f389 "commit B"
APPLE
GRAPE
ORANGE
>>>>>>> conflict 1 of 1 ends

The markers <<<<<<< and >>>>>>> indicate the start and end of a conflict
respectively. The marker +++++++ indicates the start of a snapshot, while the
marker %%%%%%% indicates the start of a diff to apply to the snapshot. The
\\\\\\\ marker just allows the label to be split across two lines to make it
more readable.

Therefore, to resolve this conflict, you would apply the diff (changing "grape"
to "grapefruit") to the snapshot (the side with every line in uppercase),
editing the file to look like this:

text
APPLE
GRAPEFRUIT
ORANGE

In practice, conflicts are usually 2-sided, meaning that there's only 2
conflicting changes being merged together at a time, but Jujutsu supports
conflicts with arbitrarily many sides, which can happen when merging 3 or more
commits at once. In that case, you would see a single snapshot section and
multiple diff sections.

Compared to just showing the content of each side of the conflict, the main
benefit of Jujutsu's style of conflict markers is that you don't need to spend
time manually comparing the sides to spot the differences between them. This is
especially beneficial for many-sided conflicts, since resolving them just
requires applying each diff to the snapshot one-by-one.

Alternative conflict marker styles

If you prefer to just see the contents of each side of the conflict without the
diff, Jujutsu also supports a "snapshot" style, which can be enabled by setting
the ui.conflict-marker-style config option to "snapshot":

text
<<<<<<< conflict 1 of 1
+++++++ rtsqusxu 2768b0b9 "commit A"
apple
grapefruit
orange
------- vpxusssl 38d49363 "merge base"
apple
grape
orange
+++++++ ysrnknol 7a20f389 "commit B"
APPLE
GRAPE
ORANGE
>>>>>>> conflict 1 of 1 ends

Some tools expect Git-style conflict markers, so Jujutsu also supports Git's
"diff3" style

conflict markers by setting the ui.conflict-marker-style config option to
"git":

text
<<<<<<< rtsqusxu 2768b0b9 "commit A"
apple
grapefruit
orange
||||||| vpxusssl 38d49363 "merge base"
apple
grape
orange
=======
APPLE
GRAPE
ORANGE
>>>>>>> ysrnknol 7a20f389 "commit B"

This conflict marker style only supports 2-sided conflicts though, so it falls
back to the similar "snapshot" conflict markers if there are more than 2 sides
to the conflict.

Long conflict markers

Some files may contain lines which could be confused for conflict markers. For
instance, a line could start with =======, which looks like a Git-style
conflict marker. To ensure that it's always unambiguous which lines are conflict
markers and which are just part of the file contents, jj sometimes uses
conflict markers which are longer than normal:

text
<<<<<<<<<<<<<<< conflict 1 of 1
%%%%%%%%%%%%%%% diff from: wqvuxsty cb9217d5 "merge base"
\\\\\\\\\\\\\\\        to: kwntsput 0e15b770 "commit A"
-Heading
+HEADING
 =======
+++++++++++++++ mpnwrytz 52020ed6 "commit B"
New Heading
===========
>>>>>>>>>>>>>>> conflict 1 of 1 ends

Conflicts with missing terminating newline

When materializing conflicts, jj outputs them in a line-based format, with
each conflict marker line terminated by a newline character (\n). This format
works well for text files that consist of a series of lines, with each line
terminated by a newline character as well.

While most text files follow this convention, some do not. Since conflict
markers must appear on their own line, when jj encounters a missing
terminating newline character in a conflict, it still needs to ensure that
there is a newline before the next conflict marker. To do this, it adds an
extra newline to each term of the conflict. Then, to compensate for this extra
newline, it also omits the terminating newline from the "end of conflict"
marker (>>>>>>>).

For instance, if a file originally contained grape with no terminating newline
character, and one person changed grape to grapefruit, while another person
added the missing newline character to make grape\n, the resulting conflict
would look like this (␊ demonstrates the \n character explicitly; note that
the last line doesn't have the terminating \n character):

text
<<<<<<< conflict 1 of 1␊
+++++++ tlwwkqxk d121763d "commit A" (no terminating newline)␊
grapefruit␊
%%%%%%% diff from: qwpqssno fe561d93 "merge base" (no terminating newline)␊
\\\\\\\        to: poxkmrxy c735fe02 "commit B"␊
 grape␊
+␊
>>>>>>> conflict 1 of 1 ends

Therefore, a resolution of this conflict could be grapefruit\n, with the
terminating newline character added.


Contributing (`docs/contributing.md`)

How to Contribute

Policies

We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.

Contributor License Agreement

Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to https://cla.developers.google.com/ to see
whether you have already signed the CLA or to sign it.

You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again. In other words, the Google CLA is not scoped to a specific project. If
you sign the Google CLA, it applies to other Google open-source projects as
well (including future projects).

{% include-markdown '../AI_POLICY.md' heading-offset=2 %}

Commit guidelines

Unlike many GitHub projects (but like many VCS projects), we care more about the
contents of commits than about the contents of PRs. We review each commit
separately, and we don't squash-merge the PR (so please manually squash any
fixup commits before sending for review).

Each commit should ideally do one thing. For example, if you need to refactor a
function in order to add a new feature cleanly, put the refactoring in one
commit and the new feature in a different commit. If the refactoring itself
consists of many parts, try to separate out those into separate commits. You can
use jj split to do it if you didn't realize ahead of time how it should be
split up. Include tests and documentation in the same commit as the code they
test and document.

The commit message should describe the changes in the commit;
the PR description can even be empty, but feel free to include a personal
message. We don't use Conventional Commits and instead start the commit
message with <topic>: rather than like chore: , feat: and fix: .
This means if you modified a command in the CLI, use its name as the topic, e.g.
next/prev: <your-modification> or conflicts: <your-modification>. We don't
currently have a specific guidelines on what to write in the topic field, but
the reviewers will help you provide a topic if you have difficulties choosing
it. How to Write a Git Commit Message is a good
guide if you're new to writing good commit messages. We are not particularly
strict about the style, but please do explain the reason for the change unless
it's obvious. If the PR closes an open issue or feature request for the
project, include the Closes # or Fixes # in
the commit description.

If you have been invited to the jj-vcs/ organization feel free to create
branches directly in the repo, ideally prefixed by your name or username. This
allows other people to directly check out your work and possibly integrate them
in their custom builds of jj.

Code reviews

All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
GitHub Help for more
information on using pull requests.

The project unfortunately has a shortage of reviewers (a common problem in
open-source projects). To maximize the chances of getting a timely review, do
what you can to provide reviewers (and other future readers) with the necessary
context. For example, if you're adding a new feature, explain why that feature
is useful, how it works from the user's point of view, how it's designed, and
what the limitations of your design are. If you have considered other designs,
it may be useful to mention them. Another way of speeding up reviews is to
review others' PRs so the maintainers don't need to spend as much time on them.

When you address comments on a PR, don't make the changes in a commit on top (as
is typical on GitHub). Instead, please make the changes in the appropriate
commit. You can do that by creating a new commit on top of the initial commit
(jj new <commit>) and then squash in the changes when you're done (jj squash).
jj git push
will automatically force-push the bookmark.

Mark conversation threads as resolved when you have addressed them. If you
realize that a comment requires non-trivial changes, write a reply instead and
ask your reviewer to take another look after updating the code.

When your first PR has been approved, we typically invite you to the
jj-vcs/contributors team to give you contributor access,
so you can address any remaining minor comments and then merge the PR yourself
when you're ready.

If your employer pays anyone (not necessarily you) to contribute to Jujutsu,
please make sure your GitHub username is recorded.
To avoid conflicts of interest, please don't merge a PR that has only been
approved by someone from the same organization. Similarly, as a reviewer, there
is no need to approve your coworkers' PRs, since the author should await an
approval from someone else anyway. It is of course still appreciated if you
review and comment on their PRs. Also, if the PR seems completely unrelated to
your company's interests, do feel free to approve it.

Changelog entries

When creating PRs, please update CHANGELOG.md with the new
changes under the "Unreleased" section. Make sure to highlight any breaking
changes or deprecations under the appropriate subsections. If you fixed a bug
that has a corresponding issue, link the issue with Markdown link format, so
that the changelog page on the generated docs can point to the actual issue.
Generally a maintainer will update "Release highlights" during a release as
needed, but feel free to add your change there if it makes sense. The order in
each subsection does not really matter, so you can add your entry to the
beginning or the end.

Not every change requires a changelog entry. Generally, changelog entries are
useful for two reasons:

  1. Teach existing users about changes that affect their behavior. For example
    new features or fixed bugs would be good to know, as well as deprecations.
  2. Attract new users. Maybe there was some limitation (lack of support for
    something they needed), or maybe a shiny new feature really spoke to them.

By this reasoning, it's not necessary to mention changes that people will simply
notice when they run into them (or not notice at all), such as a reworded error
message, backend refactors, or documentation updates.

Community Guidelines

This project follows Google's Open Source Community
Guidelines
.

Deprecations and Removals

If your PR removes or renames an option, a command, or config option don't
remove it immediately. Instead implement a warning that the command, command
option, or config option is deprecated and will be removed in
jj v<current> + 6. Also add a // TODO: remove in jj<version+6>+ comment so
other contributors can remove it later. As we currently release monthly this
translates to a six month deprecation period. This is something the project
does to not break workflows and gives users the the option to
provide feedback on features we plan to remove or supersede with other
approaches.

Changes to the repo format (everything which is in .jj/) are more disruptive.
We aim to be backwards-compatible for at least a year (12 releases). Also
consider if it's worth being forwards-compatible (i.e making sure that the last
N versions of jj can read the new format). Examples of such changes are moving
repo-level configuration out of the repository or deriving the evolog from the
op-log.

Some things are required to be supported for even longer but that is in the end
a maintainer decision. Some examples of this were the jj checkout/merge
commands (removed after 24 months/releases) or the --destination flag for
which there is no final removal date yet.

Contributing large patches

We welcome contributions from anyone.

If you wish to contribute a new large feature, you are encouraged to discuss
it in Discord or a GitHub Discussion first before diving in. Before spending
too much time on a particular design, you should see if the community and the
maintainers have any feedback on your idea and general approach. It also is a
better experience for you as an author if you contributed smaller patches
first, so you understand how the project and the community around it works.

If you open a pull request without discussion first, you may find that the
community already has discussed this and found a better approach, or that it
already was deemed not to be a fit for the project.

Since many ideas have come up before, but they haven't been implemented because
they might be deceptively nuanced, or there may be strong opinions around them,
bringing it up to the community first helps discover past discussions, so your
design can be informed by them.

Once you have vetted your idea with the community, please write a
Design Doc which requires an architecture review from
multiple stakeholders, see the process here.

Contributing to the documentation

We appreciate bug
reports

about any problems, however small, lurking in our documentation
website
or in the jj help <command> docs. If a part of the bug report template does not apply, you can
just delete it.

Before reporting a problem with the documentation website, we'd appreciate it if
you could check that the problem still exists in the "prerelease" version of the
documentation (as opposed to the docs for one of the released versions of jj).
You can use the version switcher in the top-left of the website to do so.

If you are willing to make a PR fixing a documentation problem, even better!

The documentation website sources are Markdown files located in the docs/
directory
. You do not need to
know Rust to work with them. See below for instructions on how to preview the
HTML docs
as you edit the Markdown files.
Doing so is optional, but recommended.

The jj help docs are sourced from the "docstring" comments inside the Rust
sources, currently from the cli/src/commands
directory
. Working
on them requires setting up a Rust development environment, as described
below, and may occasionally require adjusting a test.

Learning Rust

In addition to the Rust Book and the other
excellent resources at https://www.rust-lang.org/learn, we recommend the
"Comprehensive Rust" mini-course
for an overview, especially if you are familiar with C++.

Style guide

See here.

Setting up a development environment

To develop jj, the mandatory steps are simply
to install Rust (the default
installer options are fine), clone the repository, and use cargo build
, cargo fmt,
cargo clippy --workspace --all-targets, and
cargo test --workspace. If you are preparing a PR, there are some additional
recommended steps.

Summary

One-time setup:

text
rustup toolchain add nightly  # wanted for 'rustfmt'
rustup toolchain add 1.97.1   # also specified in Cargo.toml
cargo install --locked bacon
cargo install --locked cargo-insta
cargo install --locked cargo-nextest

During development (adapt according to your preference):

text
bacon clippy-all
cargo +nightly fmt # Occasionally
cargo nextest run --workspace # Occasionally
cargo insta test --workspace --test-runner nextest # Occasionally

!!! warning

text
Build artifacts from debug builds and especially from repeated
invocations of `cargo test` can quickly take up 10s of GB of disk space.
Cargo will happily use up your entire hard drive. If this happens, run
`cargo clean`.

Explanation

These are listed roughly in order of decreasing importance.

  1. Nearly any change to jj's CLI will require writing or updating snapshot
    tests that use the insta crate. To make this
    convenient, install the cargo-insta binary.
    Use cargo insta test --workspace to run tests,
    and cargo insta review --workspace to update the snapshot tests.
    The --workspace flag is needed to run the tests on all crates; by default,
    only the crate in the current directory is tested.

  2. GitHub CI checks require that the code is formatted with the nightly
    version of rustfmt. To do this on your computer, install the nightly
    toolchain and use cargo +nightly fmt.

  3. Your code will be rejected if it cannot be compiled with the minimal
    supported version of Rust ("MSRV"). Currently, jj follows a rather
    casual MSRV policy: "The current rustc stable version, minus one."
    As of this writing, that version is 1.97.1.

  4. Your code needs to pass cargo clippy. You can also
    use cargo +nightly clippy if you wish to see more warnings.

  5. You may also want to install and use bacon,
    to automatically build, check, and / or run tests.

  6. To run tests more quickly, use cargo nextest run --workspace. To
    use nextest with insta, use cargo insta test --workspace --test-runner nextest.

    On Linux, you may be able to speed up nextest even further by using
    the mold linker, as explained below.

Configuring `jj fix` to run `rustfmt`

Run this in the jj repo:

shell
jj config set --repo fix.tools.rustfmt '{ command = ["rustfmt", "+nightly"], patterns = ["glob:**/*.rs"] }'

Note: users of Nix and direnv should drop the "+nightly" argument above
since the devShell is already configured to pull in a nightly version of
rustfmt:

shell
jj config set --repo fix.tools.rustfmt '{ command = ["rustfmt"], patterns = ["glob:**/*.rs"] }'

Using `mold` for faster tests on Linux

On a machine with a multi-core CPU, one way to speed up
cargo nextest on Linux is to use the multi-threaded mold
linker
. This linker may help
if, currently, your CPU is underused while Rust is linking test
binaries. Before proceeding with mold, you can check whether this is
an issue worth solving using a system monitoring tool such as htop.

mold is packaged for many distributions. On Debian, for example,
sudo apt install mold should just work.

A simple way to use mold is via the -run option, e.g.:

shell
mold -run cargo insta test --workspace --test-runner nextest

There will be no indication that a different linker is used, except for
higher CPU usage while linking and, hopefully, faster completion. You
can verify that mold was indeed used by running
readelf -p .comment target/debug/jj.

There are also ways of having Rust use mold by default, see the "How
to use" instructions
.

On recent versions of MacOS, the default linker Rust uses is already
multi-threaded. It should use all the CPU cores without any configuration.

Set up a RAM disk for faster tests on macOS

jj tests can be sped up significantly on macOS by using a RAM disk instead of
the usual /tmp directory (on one Mac, the tests sped up ~3 times from 180
seconds to 55 seconds). You can set this up as follows:

sh
sudo mkdir -p /Volumes/RAMDisk
sudo chmod a+wx /Volumes/RAMDisk
sudo mount_tmpfs /Volumes/RAMDisk  # Add `-e` to make it case-sensitive
                      # Add `-o nobrowse` to hide the tmpfs from Finder

You will need to re-run this after a reboot. Consider making it a script you can
run with sudo. (The first two commands don't need sudo if you change the dir
to somewhere in your HOME, the last one always does.)

Then, you can rust tests as, for example,

sh
TMPDIR=/Volumes/RAMDisk cargo insta test --workspace --test-runner nextest

You can double-check whether or not the RAM disk is mounted with

sh
mount | grep tmpfs

Some details and speculation

Hard drive speed is not the issue here. Experimentally, when /tmp
is physical, Ilya experienced the tests doing writes at 60MB/s (as reported by
the Activity Monitor) on an SSD that should be many times as fast.

Instead, it seems to have something to do with file locking and
fdatasync. A likely curlprit is the presence of
global kernel locks in APFS
. There are some benchmarks in
this
PR
.

It might be possible to add an entry to /etc/fstab to mount tmpfs
automatically. If you succeed with this, let us (and Ilya in particular) know!

Editor setup

Visual Studio Code

We recommend at least these settings:

js
{
    "files.insertFinalNewline": true,
    "files.trimTrailingWhitespace": true,
    "[rust]": {
        "files.trimTrailingWhitespace": false
    }
}

Zed

js
// .zed/settings.json
{
  "ensure_final_newline_on_save": true,
  "remove_trailing_whitespace_on_save": true,

  "languages": {
    // We don't use a formatter for Markdown files, so format_on_save would just
    // mess with others' docs
    "Markdown": { "format_on_save": "off" },
    "Rust": {
      "format_on_save": "on",
      // Avoid removing trailing spaces within multi-line string literals
      "remove_trailing_whitespace_on_save": false
    }
  },

  "lsp": {
    "rust-analyzer": {
      "initialization_options": {
        // If you are working on docs and don't need `cargo check`, uncomment
        // this option:
        //
        //   "checkOnSave": false,

        // Use nightly `rustfmt`, equivalent to `cargo +nightly fmt`
        "rustfmt": { "extraArgs": ["+nightly"] }
      }
    }
  }
}

Helix

toml
# .helix/languages.toml
[language-server.rust-analyzer.config.rustfmt]
extraArgs = ["+nightly"]

Alternative development setup with Nix

If you have Nix installed, you can use the provided
flake.nix to get a development environment with all necessary tools
pre-configured:

shell
nix develop

Once inside the shell, you can run cargo build, cargo test etc. as usual.
Other tools like uv, cargo-insta, cargo-nextest and more are installed,
and so you should be able to follow any other recommendations in this guide as
needed.

!!! tip "Editor integration"

text
Launch your editor from within the `nix develop` shell so that
rust-analyzer and other LSP tools can find the correct toolchain. For
example:

```shell
nix develop
code .  # or your editor of choice
```

!!! note "direnv users"

text
If you use [direnv](https://direnv.net/) with
[nix-direnv](https://github.com/nix-community/nix-direnv), you can `echo
"source_env .envrc.recommended" >> .envrc` to automatically activate the
development environment when entering the directory.

Alternative development setup with `mise`

An experimental development setup is available using
mise. If you try it, file bugs,
PRs, or tell us on Discord/IRC/discussions if you experience problems
or if this config is too inflexible. If we can make it work for most
platforms and most people's needs, including people previously unfamiliar
with mise, we may make mise the recommended way to set up a development
environment.

This tool manages the necessary dependencies for you, eliminating the need for
a separate setup process. mise automatically installs the required tools when
they are needed.

Here are some of the commands you may find yourself using frequently during
development:

  • mise test: Runs all tests.
  • mise test <string>: Runs tests that contain a specific string. For
    example, mise test squash would run tests with "squash" in their name.
  • mise build: Compiles jj.
  • mise build:release: Compiles jj in release mode.
  • mise build:docs: Builds the documentation for jj.

For a complete list of all available tasks, you can run mise tasks or review
the configuration file at .config/mise.toml.

You can customize this configuration using
a mise.local.toml file.

Previewing the HTML documentation

The documentation for jj is automatically published online at
https://docs.jj-vcs.dev/.

When editing documentation, you should check your changes locally — especially
if you are adding a new page, or doing a major rewrite.

Install `uv`

The only thing you need is uv (version 0.5.1 or newer).

uv is a Python project manager written in Rust. It will fetch the right Python
version and the dependencies needed to build the docs. Install it like so:

=== "macOS/Linux"

text
```shell
curl -LsSf https://astral.sh/uv/install.sh | sh
```

!!! note
    If you don't have `~/.local/bin` in your `PATH`, the installer will
    modify your shell profile. To avoid it:

    ```shell
    curl -LsSf https://astral.sh/uv/install.sh | env INSTALLER_NO_MODIFY_PATH=1 sh
    ```

=== "Windows"

text
```shell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```

=== "Homebrew"

text
```shell
brew install uv
```

=== "Cargo"

text
```shell
# This might take a while
cargo install --git https://github.com/astral-sh/uv uv
```

=== "Other options"

text
* Directly download the binaries from GitHub: [uv releases](https://github.com/astral-sh/uv/releases).
* Even more options: [Installing uv](https://docs.astral.sh/uv/getting-started/installation/).

Build the docs

To build the docs, run while inside the jj repository:

shell
uv run mkdocs serve

Open http://127.0.0.1:8000 in your browser to see the docs.

As you edit the .md files in docs/, the website should be rebuilt and
reloaded in your browser automatically.

!!! note "If the docs are not updating"
Check the terminal from which you ran uv run mkdocs serve for any build
errors or warnings. Warnings about "GET /versions.json HTTP/1.1" code 404
are expected and harmless.

Offline distribution

To build the rendered docs for offline distribution or to view them from your file
system, run while inside the jj repository:

shell
MKDOCS_OFFLINE=true uv run mkdocs build

Building the entire website

!!! tip
Building the entire website is not usually necessary. If you are editing
documentation, the previous section is enough.

text
These instructions are relevant if you are working on the versioning of the
documentation that we currently do with `mike`.

The full jj website includes the documentation for several jj versions
(prerelease, latest release, and the older releases). The top-level
URL https://docs.jj-vcs.dev redirects to
https://docs.jj-vcs.dev/latest, which in turn redirects to
the docs for the last stable version.

The different versions of documentation are managed and deployed with
mike, which can be run with
uv run mike.

On a POSIX system or WSL, one way to build the entire website is as follows (on
Windows, you'll need to understand and adapt the shell script):

  1. Check out jj (jj git clone), cloned from your fork of jj (e.g.
    github.com/jjfan/jj). You can also use a pure Git repo if you prefer.

  2. Make sure github.com/jjfan/jj includes the gh-pages bookmark of the jj repo
    and run git fetch origin gh-pages.

  3. Go to the GitHub repository settings, enable GitHub Pages, and configure them
    to use the gh-pages bookmark (this is usually the default).

  4. Install uv as explained in Previewing the HTML
    documentation
    , and run the same sh script
    that is used in GitHub CI (details below):

    shell
    .github/scripts/docs-build-deploy prerelease main --push

    This should build the version of the docs from the current commit,
    deploy it as a new commit to the gh-pages bookmark,
    and push the gh-pages bookmark to the origin.

  5. Now, you should be able to see the full website, including your latest changes
    to the prerelease version, at https://jjfan.github.io/jj/prerelease/.

  6. (Optional) The previous steps actually only rebuild
    https://jjfan.github.io/jj/prerelease/ and its alias
    https://jjfan.github.io/jj/main/. If you'd like to test out version switching
    back and forth, you can also rebuild the docs for the latest release as follows.

    shell
    jj new v1.33.1  # Let's say `jj 1.33.1` is the latest release
    .github/scripts/docs-build-deploy v1.33.1 latest --push
  7. (Optional) When you are done, you may want to reset the gh-pages bookmark to the
    same spot as it is upstream. If you configured the "upstream" remote,
    this can be done with:

    shell
    # This will LOSE any changes you made to `gh-pages`
    jj git fetch --remote upstream
    jj bookmark set gh-pages -r gh-pages@upstream
    jj git push --remote origin --bookmark gh-pages

    If you want to preserve some of the changes you made, you can do jj bookmark set my-changes -r gh-pages BEFORE running the above commands.

Explanation of the `docs-build-deploy` script

The script sets up a few environment variables and invokes uv run mike deploy
with some default arguments and whatever arguments were passed to
docs-build-deploy. Run uv run mike help deploy to find out what the
arguments do.

If you need to do something more complicated, you can use uv run mike ... commands. You can also edit the gh-pages bookmark directly, but take care
to avoid files that will be overwritten by future invocations of mike. Then,
you can submit a PR based on the gh-pages bookmark of
https://docs.jj-vcs.dev/ (instead of the usual main bookmark).

Previously, the version switcher would not work unless the value of the
site_url config in mkdocs.yml matched the actual URL the site is being
served from. This bug should now be fixed, but if you are not serving the site
from https://docs.jj-vcs.dev/ and something fails weirdly, you might want
to adjust the site_url to something like https://jjfan.github.io/jj.

Modifying protobuffers (this is not common)

Occasionally, you may need to change the .proto files that define jj's data
storage format. In this case, you will need to add a few steps to the above
workflow.

  • Install the protoc compiler. This usually means either apt-get install protobuf-compiler or downloading an official release. The
    prost library docs have additional advice.
  • Run cargo run -p gen-protos regularly (or after every edit to a .proto
    file). This is the same as running cargo run from lib/gen-protos. The
    gen-protos binary will use the prost-build library to compile the
    .proto files into .rs files.
  • If you are adding a new .proto file, you will need to edit the list of
    these files in lib/gen-protos/src/main.rs.

The .rs files generated from .proto files are included in the repository,
and there is a GitHub CI check that will complain if they do not match.

Logging

You can print internal jj logs using JJ_LOG. It acts like the RUST_LOG
environment variable, frequent in Rust codebases, and accepts one or more
directives. You can also run JJ_LOG=debug jj to get debug level logs
enabled for all targets. You can also use the --debug global option, which
turns on debug log level for jj-lib and jj-cli only.

Profiling

Introduction

jj provides two main approaches to profiling:

  1. Tracing-based profiling - Uses the Rust tracing crate to record spans
    and events. Good for understanding control flow and finding where time is spent
    in instrumented code.
  2. Sampling-based profiling - Uses system profilers (perf, dtrace) to sample
    the call stack. Good for finding hot spots including in uninstrumented code and
    external libraries.

Tracing-based profiling

jj uses the tracing crate with Chrome Trace Format output.

Set the JJ_TRACE environment variable to generate a trace file:

shell
# Generate trace with auto-generated filename (jj-trace-{timestamp}.json)
JJ_TRACE= jj st

# Generate trace with specific filename
JJ_TRACE=/tmp/trace.json jj st

To include function argument values in the trace (useful for detailed
debugging):

shell
JJ_TRACE_INCLUDE_ARGS=1 JJ_TRACE=/tmp/trace.json jj log -r 'ancestors(@, 10)'

The produced profiles can be imported into chrome://tracing (on Google Chrome)
or https://ui.perfetto.dev/ (all browsers).

Only functions annotated with #[tracing::instrument] or explicit trace_span!
macros appear in traces. If you need to profile a specific area that isn't
instrumented, you can add #[instrument] attributes to the relevant functions.

Sampling-based profiling

Sampling profilers periodically capture the call stack to identify hot spots. This approach:

  • Has lower overhead than tracing
  • Captures all code, including uninstrumented functions and external libraries (like gitoxide)
  • May miss short-lived function calls since it samples rather than traces every
    call

Using samply

One easy-to-use sampling profiler
is samply. For example:

shell
cargo install samply
samply record jj diff

Then just open the link it prints.

Using cargo-flamegraph

cargo-flamegraph generates flamegraph visualizations using system profilers:

shell
cargo install flamegraph
cargo flamegraph --bin jj -- diff

You may need to use sudo or install perf on your system, see flamegraph's
documentation for more.


Core Tenets (`docs/core_tenets.md`)

Jujutsu's Core Tenets

Jujutsu's core tenets are:

  • Separation of logic and UI: It should be easy to create new UIs (CLIs, GUIs,
    TUIs, servers) without having to duplicate logic.
  • Easy-to-use APIs: It should be easy to create new commands. For example,
    each command should not have to worry about concurrency, working-copy state,
    and rebasing descendants of rewritten commits.
  • User-friendliness: Making the working copy a commit is simpler. This is
    how the project started.
  • The repo is the source of truth: Most commands should operate on the commit
    graph. The working copy is just one way of editing commits.
  • Pluggable storage: Must be easy to integrate with different commit storage,
    virtual file systems and more.
  • Git-interoperability: Git is everywhere. We need to have good
    interoperability to be adopted.
  • All operations must be able to scale to Google-scale repos (lots of commits,
    lots of files): Laziness is important, must avoid accessing data
    unnecessarily.
  • Strive to be modeless: If at all possible commands should work consistently
    and as the user expects in every situation.
  • Make it incredibly hard to lose work in your repo.
  • Concurrent modifications to the repo should be safe.
  • Allow concurrent edits on any commit, pending or finished.
  • Make a "stacked diffs" workflow as easy as possible.

Design Doc Blueprint (`docs/design_doc_blueprint.md`)

Title

A cool name for your Project

Author: Your-Name

If there are multiple authors, just list them all

Summary

A short summary of your project/re-design/component and what problems it
addresses in about 3-10 sentences.

State of the Feature as of `$VERSION` (optional)

The state of the feature you want to improve and where it currently falls
short. If there's nothing to compare to, leave it out.

Prior work (optional)

Does this feature exist somewhere else and which tradeoffs it made.

If there's no prior work, then use the related work section below.

Goals and non-goals

Direct goals of the project and features deemed not worth pursuing.

Overview

A detailed overview of the project and the improvements it brings.

Detailed Design

The place to describe all new interfaces and interactions and how it plays into
the existing code and behavior. This is the place for all nitty-gritty details
which interact with the system.

Alternatives considered (optional)

Other alternatives to your suggested approach, and why they fall short.

Issues addressed (optional)

A list of issues which are addressed by this design.

If there's a feature in another VCS which shares some similarities to your
proposed work, it belongs here. An example would be Jujutsu sparse workspaces
and Perforce client workspaces.

Future Possibilities

The section for things which could be added to it or deemed out of scope during
the discussion.


Design Docs (`docs/design_docs.md`)

Jujutsu Design Docs

Jujutsu uses Design Docs to drive technical decisions on large projects and it
is the place to discuss your proposed design or new component. It is a very
thorough process, in which the design doc must be approved before PRs for the
feature will be accepted. It shares some similarities with Rust RFCs but
mostly addresses technical problems and gauges the technical and social
concerns of all stakeholders.

So if you want to start building a native backend or the server component for
Jujutsu, you'll need to go through this process.

Process

  1. Add a new markdown document to docs/design, named after your improvement
    or project.
  2. Describe the current state of the world and the things you want to improve.
  3. Wait for the Maintainers and Stakeholders to show up.
  4. Iterate until everyone accepts the change in normal codereview fashion.

Blueprint (Template)

You can find the base template of a new Design Doc
here.


Filesets (`docs/filesets.md`)

Filesets

Jujutsu supports a functional language for selecting a set of files.
Expressions in this language are called "filesets" (the idea comes from
Mercurial). The language
consists of file patterns, operators, and functions.

Quoting file names

Many jj commands accept fileset expressions as positional arguments. File
names passed to these commands must be quoted if they contain
whitespace or meta characters. However, as a special case, quotes can be omitted
if the expression has no operators nor function calls. For example:

  • jj diff 'Foo Bar' (shell quotes are required, but inner quotes are optional)
  • jj diff '~"Foo Bar"' (both shell and inner quotes are required)
  • jj diff '"Foo(1)"' (both shell and inner quotes are required)

Glob characters aren't considered meta characters, but shell quotes are still
required:

  • jj diff '~glob:**/*.rs'

File patterns

The following patterns are supported. In all cases, we do not mention any shell
quoting that might be necessary, and the quotes around "path" are optional if
the path has no special characters.

By default, "path" is parsed as a prefix-glob: pattern, which matches
cwd-relative path prefix.

  • cwd:"path": Matches cwd-relative path prefix (file or files under directory
    recursively.)
  • file:"path" or cwd-file:"path": Matches cwd-relative file (or exact) path.
  • glob:"pattern" or cwd-glob:"pattern": Matches file paths with cwd-relative
    Unix-style shell wildcard pattern. For example, glob:"*.c" will
    match all .c files in the current working directory non-recursively.
  • prefix-glob:"pattern" or cwd-prefix-glob:"pattern": Like glob:, but also
    matches path prefix (file or files under directory recursively.) For example,
    prefix-glob:"*.d" is equivalent to glob:"*.d" | glob:"*.d/**".
  • root:"path": Matches workspace-relative path prefix (file or files under
    directory recursively.)
  • root-file:"path": Matches workspace-relative file (or exact) path.
  • root-glob:"pattern": Matches file paths with workspace-relative Unix-style
    shell wildcard pattern.
  • root-prefix-glob:"pattern": Like root-glob:, but also matches path prefix
    (file or files under directory recursively.)

Glob patterns support case-insensitive matching by appending -i to the pattern
name. For example, glob-i:"*.TXT" will match both file.txt and FILE.TXT.

Operators

The following operators are supported. x and y below can be any fileset
expressions.

Operators are listed in order of binding power from strongest to weakest, e.g.
x | y & z is interpreted as x | (y & z) since & has stronger binding power
than |. Infix operators of the same binding power are parsed from left to
right, e.g. x ~ y & z is interpreted as (x ~ y) & z rather than x ~ (y & z).

As seen above, parentheses can be used to control evaluation order, e.g. (x & y) | z or x & (y | z).

    • f(x): Function call.
    • p:x: File pattern or pattern alias named p.
    • ~x: Matches everything but x.
    • x & y: Matches both x and y.
    • x ~ y: Matches x but not y.
    • x | y: Matches either x or y (or both).

Functions

You can also specify patterns by using functions.

  • all(): Matches everything.
  • none(): Matches nothing.

Aliases

New symbols, functions, and <name>:<value> patterns can be defined in the
config file, by using any combination of the predefined symbols / functions and
other aliases.

Alias functions can be overloaded by the number of parameters. However, builtin
function will be shadowed by name, and can't co-exist with aliases.

For example:

toml
[fileset-aliases]
LOCK = '**/Cargo.lock | **/package-lock.json | **/uv.lock'
'not:x' = '~x'

Alias descriptions

Alias descriptions can be surfaced in shell completions by defining the alias
as a table with .doc and .definition properties. For example:

toml
[fileset-aliases]
LOCK = {
    definition = '**/Cargo.lock | **/package-lock.json | **/uv.lock',
    doc = 'Lockfiles'
}

You can also use the dotted key syntax:

toml
[fileset-aliases]
LOCK.definition = '**/Cargo.lock | **/package-lock.json | **/uv.lock'
LOCK.doc = 'Lockfiles'

Examples

Show diff excluding Cargo.lock.

shell
jj diff '~Cargo.lock'

List files in src excluding Rust sources.

shell
jj file list 'src ~ glob:"**/*.rs"'

Split a revision in two, putting foo into the second commit.

shell
jj split '~foo'

Gerrit (`docs/gerrit.md`)

Using Jujutsu with Gerrit Code Review

JJ and Gerrit share the same mental model, which makes Gerrit feel like a
natural collaboration tool for JJ. JJ tracks a "change identity" across
rewrites, and Gerrit's Change-Id tracks the same logical change across patch
sets. JJ and Gerrit's Change-Ids aren't natively compatible yet, but they're
philosophically aligned. jj gerrit upload bridges the gap today by adding a
Gerrit-style Change-Id trailer while JJ keeps its own notion of change
identity on the client. In practice, that means small, clean commits that evolve
over time, exactly how Gerrit wants you to work.

This guide assumes a basic understanding of Git, Gerrit, and Jujutsu.

Set up a Gerrit remote

Jujutsu communicates with Gerrit by pushing commits to a Git remote. If you're
starting from an existing Git repository with Gerrit remotes already configured,
you can use jj git init to start using JJ in that repo. Otherwise, set up your
Gerrit remote:

shell
# Option 1: Start JJ in an existing Git repo with Gerrit remotes
$ jj git init

# Option 2: Add a Gerrit remote to a JJ repo
$ jj git remote add gerrit https://review.gerrithub.io/yourname/yourproject

# Option 3: Clone the repo via jj
$ jj git clone https://review.gerrithub.io/your/project

If you used option 2, you can configure default values in your repository config
by appending the following lines to your config file, like so (to do this for
a specific repo, run jj config edit --repo):

toml
[gerrit]
default-remote = "gerrit"       # name of the Git remote to push to
default-remote-branch = "main"  # target branch in Gerrit

Basic workflow

jj gerrit upload takes one or more revsets and uploads the stack of commits
and their ancestors to Gerrit. Each JJ change will map to a single Gerrit change
by generating a Change-Id based on the JJ change ID (or using the existing
Change-Id trailer if already present). This should be what you want most of
the time, but if you want to associate a JJ change with a specific change
already uploaded to Gerrit, you can copy the Change-Id trailer from Gerrit to
the bottom of the commit description in JJ.

Note: Gerrit identifies and updates changes by the Change-Id trailer. When
you re-upload a commit with the same Change-Id, Gerrit creates a new patch
set.

Upload a single change

shell
# Uploads `@` if it has a description, otherwise uploads `@-`.
$ jj gerrit upload

# Or explicitly specify a revision to upload.
$ jj gerrit upload -r @-

Selecting revisions (revsets)

jj gerrit upload accepts one or more -r/--revisions arguments. Each
argument may expand to multiple commits. Common patterns:

  • -r @-: the commit previous to the one you're currently working on
  • -r A..B: commits that are ancestors of B but not of A

See the revsets guide for more information.

Preview without pushing

Use --dry-run to see which commits would be modified and pushed, and where,
without changing anything or contacting the remote.

shell
$ jj gerrit upload -r '@-' --remote-branch main --dry-run

Target branch and remote selection

There are a few ways of specifying the target remote for your projects:

  • Run jj config set --user gerrit.default-remote <remote name> to set your
    default remote across all repos.
  • Run jj config set --repo gerrit.default-remote <remote name> to set your
    default remote for this specific repo.
  • Use --remote <remote name> to use a specific remote in one invocation of
    jj gerrit upload.
  • Otherwise, a remote named gerrit will be used. If that doesn't exist, the
    command will fail, and you will need to use one of the above methods to
    specify the remote to upload to.

Additionally, you can specify the target remote branch:

  • Run jj config set --user gerrit.default-remote-branch to set
    your default branch across all repos.
  • Run jj config set --repo gerrit.default-remote-branch to set
    your default branch for this specific repo.
  • Use --remote-branch to use a specific branch in one invocation
    of jj gerrit upload.

Updating changes after review

To address review feedback, update your revisions, then run jj gerrit upload again with the same revsets. Gerrit will add new patch sets to the
existing changes instead of creating new ones.

Examples:

shell
# Edit an earlier commit in the stack
$ jj edit xyz  # position on the stack to edit
# Apply needed edits, then upload the updates
$ jj gerrit upload -r xyz

`Change-Id` management

If you do not provide an explicit Change-Id trailer in your commits,
jj gerrit upload will generate a transient one for you based on your JJ
change ID. This means that as long as the JJ change ID remains the same (and
you don't add an explicit Change-Id trailer), it will upload as a new patch
set on the existing change.

Keep this association in mind when splitting or squashing changes. For example,
when splitting a change, the portion that you want associated with the
original Gerrit change should remain in the original JJ change (the first half
of the split). Similarly, when squashing new changes, you typically want to
squash into the change that was previously uploaded to Gerrit.

If your JJ changes no longer align with the desired mapping to Gerrit changes,
you can manually copy a Gerrit Change-Id trailer into your JJ change
description to directly assign a JJ change to an exist Gerrit change.

As an alternative to jj gerrit upload's automatic Change-Id mapping, you
can configure JJ to automatically add Change-Id trailer to all change
descriptions:

toml
[templates]
commit_trailers = '''
if(
  !trailers.contains_key("Change-Id"),
  format_gerrit_change_id_trailer(self)
)
'''

In this case, the Gerrit change mapping is defined entirely by the Change-Id
trailer. When splitting or squashing changes, be sure to keep the Change-Id
trailer associated with the desired changes. Be sure not to duplicate the same
Change-Id across different changes. Gerrit will reject pushes that contain
duplicate Change-Ids, but if the uploads are done separately, you may
unintentionally overwrite an existing change.

Note that when jj gerrit upload automatically adds a Change-Id trailer
before pushing to Gerrit, this addition to the commit description will only be
reflected on the uploaded commit, not your local commit. This means the server
will have a different commit ID than you do. As a result, you may encounter
divergence when you fetch a merged change into your local repo. To address this,
you can abandon your local change or rebase it on top of trunk with
jj rebase --skip-emptied ..., which will resolve the divergence. Alternatively,
add the above config so that the Change-Id trailer is automatically added to
your local commits before jj gerrit upload does.

Since version 3.3.1 Gerrit supports an alternative to the Change-Id trailer,
using a Link trailer in the format of <review-url>/id/I<change-id>. It is
only documented in the commit-msg hook documentation. Jujutsu's
jj gerrit upload will do the same if you set
jj config set --repo gerrit.review-url <review-url>.


Git Command Table (`docs/git-command-table.md`)

Git command table

Note that all jj commands can be run on any commit (not just the working-copy
commit), but that's left out of the table to keep it simple. For example,
jj squash -r <revision> will move the diff from that revision into its
parent.

{{ read_yaml("git-command-table.yml") }}


Git Command Table.Yml (`docs/git-command-table.yml`)

  • Use case: Create a new repo
    Git command: >
    git init
    Jujutsu command: >
    jj git init [--no-colocate]
    Notes:

  • Use case: Clone an existing repo
    Git command: >
    git clone <destination> [--origin <remote name>]
    Jujutsu command: >
    jj git clone <destination> [--remote <remote name>]
    Notes: There is no support for cloning non-Git repos yet.

TODO: Mention that you might need to do a `jj bookmark track branch` to see

the bookmark in `jj log`.

  • Use case: Update the local repo with all bookmarks/branches from a remote
    Git command: >
    git fetch [<remote>]
    Jujutsu command: >
    jj git fetch [--remote <remote>]
    Notes: There is no support for fetching into non-Git repos yet.

TODO: This only affects tracked branches now.

  • Use case: Update a remote repo with all bookmarks/branches from the local repo
    Git command: >
    git push --all [<remote>]
    Jujutsu command: >
    jj git push --all [--remote <remote>]
    Notes: There is no support for pushing from non-Git repos yet.

  • Use case: Update a remote repo with a single bookmark from the local repo
    Git command: >
    git push <remote>
    Jujutsu command: >
    jj git push --bookmark [--remote <remote>]
    Notes: There is no support for pushing from non-Git repos yet.

  • Use case: Add a remote target to the repo
    Git command: >
    git remote add <remote>
    Jujutsu command: >
    jj git remote add <remote>
    Notes:

  • Use case: Show summary of current work and repo status
    Git command: >
    git status
    Jujutsu command: >
    jj st
    Notes:

  • Use case: Show diff of the current change
    Git command: >
    git diff HEAD
    Jujutsu command: >
    jj diff
    Notes:

  • Use case: Show diff of another change
    Git command: >
    git diff <revision>^ <revision>
    Jujutsu command: >
    jj diff -r <revision>
    Notes:

  • Use case: Show diff from another change to the current change
    Git command: >
    git diff <revision>
    Jujutsu command: >
    jj diff --from <revision>
    Notes:

  • Use case: Show diff from change A to change B
    Git command: >
    git diff A B
    Jujutsu command: >
    jj diff --from A --to B
    Notes:

  • Use case: Show all the changes in A..B
    Git command: >
    git diff A...B
    Jujutsu command: >
    jj diff -r A..B
    Notes:

  • Use case: Show description and diff of a change
    Git command: >
    git show <revision>
    Jujutsu command: >
    jj show <revision>
    Notes:

  • Use case: Add a file to the current change
    Git command: >
    touch filename; git add filename
    Jujutsu command: >
    touch filename
    Notes:

  • Use case: Remove a file from the current change
    Git command: >
    git rm filename
    Jujutsu command: >
    rm filename
    Notes:

  • Use case: >
    Remove a previously tracked file from the current change, but keep it in the working
    copy
    Git command: >
    git rm --cached filename
    Jujutsu command: >
    jj file untrack filename
    Notes: >
    File name must match an ignore pattern to remain untracked. See
    the documentation for working copies for
    more.

  • Use case: Modify a file in the current change
    Git command: >
    echo stuff >> filename
    Jujutsu command: >
    echo stuff >> filename
    Notes:

  • Use case: Finish work on the current change and start a new change
    Git command: >
    git commit -a
    Jujutsu command: >
    jj commit
    Notes:

  • Use case: See compact log graph of ancestors of the current commit
    Git command: >
    git log --oneline --graph
    Jujutsu command: >
    jj log -r ::@
    Notes:

  • Use case: See compact log graph of all reachable commits
    Git command: >
    git log --oneline --graph --all
    Jujutsu command: >
    jj log -r 'all()'
    or jj log -r ::
    Notes: >
    In a Git-backed Jujutsu repository the Git command will also show all commits preserved by Jujutsu,
    including hidden commits. To exclude all commits only preserved by Jujutsu, replace --all by --exclude refs/jj/* --all.
    This will also exclude the Jujutsu-reachable commits though, if they are not Git-reachable.

  • Use case: Show compact log graph of commits not on the main branch
    Git command: >
    git log --oneline --graph --branches --not upstream/main
    Jujutsu command: >
    jj log
    Notes:

  • Use case: >
    Show log of commits that have the string "stuff" in the changed lines,
    also known as the "pickaxe" in Git
    Git command: >
    git log -G stuff
    Jujutsu command: >
    jj log -r 'diff_lines(regex:stuff)'
    Notes:

  • Use case: List versioned files in the working copy
    Git command: >
    git ls-files --cached
    Jujutsu command: >
    jj file list
    Notes:

  • Use case: Search among files versioned in the repository
    Git command: >
    git grep foo
    Jujutsu command: >
    jj file search --pattern=foo
    or rg --no-require-git foo
    Notes:

  • Use case: Abandon the current change and start a new change
    Git command: >
    git reset --hard
    (cannot be undone)
    Jujutsu command: >
    jj abandon
    Notes:

  • Use case: Make the current change empty
    Git command: >
    git reset --hard
    (same as abandoning a change since Git has no concept of a "change")
    Jujutsu command: >
    jj restore
    Notes:

  • Use case: Abandon the parent of the working copy, but keep its diff in the working copy
    Git command: >
    git reset --soft HEAD~
    Jujutsu command: >
    jj squash --from @-
    Notes:

  • Use case: Discard working copy changes in some files
    Git command: >
    git restore ...
    or git checkout HEAD -- ...
    Jujutsu command: >
    jj restore ...
    Notes:

  • Use case: Edit description (commit message) of the current change
    Git command: Not supported
    Jujutsu command: >
    jj describe
    Notes:

  • Use case: Edit description (commit message) of the previous change
    Git command: >
    git commit --amend --only
    Jujutsu command: >
    jj describe @-
    Notes:

  • Use case: Edit description (commit message) of any change
    Git command: >
    git commit --fixup=reword:X; git rebase --autosquash X^
    Jujutsu command: >
    jj describe X
    Notes:

  • Use case: Temporarily put away the current change
    Git command: >
    git stash
    Jujutsu command: >
    jj new @-
    Notes: >
    The old working-copy commit remains as a sibling commit.
    The old working-copy commit X can be restored with jj edit X.

  • Use case: Start working on a new change based on the

    bookmark/branch
    Git command: >
    git switch -c topic main
    or git checkout -b topic main
    (may need to stash or commit first)
    Jujutsu command: >
    jj new main
    Notes:

  • Use case: Merge branch A into the current change
    Git command: >
    git merge A
    Jujutsu command: >
    jj new @ A
    Notes:

  • Use case: Check out a named revision (or branch) to examine source
    Git command: >
    git checkout v1.0.1
    Jujutsu command: >
    jj new v1.0.1
    Notes: Creates new empty change on top (see jj new main)

  • Use case: Move bookmark/branch A onto bookmark/branch B
    Git command: >
    git rebase B A
    (may need to rebase other descendant branches separately)
    Jujutsu command: >
    jj rebase -b A -o B
    Notes:

  • Use case: Move change A and its descendants onto change B
    Git command: >
    git rebase --onto B A^ <some descendant bookmark>
    (may need to rebase other descendant bookmarks separately)
    Jujutsu command: >
    jj rebase -s A -o B
    Notes:

  • Use case: Reorder changes from A-B-C-D to A-C-B-D
    Git command: >
    git rebase -i A
    Jujutsu command: >
    jj rebase -r C --before B
    Notes:

  • Use case: Reorder multiple commits at once
    Git command: >
    git rebase -i
    Jujutsu command: >
    jj arrange
    Notes:

  • Use case: Move the diff in the current change into the parent change
    Git command: >
    git commit --amend -a
    Jujutsu command: >
    jj squash
    Notes:

  • Use case: Interactively move part of the diff in the current change into the parent change
    Git command: >
    git add -p; git commit --amend
    Jujutsu command: >
    jj squash -i
    Notes:

  • Use case: Move the diff in the working copy into an ancestor
    Git command: >
    git commit --fixup=X; git rebase --autosquash X^
    Jujutsu command: >
    jj squash --into X
    Notes:

  • Use case:
    Interactively move part of the diff in an arbitrary change to another arbitrary
    change
    Git command: Not supported
    Jujutsu command: >
    jj squash -i --from X --into Y
    Notes:

  • Use case: Interactively split the changes in the working copy in two
    Git command: >
    git commit -p
    Jujutsu command: >
    jj split
    Notes:

  • Use case: Interactively split an arbitrary change in two
    Git command: >
    Not supported
    (can be emulated with the "edit" action in git rebase -i)
    Jujutsu command: >
    jj split -r <revision>
    Notes:

  • Use case: Interactively edit the diff in a given change
    Git command: >
    Not supported
    (can be emulated with the "edit" action in git rebase -i)
    Jujutsu command: >
    jj diffedit -r <revision>
    Notes:

  • Use case: Resolve conflicts and continue interrupted operation
    Git command: >
    echo resolved > filename; git add filename; git rebase/merge/cherry-pick --continue
    Jujutsu command: >
    echo resolved > filename; jj squash
    Notes: Operations don't get interrupted, so no need to continue.

  • Use case: Create a copy of a commit on top of another commit
    Git command: >
    git co <destination>; git cherry-pick
    Jujutsu command: >
    jj duplicate -o <destination>
    Notes:

  • Use case: Find the root of the working copy (or check if in a repo)
    Git command: >
    git rev-parse --show-toplevel
    Jujutsu command: >
    jj workspace root
    Notes:

  • Use case: List bookmarks/branches
    Git command: >
    git branch
    Jujutsu command: >
    jj bookmark list
    or jj b l for short
    Notes:

  • Use case: Create a bookmark/branch
    Git command: >
    git branch <name> <revision>
    Jujutsu command: >
    jj bookmark create <name> -r <revision>
    Notes:

  • Use case: Move a bookmark/branch forward
    Git command: >
    git branch -f <name> <revision>
    Jujutsu command: >
    jj bookmark move <name> --to <revision>
    or jj b m <name> -t <revision> for short
    Notes:

  • Use case: Move a bookmark/branch backward or sideways
    Git command: >
    git branch -f <name> <revision>
    Jujutsu command: >
    jj bookmark move <name> --to <revision> --allow-backwards
    Notes:

  • Use case: Delete a bookmark/branch
    Git command: >
    git branch --delete <name>
    Jujutsu command: >
    jj bookmark delete <name>
    Notes:

  • Use case: List tags
    Git command: >
    git tag -l
    Jujutsu command: >
    jj tag list
    Notes:

  • Use case: Create a tag
    Git command: >
    git tag <name> <revision>
    Jujutsu command: >
    jj tag set <name> -r <revision>
    Notes:

  • Use case: Delete a tag
    Git command: >
    git tag -d <name>
    Jujutsu command: >
    jj tag delete <name>
    Notes:

  • Use case: List tags that contain a revision (targets are descendants of the revision)
    Git command: >
    git tag --contains <revision>
    Jujutsu command: >
    jj tag list -r '<revision>::'
    Notes: >
    Uses jj's revset language. The :: postfix operator selects all
    descendants of the given revision.

  • Use case: List tags merged into a revision (targets are ancestors of the revision)
    Git command: >
    git tag --merged <revision>
    Jujutsu command: >
    jj tag list -r '::<revision>'
    Notes: >
    Uses jj's revset language. The :: prefix operator selects all
    ancestors of the given revision.

  • Use case: See log of operations performed on the repo
    Git command: Not supported
    Jujutsu command: >
    jj op log
    Notes:

  • Use case: Undo the last operation
    Git command: Not supported
    Jujutsu command: >
    jj undo
    Notes: >
    There is also a matching jj redo command to redo the most recently undone
    operation.

  • Use case: Revert an earlier operation
    Git command: Not supported
    Jujutsu command: >
    jj op revert
    Notes:

  • Use case: Create a commit that cancels out a previous commit
    Git command: >
    git revert <revision>
    Jujutsu command: >
    jj revert -r <revision> -B @
    Notes:

  • Use case: Show what revision and author last modified each line of a file
    Git command: >
    git blame <file>
    Jujutsu command: >
    jj file annotate
    Notes:


Git Comparison (`docs/git-comparison.md`)

Comparison with Git

Introduction

This document attempts to describe how Jujutsu is different from Git. See
the Git-compatibility doc for information about how
the jj command interoperates with Git repos. See
the Git command table for a table of similar commands.

Overview

Here is a list of conceptual differences between Jujutsu and Git, along with
links to more details where applicable and available. There's a
table explaining how to achieve various use cases.

  • The working copy is automatically committed. That results in a simpler and
    more consistent CLI because the working copy is now treated like any other
    commit. Details.

  • There's no index (staging area). Because the working copy is automatically
    committed, an index-like concept doesn't make sense. The index is very similar
    to an intermediate commit between HEAD and the working copy, so workflows
    that depend on it can be modeled using proper commits instead. Jujutsu has
    excellent support for moving changes between commits. Details.

  • No need for branch names (but they are supported as
    bookmarks).
    Git lets you check out a commit without
    attaching a branch to it. It calls this state "detached HEAD". This is the
    normal state in Jujutsu (there's actually no way -- yet, at least -- to have
    an active branch/bookmark). However, Jujutsu keeps track of all visible heads
    (leaves) of the commit graph, so the commits won't get lost or
    garbage-collected.

  • No current branch. Git lets you check out a branch, making it the 'current
    branch', and new commits will automatically update the branch. This is
    necessary in Git because Git might otherwise lose track of the new commits.

    Jujutsu does not have a corresponding concept of a 'current bookmark';
    instead, you update bookmarks manually. For example, if you start work on top
    of a commit with a bookmark, new commits are created on top of the bookmark,
    then you issue a later command to update the bookmark.

  • Conflicts can be committed. No commands fail because of merge conflicts.
    The conflicts are instead recorded in commits and you can resolve them later.
    Details. This lets us rebase conflict and conflict
    resolutions, and thereby addressing most git rerere use cases.

  • Descendant commits are automatically rebased. Whenever you rewrite a
    commit (e.g. by running jj rebase), all its descendants commits will
    automatically be rebased on top. Branches pointing to it will also get
    updated, and so will the working copy if it points to any of the rebased
    commits.

  • No "evil merges". In Git, a merge commit that contains changes that are
    not in any parent are called evil merges. We believe the reason for this
    opinionated term is that Git has historically not been very good at working
    with such commits. For example, git show doesn't show them well (without
    --remerge-diff from 2022), and git rebase drops the changes (without
    --rebase-merges from 2018). Jujutsu defines the changes in a commit as
    being relative to the auto-merged parents in all contexts, so you can safely
    include change in merge commits.

  • Bookmarks/branches are identified by their names (across remotes). For
    example, if you pull from a remote that has a main branch, you'll get a
    bookmark by that name in your local repo. If you then move it and push back to
    the remote, the main branch on the remote will be updated.
    Details.

  • The operation log replaces reflogs. The operation log is similar to
    reflogs, but is much more powerful. It keeps track of atomic updates to all
    refs at once (Jujutsu thus improves on Git's per-ref history much in the same
    way that Subversion improved on RCS's per-file history). The operation log
    powers e.g. the undo functionality. Details

  • There's a single, virtual root commit. Like Mercurial, Jujutsu has a
    virtual commit (with a hash consisting of only zeros) called the "root commit"
    (called the "null revision" in Mercurial). This commit is a common ancestor of
    all commits. That removes the awkward state Git calls the "unborn branch"
    state (which is the state a newly initialized Git repo is in), and related
    command-line flags (e.g. git rebase --root, git checkout --orphan).

The index

Git's "index" has
multiple roles. One role is as a cache of file system information. Jujutsu has
something similar. Unfortunately, Git exposes the index to the user, which makes
the CLI unnecessarily complicated (learning what the different flavors of
git reset do, especially when combined with commits and/or paths, usually
takes a while). Jujutsu, like Mercurial, doesn't make that mistake.

As a Git power-user, you may think that you need the power of the index to
commit only part of the working copy. However, Jujutsu provides commands for
more directly achieving most use cases you're used to using Git's index for. For
example, to create a commit from part of the changes in the working copy, you
might be used to using git add -p; git commit. With Jujutsu, you'd instead
use jj split to split the working-copy commit into two commits. To add more
changes into the parent commit, which you might normally use
git add -p; git commit --amend for, you can instead use jj squash -i to
choose which changes to move into the parent commit, or jj squash <file> to
move a specific file.


Git Compatibility (`docs/git-compatibility.md`)

Git compatibility

Jujutsu has two backends for storing commits. One of them uses a regular Git
repo, which means that you can collaborate with Git users without them even
knowing that you're not using the git CLI.

See jj help git for help about the jj git family of commands, and e.g.
jj help git push for help about a specific command (use jj git push -h for
briefer help).

Supported features

The following list describes which Git features Jujutsu is compatible with. For
a comparison with Git, including how workflows are different, see the
Git-comparison doc.

  • Configuration: Partial. The only configuration from Git (e.g. in
    ~/.gitconfig) that's respected is the following. Feel free to file a bug if
    you miss any particular configuration options.
    • The configuration of remotes ([remote "<name>"]). Simple fetch refspecs
      are respected when branches are not explicitly specified on the CLI.
      (git is used for remote operations)
    • core.excludesFile
  • Authentication: Yes. git is used for remote operations under the hood.
  • Branches: Yes. You can read more about
    how branches work in Jujutsu
    and how they interoperate with Git.
  • Tags: Partial. You can check out tagged commits by name (pointed to by
    either annotated or lightweight tags). You can also create lightweight tags,
    but you cannot create annotated tags.
  • .gitignore: Yes. Patterns in .gitignore files are supported. So are
    ignores in .git/info/exclude or configured via Git's core.excludesFile
    config. Since working-copy files are snapshotted by almost every jj command,
    you might need to run jj file untrack to exclude newly ignored files from the
    working-copy commit. It's recommended to set up the ignore patterns earlier.
    The .gitignore support uses a native implementation, so please report a bug
    if you notice any difference compared to git.
  • .gitattributes: No. There's #53
    about adding support for at least the eol attribute.
  • Hooks: No. There's #405
    specifically for providing the checks from https://pre-commit.com.
  • Merge commits: Yes. Octopus merges (i.e. with more than 2 parents) are
    also supported.
  • Detached HEAD: Yes. Jujutsu supports anonymous branches, so this is a
    natural state.
  • Orphan branch: Yes. Jujutsu has a virtual root commit that appears as
    parent of all commits Git would call "root commits".
  • Staging area: Kind of. The staging area will be ignored. For example,
    jj diff will show a diff from the Git HEAD to the working copy. There are
    ways of fulfilling your use cases without a staging
    area
    .
  • Garbage collection: Yes. It should be safe to run git gc in the Git
    repo, but it's not tested, so it's probably a good idea to make a backup of
    the whole workspace first. There's no garbage collection and repacking of
    Jujutsu's own data structures yet
    ,
    however.
  • Bare repositories: Yes. You can use jj git init --git-repo= to
    create a repo backed by a bare Git repo.
  • Submodules: No. They will not show up in the working copy, but they will
    not be lost either.
  • Partial clones: No.
  • Shallow clones: Kind of. Shallow commits all have the virtual root commit
    as their parent. However, deepening or fully unshallowing a repository is
    currently not yet supported and will cause issues.
  • git-worktree: No. However, there's native support for multiple working
    copies backed by a single repo. See the jj workspace family of commands.
  • Sparse checkouts: No. However, there's native support for sparse
    checkouts. See the jj sparse command.
  • Signed commits: Yes.
    You can sign commits automatically by configuration,
    or use the jj sign command.
  • Git LFS: No. (#80)

Creating an empty repo

To create an empty repo using the Git backend, use jj git init <name>. This
creates a colocated Jujutsu workspace,
there will be a .jj directory and a .git directory.

Creating a repo backed by an existing Git repo

There are two common ways to start using Jujutsu with a Git repository you
already have on disk.

In the same working copy (colocated)

To create a Jujutsu repo in the Git repository itself (a repo with a
colocated workspace), change into that
directory and run:

shell
$ cd path/to/git-repo
$ jj git init

Jujutsu reuses the existing Git repository as its backing store, so both .jj/
and .git/ live at the top level of the same working copy and you can use both
jj and git commands there. This is the usual way to start using Jujutsu on
an existing local Git checkout.

In a separate working copy

To create a Jujutsu repo that shares commits with an existing Git repo but has
its own working-copy files (similar to a
Git worktree), create it in a directory
that is not already a Git checkout and point it at the Git repo:

shell
$ cd path/to/new/jj-repo
$ jj git init --git-repo=path/to/git-repo

(path/to/git-repo may be absolute, or relative to the current directory.) See
jj git init --help for other arguments.

The working-copy files and the record of the working-copy commit will be
separate, but the commits will be accessible in both repos. Use jj git import
to update the Jujutsu repo with changes made in the Git repo. Use
jj git export to update the Git repo with changes made in the Jujutsu repo.

Creating a repo by cloning a Git repo

To create a Jujutsu repo from a remote Git URL, use jj git clone [<destination>]. For example, jj git clone https://github.com/octocat/Hello-World will clone GitHub's "Hello-World" repo
into a directory by the same name.

By default, the remote repository will be named origin. You can use a name of
your choice by adding --remote <remote name> to the jj git clone command.

Colocated Jujutsu/Git workspaces

A colocated Jujutsu workspace is a hybrid Jujutsu/Git workspace. This is the
default for Git-backed workspace created with jj git init or jj git clone.
The Git repo and the Jujutsu workspace then share the same working copy. Jujutsu
will import and export from and to the Git repo on every jj command
automatically.

This mode is very convenient when tools (e.g. build tools) expect a Git repo to
be present.

It is allowed to mix jj and git commands in such a workspace in any order.
However, it may be easier to keep track of what is going on if you mostly use
read-only git commands and use jj to make changes to the repo. One reason
for this (see below for more) is that jj commands will usually put the Git
repo in a "detached HEAD" state, since in jj there is not concept of a
"currently tracked branch". Before doing mutating Git commands, you may need to
tell Git what the current branch should be with a git switch command.

You can undo the results of mutating git commands using jj undo and jj op restore. Inside jj op log, changes by git will be represented as an "import
git refs" operation.

You can disable colocation with the --no-colocate flag on the commands jj git init and jj git clone or by setting the configuration git.colocate = false.
Much of the repo data will still be stored in the Git repository format, but the
Git repository will be hidden inside a sub-directory of the .jj directory.
Moreover, unless you explicitly use the jj git import and jj git export
commands, that Git repository will either have no branches at all (not even a
main branch) or will have branches that are out of date with jj's bookmarks.

Colocation can be disabled because it does have some disadvantages:

  • Interleaving jj and git commands increases the chance of confusing branch
    conflicts or conflicted (AKA divergent) change
    ids
    . These never lose data, but can be
    annoying.

    Such interleaving can happen unknowingly. For example, some IDEs can cause
    it because they automatically run git fetch in the background from time to
    time.

  • In colocated workspaces with a very large number of branches or other refs,
    jj commands can get noticeably slower because of the automatic
    jj git import executed on each command. This can be mitigated by
    occasionally running jj util gc to speed up the import (that command
    includes packing the Git refs).

  • Git tools will have trouble with revisions that contain conflicted files.
    While jj renders these files with conflict markers in the working copy, they
    are stored in a non-human-readable fashion inside the repo. Git tools will
    often see this non-human-readable representation.

  • When a jj branch is conflicted, the position of the branch in the Git repo
    will disagree with one or more of the conflicted positions. The state of that
    branch in git will be labeled as though it belongs to a remote named "git",
    e.g. branch@git.

  • Jujutsu will ignore Git's staging area. It will not understand merge conflicts
    as Git represents them, unfinished git rebase states, as well as other less
    common states a Git repository can be in.

  • Colocated workspaces are less resilient to
    concurrency
    issues if you share the repo using an NFS filesystem or Dropbox. In general,
    such use of Jujutsu is not currently thoroughly tested.

  • There may still be bugs when interleaving mutating jj and git commands,
    usually having to do with a branch pointer ending up in the wrong place. We
    are working on the known ones, and are not aware of any major ones. Please
    report any new ones you find, or if any of the known bugs are less minor than
    they appear.

Converting a workspace into a colocated workspace

A Jujutsu workspace backed by a Git repo has a full Git repo inside. Such a
workspace can be converted into a colocated workspace using the
jj git colocation command.

To check the current colocation status of your workspace:

bash
jj git colocation status

To convert to a colocated workspace:

bash
jj git colocation enable

To convert to a non-colocated workspace:

bash
jj git colocation disable

The jj git colocation enable command automates the following manual process:

bash
# Ignore the .jj directory in Git
echo '/*' > .jj/.gitignore
# Move the Git repo
mv .jj/repo/store/git .git
# Tell jj where to find it (do not use on Windows! See below.)
echo -n '../../../.git' > .jj/repo/store/git_target
# Make the Git repository non-bare and set HEAD
git config --unset core.bare
# Convince jj to update .git/HEAD to point to the working-copy commit's parent
jj new && jj undo

!!! warning

text
On Windows, the `echo` command will append line endings and cause `jj`
to complain about the contents of `git_target`.

Instead of the `echo -n ...` line, use:
`Set-Content -Path .jj/repo/store/git_target -Value ../../../.git -NoNewLine`

Branches

TODO: Describe how branches are mapped

Format mapping details

Paths are assumed to be UTF-8. I have no current plans to support paths with
other encodings.

Commits created by jj have a ref starting with refs/jj/ to prevent GC.

Commits with conflicts cannot be represented in Git. They appear in the Git
commit as root directories called.jjconflict-base-*/ and
.jjconflict-side-*/. Note that the purpose of this representation is only to
prevent GC of the relevant trees; the authoritative information is in a
non-standard jj:trees commit header. As long as you use jj
commands to work with them, you won't notice those paths. If, on the other hand,
you use e.g. git switch to check one of them out, you will see those
directories in your working copy. If you then run e.g. jj status, the
resulting snapshot will contain the contents of the first side of the conflict
as well as the .jjconflict-*/ directories, making it look like they
were added to your working copy. You will probably want to run
jj abandon to get back to the state with the unresolved conflicts.

Change IDs are stored in git commit headers as reverse hex encodings. This is
a non-standard header and is not preserved by all git tooling. For example,
the header is preserved by a git commit --amend, but is not preserved through
a rebase operation. GitHub and other major forges seem to preserve them for the
most part. The header is written by default in commits created by jj (since
version 0.30.0) and this functionality can be turned off using the
git.write-change-id-header setting.

Note that git show and git log do not print this header. To verify presence
of a change-id commit header using git itself, use
git-cat-file like this:

shell
git cat-file -p <commit ref>

Git Experts (`docs/git-experts.md`)

Jujutsu for Git experts

People who are proficient with Git often ask what benefit there is to using
Jujutsu. This page explains the practical advantages for Git experts, with
examples showing how common workflows become easier, safer, or faster with
Jujutsu.

Git can be used side-by-side in the same repository

Jujutsu and Git repositories exist in the same directory, so you can use jj
and git side-by-side (this is called colocation). If you find a situation
that's easier with Git, run the git command and return to jj when you're
done (and please make a feature request if there isn't one yet!).

Colocation makes migration easier because you can adopt Jujutsu for the
workflows it improves without losing access to the Git commands and tools you
already know.

The Git index/staging area

Jujutsu does not have an index/staging area as Git does. Because rewriting
commits is quick and easy, it's natural to use commits as a replacement for the
index.

Instead of separate commands just for interacting with the index (git add,
git rm --cached), the commands jj split and jj squash can be used to move
work-in-progress as easily as moving finished work.

sh
# Split the working copy commit into two sequential commits, putting file1 and
# file2 In the first commit
jj split file1 file2

# or interactively select which changes to split
jj split

# Move the changes in file3 into the parent commit
jj squash file3
# or, interactively:
jj squash -i

Automatic and safer history editing

If you frequently amend, reorder, or squash commits, Jujutsu can often perform
the same operations in fewer commands.

Suppose you want to amend an older commit. With Git you might do this in three
steps:

sh
git add file1 file2
git commit --fixup abc
git rebase -i --autosquash

With Jujutsu, you simply squash the changes directly into the commit you want to
amend. All descendants are automatically rebased on top of the amended commit:

sh
jj squash --into abc file1 file2

Undo is more powerful than using the reflog

Git's reflog is powerful, but it's per-ref and can be awkward to use when
multiple refs and operations are involved.

Jujutsu's operation log records the state of the entire repository: Every change
is an operation you can inspect, and you can restore to any earlier state with
one command.

Common uses of the operation log:

  • jj undo reverts the last operation in one step, without needing to figure
    out which ref to reset. You can repeat jj undo to continue stepping backwards
    in time.

  • jj op log -p shows operations with diffs so you can inspect what happened.

  • --at-operation ID lets you run commands as if the repository were in a
    previous state.

The evolution log shows the history of a single change

The Git reflog shows how refs moved over time, but makes it difficult to see how
a particular commit evolved over time. Jujutsu's evolution log ("evolog") shows
exactly this: Each time a change is rewritten, the update is visible in the
evolog.

You can use the evolog to find a previous version, then jj restore to restore
the complete or partial contents to the current version.

`jj absorb` makes it easier to update a patch stack

When amending several commits in a stack of changes, Git requires you to run
git commit --fixup at least once for each commit before running git rebase --autosquash.

jj absorb is useful when you've made small fixes in the working copy and want
them incorporated into recent commits. It automatically moves each change in the
working copy into the previous commit where that line was changed.

Use jj absorb --interactive to choose only some hunks to consider for
absorption. Any hunks that are not selected or cannot be absorbed unambiguously
remain in the source commit.

It doesn't solve all cases: If multiple commits in the stack modified the same
line as was changed in the working copy, it will not move that change. But it
does help the trivial cases, leaving you to decide how to squash the remaining
changes.


Github (`docs/github.md`)

Using Jujutsu with GitHub and GitLab Projects

This guide assumes a basic understanding of either Git or Mercurial.

Basic workflow

The simplest way to start with Jujutsu is to create a stack of commits first.
You will only need to create a bookmark when you need to push the stack to a
remote. There are two primary workflows: using a generated bookmark name or
naming a bookmark.

Using a generated bookmark name

In this example we're letting Jujutsu auto-create a bookmark.

shell
# Start a new commit off of the default bookmark.
$ jj new main
# Refactor some files, then add a description and start a new commit
$ jj commit -m 'refactor(foo): restructure foo()'
# Add a feature, then add a description and start a new commit
$ jj commit -m 'feat(bar): add support for bar'
# Let Jujutsu generate a bookmark name and push that to GitHub. Note that we
# push the working-copy commit's *parent* because the working-copy commit
# itself is empty.
$ jj git push --change @- # or -c for short

Using a named bookmark

In this example, we create a bookmark named bar and then push it to the remote.

shell
# Start a new commit off of the default bookmark.
$ jj new main
# Refactor some files, then add a description and start a new commit
$ jj commit -m 'refactor(foo): restructure foo()'
# Add a feature, then add a description and start a new commit
$ jj commit -m 'feat(bar): add support for bar'
# Create a bookmark so we can push it to GitHub. Note that we created the bookmark
# on the working-copy commit's *parent* because the working copy itself is empty.
$ jj bookmark create bar -r @- # `bar` now contains the previous two commits.
# Set the bookmark to be tracked on the remote.
$ jj bookmark track bar
# Push the bookmark to GitHub (pushes only `bar`)
$ jj git push

While it's possible to create a bookmark in advance and commit on top of it in a
Git-like manner, you will then need to move the bookmark manually when you create
a new commits. Unlike Git, Jujutsu will not do it automatically.

Updating the repository

Jujutsu does not currently have a direct equivalent of git pull, though it may
gain a similar command someday.

In the meantime, updating your branches is a two step process. The first is to
jj git fetch to fetch everything that's happened on your remote. The second is
to rebase all of your branches on top of your main branch with jj rebase -o main. This command's default is the same as passing -b @, so if you have more
than one outstanding branch, you'll need to call jj rebase again with the -b
flag for each of them, or pass multiple -b arguments, one for every branch.

Working in a Git colocated workspaces

After doing jj git init, which
colocates the .jj and
.git directories, Git will be in a detached HEAD state, which is
unusual, as Git mainly works with named branches; jj does not.

In a colocated workspace, every jj command will automatically synchronize
Jujutsu's view of the repo with Git's view. For example, jj commit updates the
HEAD of the Git repository, enabling an incremental migration.

shell
$ nvim docs/tutorial.md
$ # Do some more work.
$ jj commit -m "Update tutorial"
# Create a bookmark on the working-copy commit's parent
$ jj bookmark create doc-update -r @-
$ jj bookmark track doc-update
$ jj git push

Working in a Jujutsu repository

In a Jujutsu repository, the workflow is simplified. If there's no need for
explicitly named bookmarks, you can just generate one for a change. As Jujutsu is
able to create a bookmark for a revision.

shell
$ # Do your work
$ jj commit
$ # Push change "mw", letting Jujutsu automatically create a bookmark called
$ # "push-mwmpwkwknuz"
$ jj git push -c mw

Addressing review comments

There are two workflows for addressing review comments, depending on your
project's preference. Many projects prefer that you address comments by adding
commits to your bookmark[^1]. Some projects (such as Jujutsu and LLVM) instead
prefer that you keep your commits clean by rewriting them and then
force-pushing[^2].

Adding new commits

If your project prefers that you address review comments by adding commits on
top, you can do that by doing something like this:

shell
$ # Create a new commit on top of the `your-feature` bookmark from above.
$ jj new your-feature
$ # Address the comments by updating the code. Then review the changes.
$ jj diff
$ # Give the fix a description and create a new working-copy on top.
$ jj commit -m 'address pr comments'
$ # Update the bookmark to point to the new commit.
$ jj bookmark move your-feature --to @-
$ # Push it to your remote
$ jj git push

Notably, the above workflow creates a new commit for you. The same can be
achieved without creating a new commit.

!!! note

text
If the working copy commit becomes immutable (for example, after
`jj git push`), Jujutsu automatically creates a new working-copy commit on
top. If you need to make further edits before pushing, run `jj new` to
avoid amending them to the previous commit.
shell
$ # Create a new commit on top of the `your-feature` bookmark from above.
$ jj new your-feature
$ # Address the comments by updating the code. Then review the changes.
$ jj diff
$ # Give the fix a description.
$ jj describe -m 'address pr comments'
$ # Update the bookmark to point to the current commit.
$ jj bookmark move your-feature --to @
$ # Push it to your remote
$ jj git push

Rewriting commits

If your project prefers that you keep commits clean, you can do that by doing
something like this:

shell
$ # Create a new commit on top of the second-to-last commit in `your-feature`,
$ # as reviewers requested a fix there.
$ jj new your-feature- # NOTE: the trailing hyphen is not a typo!
$ # Address the comments by updating the code. Then review the changes.
$ jj diff
$ # Squash the changes into the parent commit
$ jj squash
$ # Push the updated bookmark to the remote. Jujutsu automatically makes it a
$ # force push
$ jj git push --bookmark your-feature

The hyphen after your-feature comes from the revset syntax.

Working with other people's bookmarks

By default, jj git clone imports the default remote bookmark (which is usually
main or master), but jj git fetch doesn't import new remote bookmarks to
local bookmarks. This means that if you want to iterate or test another
contributor's bookmark, you'll need to do jj new @<remote> onto it.

If you want to import all remote bookmarks including inactive ones, set
remotes.<name>.auto-track-bookmarks = "*" in the config file. Then you can
specify a contributor's bookmark as jj new instead of jj new @<remote>.

You can find more information on that setting here.

Using GitHub CLI

GitHub CLI will have trouble finding the proper Git repository path in jj repos
that aren't colocated
(see issue #1008). You can configure the $GIT_DIR environment variable to
point it to the right path:

shell
$ GIT_DIR=$(jj git root) gh issue list

You can make that automatic by installing direnv and
defining hooks in a .envrc file in the repository root to configure $GIT_DIR.
Just add this line into .envrc:

shell
export GIT_DIR=$(jj git root)

and run direnv allow to approve it for direnv to run. Then GitHub CLI will
work automatically even in workspaces that aren't colocated so you can execute
commands like gh issue list normally.

Useful Revsets

Log all revisions across all local bookmarks that aren't on the main bookmark nor
on any remote:

shell
$ jj log -r 'bookmarks() & ~(main | remote_bookmarks())'

Log all revisions that you authored, across all bookmarks that aren't on any
remote:

shell
$ jj log -r 'mine() & bookmarks() & ~remote_bookmarks()'

Log all remote bookmarks that you authored or committed to:

shell
$ jj log -r 'remote_bookmarks() & (mine() | committer([email protected]))'

Log all ancestors of the current working copy that aren't on any remote:

shell
$ jj log -r 'remote_bookmarks()..@'

Merge conflicts

For a detailed overview, how Jujutsu handles conflicts, revisit
the tutorial.

[^1]:
This is a GitHub-style review, as GitHub currently only is able to compare
bookmarks.

[^2]:
If you're wondering why we prefer clean commits in this project, see
e.g. this blog post

Using several remotes

It is common to use several remotes when contributing to a shared repository.
For example, "upstream" can designate the remote where the changes will be
merged through a pull-request while "origin" is your private fork of the
project.

shell
$ jj git clone --remote upstream https://github.com/upstream-org/repo
$ cd repo
$ jj git remote add origin [email protected]:your-org/your-repo-fork

This will automatically setup your repository to track the main
bookmark from the upstream repository, typically main@upstream
or master@upstream.

You might want to jj git fetch from "upstream" and to jj git push
to "origin". You can configure the default remotes to fetch from and
push to in your configuration file
(jj config edit --user|repo|workspace):

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

The default for both git.fetch and git.push is "origin".

If you usually work on a project from several computers, you may
configure jj to fetch from both repositories by default, in order to
keep your own bookmarks synchronized through your origin repository:

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

Git push options

jj git push supports passing Git “push options” to the server via
-o/--option. These are forwarded to the remote and interpreted by the hosting
platform, if it supports them. You can repeat -o to send multiple options.

  • Syntax: jj git push -o or jj git push --option
  • Multiple options: jj git push -o foo -o bar=val
  • Quoting: If the option’s value has spaces, wrap it in double quotes.

Support is server-dependent. GitLab supports push options for CI and merge
requests; other platforms may not. Refer to your host’s documentation for the
full list and behavior.

Examples with GitLab push options (see GitLab docs for details):

  • Skip CI for a push:
    shell
    jj git push -o ci.skip
  • Pass CI variables to the created pipeline:
    shell
    jj git push -o 'ci.variable=MAX_RETRIES=10' -o 'ci.variable=MAX_TIME=600'
  • Create a merge request with metadata on push:
    shell
    jj git push \
      -o merge_request.create \
      -o merge_request.target=main \
      -o 'merge_request.title=Add feature X' \
      -o 'merge_request.description=Implements X with tests' \
      -o merge_request.draft
  • Auto-merge when pipeline succeeds and remove the source branch:
    shell
    jj git push \
      -o merge_request.merge_when_pipeline_succeeds \
      -o merge_request.remove_source_branch
  • Add and remove labels:
    shell
    jj git push \
      -o 'merge_request.label=label1' \
      -o 'merge_request.label=label2' \
      -o 'merge_request.unlabel=label3'
  • Assign and unassign users:
    shell
    jj git push \
      -o 'merge_request.assign=user1' \
      -o 'merge_request.assign=user2' \
      -o 'merge_request.unassign=user3'

Glossary (`docs/glossary.md`)

Glossary

Anonymous branch

An anonymous branch is a chain of commits that doesn't necessarily have any
bookmarks pointing to it or to any of its descendants. Unlike Git,
Jujutsu keeps commits on anonymous branches around until they are explicitly
abandoned. Visible anonymous branches are tracked by the view, which
stores a list of heads of such branches.

Author date

The author date of a commit records when the commit's changes were
originally authored. It is set when the commit is first authored and usually
stays the same when the commit is rewritten. For a newly-created
commit, the author date and committer date are usually the
same, but they can differ after history-editing commands such as jj rebase,
jj describe, or jj squash. When using the Git backend, this is
stored as Git's native author date field.

Backend

A backend is an implementation of the storage layer. There is currently only one
production-ready builtin commit backend: the Git backend. The Git backend stores
commits in a Git repository. There are several backends used for testing. Google
also has its own cloud-based backend.

There are also pluggable backends for storing other information than commits,
such as the "operation store backend" for storing
the operation log.

Bookmark

A bookmark is a named pointer to a commit. They are similar to Git's
branches and even more similar to Mercurial's
bookmarks
. See here
for details.

Unlike in Git, there is no concept of a "current bookmark"; bookmarks do not
move when you create a new commit. Bookmarks do automatically follow the
commit if it gets rewritten.

Branch

In the context of jj, the word "branch" usually refers to an anonymous
branch
or, less formally, a branch of the commit "tree"
(which is itself an informal way to refer to the commit graph, parts of which
might resemble a tree even when it's not mathematically a tree).

We also sometimes discuss Git's branches and branches on Git remotes. Locally,
these correspond to bookmarks. In a colocated workspace, each
local Git branch corresponds to a jj bookmark.

Change

A change is a commit as it evolves over time. Changes themselves
don't exist as an object in the data model; only the change ID does. The change
ID is a property of a commit.

Change ID

A change ID is a unique identifier for a change. They are typically
16 bytes long and are often randomly generated. By default, jj log presents
them as a sequence of 12 letters in the k-z range, at the beginning of a line.
These are actually hexadecimal numbers that use "digits" z-k instead of 0-9a-f.

Change offset

Sometimes a change ID might not unambiguously identify a commit, such as if the
commit is hidden or if the change ID is divergent. In these
cases, you can add an offset after the change ID to make it clear which commit
you are referring to, with the most recent commit having an offset of 0. For
instance, the most recent commit with change ID xyz could be referred to as
xyz/0, while the one before it would be xyz/1, and so on.

Commit

A snapshot of the files in the repository at a given point in time (technically
a tree object), together with some metadata. The metadata includes the
author, the committer, the author date, the
committer date, and pointers to the commit's parents. Through
the pointers to the parents, the commits form a
Directed Acyclic Graph (DAG)
.

Note that even though commits are stored as snapshots, they are often treated
as differences between snapshots, namely compared to their parent's snapshot. If
they have more than one parent, then the difference is computed against the
result of merging the parents. For example, jj diff will show the differences
introduced by a commit compared to its parent(s), and jj rebase will apply
those changes onto another base commit.

The word "revision" is used as a synonym for "commit".

Commit ID

A commit ID is a unique identifier for a commit. They are 20 bytes
long when using the Git backend. They are presented in regular hexadecimal
format at the end of the line in jj log, using 12 hexadecimal digits by
default. When using the Git backend, the commit ID is the Git commit ID.

Committer date

The committer date of a commit records when that particular commit
object was created. It is set for a new commit and updated whenever jj
rewrites the commit, such as when rebasing, describing, or squashing
it. For a newly-created commit, the committer date and
author date are usually the same, but they can differ after the
commit has been rewritten. When using the Git backend, this is
stored as Git's native committer date field.

Colocated workspaces

When using the Git backend and the backing Git repository's .git/
directory is a sibling of .jj/, we call the workspace colocated. Most
tools designed for Git can be easily used on such workspaces. jj and git
commands can be used interchangeably.

See here for details.

Conflict

Conflicts can occur in many places. The most common type is conflicts in files.
Those are the conflicts that users coming from other VCSs are usually familiar
with. You can see them in jj status and in jj log (the red "conflict"
label at the end of the line). See here for details.

Conflicts can also occur in bookmarks. For example, if you moved a
bookmark locally, and it was also moved on the remote, then the bookmark will be
in a conflicted state after you pull from the remote.
See here for details.

Similar to a bookmark conflict, when a change is rewritten locally
and remotely, for example, then the change will be in a conflicted state. We
call that a divergent change.

Divergent change

A divergent change is a change that has more than one
visible commit. These changes are displayed with a label of
"divergent" in the log.

A head is a commit with no descendants. The context in which it has no
descendants varies. For example, the heads(X)
revset function returns commits that have no descendants
within the set X itself. The view records which
anonymous heads (heads without a bookmark pointing to them) are visible at a
given operation. Note that this is quite different from Git's
HEAD.

Hidden commits, abandoned commits

See visible commits.

Operation

A snapshot of the visible commits and bookmarks
at a given point in time (technically a view object), together with
some metadata. The metadata includes the username, hostname, timestamps, and
pointers to the operation's parents.

Operation log

The operation log is the
DAG formed by
operation objects, much in the same way that commits form a DAG,
which is sometimes called the "commit history". When operations happen in
sequence, they form a single line in the graph. Operations that happen
concurrently from jj's perspective result in forks and merges in the DAG.

Repository

Basically everything under .jj/, i.e. the full set of operations
and commits.

Remote

A remote is a reference to a copy of your repository. The most common case is
that the remote is hosted on the internet or another network, but local remotes
are also possible. Remotes are useful when working on a project with multiple
collaborators. As Jujutsu is compatible with Git you can use all popular
providers that are also available for Git. For example
GitHub, GitLab or
Codeberg.

Revision

A synonym for Commit.

Revset

Jujutsu supports a functional language for selecting a set of revisions.
Expressions in this language are called "revsets". See here for
details. We also often use the term "revset" for the set of revisions selected
by a revset.

Rewrite

To "rewrite" a commit means to create a new version of that commit with
different contents, metadata (including parent pointers), or both. Rewriting a
commit results in a new commit, and thus a new commit ID, but the
change ID generally remains the same. Some examples of rewriting a
commit would be changing its description or rebasing it. Modifying the working
copy rewrites the working copy commit.

Root commit

The root commit is a virtual commit at the root of every repository. It has a
commit ID consisting of all '0's (00000000...) and a change ID consisting of
all 'z's (zzzzzzzz...). It can be referred to in revsets by the
function root(). Note that our definition of "root commit" is different from
Git's; Git's "root commits" are the first commit(s) in the repository, i.e. the
commits jj log -r 'root()+' will show.

Tree

A tree object represents a snapshot of a directory in the repository. Tree
objects are defined recursively; each tree object only has the files and
directories contained directly in the directory it represents.

Tracked bookmarks and tracking bookmarks

A remote bookmark can be made "tracked" with the jj bookmark track command. This
results in a "tracking" local bookmark that tracks the remote bookmark.

See the bookmarks documentation for a more
detailed definition of these terms.

Visible commits

Visible commits are the commits you see in jj log -r 'all()'. They are the
commits that are reachable from an anonymous head in the view.
Ancestors of a visible commit are implicitly visible.

Intuitively, a visible commit is the "latest version" of a given
change. A commit that is abandoned or rewritten stops
being visible and is labeled as "hidden". Such a commit is still accessible by
its commit ID or the combination of its change ID
and a change offset.

View

A view is a snapshot of bookmarks and their targets, anonymous heads,
and working-copy commits. The anonymous heads define which commits
are visible.

A view object is similar to a tree object in that it represents a
snapshot without history, and an operation object is similar to a
commit object in that it adds metadata and history.

Workspace

A workspace is a working copy and an
associated repository. There can be multiple workspaces for a
single repository. Each workspace has a .jj/ directory, but the
commits and operations will be stored in the initial
workspace; the other workspaces will have pointers to the initial workspace. See
here for details.

This is what Git calls a "worktree".

Working copy

The working copy contains the files you're currently working on. It is
automatically snapshot at the beginning of almost every jj command, thus
creating a new working-copy commit if any changes had
been made in the working copy. Conversely, the working copy is automatically
updated to the state of the working-copy commit at the end of almost every jj
command. See here for details.

This is what Git calls a "working tree".

Working-copy commit

A commit that corresponds to the current state of the working copy. There is
one working-copy commit per workspace. The current working-copy
commits are tracked in the operation log.


Install And Setup (`docs/install-and-setup.md`)

Installation and setup

Installation

Download pre-built binaries for a release

There are pre-built binaries of the last released version of jj for
Windows, Mac, or Linux (the "musl" version should work on all distributions).

Cargo Binstall

If you use cargo-binstall, you
can install binaries of the latest jj release from GitHub as follows:

shell
# Will put the jj binary for the latest release in ~/.cargo/bin by default
cargo binstall --strategies crate-meta-data jj-cli

Without the --strategies option, you may get equivalent binaries that should
be compiled from the same source code.

!!! note

text
If you'd like to install a prerelease version, you'll need to use one of the
options below.

Linux

From Source

First make sure that you have a Rust version >= 1.97.1 and that the
build-essential package is installed by running something like this:

shell
sudo apt-get install build-essential

Now run either:

shell
# To install the *prerelease* version from the main branch
cargo install --git https://github.com/jj-vcs/jj.git --locked --bin jj jj-cli

or:

shell
# To install the latest release
cargo install --locked --bin jj jj-cli

Arch Linux

You can install the jujutsu package from the official extra repository:

shell
pacman -S jujutsu

Or install from the AUR repository with an AUR Helper:

shell
yay -S jujutsu-git

NixOS

If you're on NixOS you can install a released version of jj using the
nixpkgs jujutsu package.

To install a prerelease version, you can use the flake for this repository.
For example, if you want to run jj loaded from the flake, use:

shell
nix run 'github:jj-vcs/jj'

You can also add this flake url to your system input flakes. Or you can
install the flake to your user profile:

shell
# Installs the prerelease version from the main branch
nix profile install 'github:jj-vcs/jj'

Homebrew

If you use Homebrew, you can run:

shell
# Installs the latest release
brew install jj

Gentoo Linux

dev-vcs/jj is available in the GURU repository.
Details on how to enable the GURU repository can be found here.

Once you have synced the GURU repository, you can install dev-vcs/jj via Portage:

shell
emerge -av dev-vcs/jj

openSUSE Tumbleweed

jujutsu can be installed from the official openSUSE-Tumbleweed-Oss repository:

shell
zypper install jujutsu

Fedora Linux

jujutsu has an unofficial COPR
repo
for Fedora:

shell
sudo dnf copr enable aldantanneo/jj-vcs
sudo dnf install jj-cli

Mac

From Source

First make sure that you have a Rust version >= 1.97.1. You may also need to run:

shell
xcode-select --install

Now run either:

shell
# To install the *prerelease* version from the main branch
cargo install --git https://github.com/jj-vcs/jj.git \
     --locked --bin jj jj-cli

or:

shell
# To install the latest release
cargo install --locked --bin jj jj-cli

Homebrew

If you use Homebrew, you can run:

shell
# Installs the latest release
brew install jj

MacPorts

You can also install jj via the MacPorts jujutsu
port
:

shell
# Installs the latest release
sudo port install jujutsu

Windows

First make sure that you have a Rust version >= 1.97.1. Now run either:

shell
# To install the *prerelease* version from the main branch
cargo install --git https://github.com/jj-vcs/jj.git --locked --bin jj jj-cli

or:

shell
# To install the latest release
cargo install --locked --bin jj jj-cli

via winget:

shell
# To install the latest release via winget
winget install jj-vcs.jj

via scoop:

shell
# To install the latest release via scoop
scoop install main/jj

FreeBSD

devel/jujutsu is available in the ports.

Install pre-built pkg:

shell
# To install the latest release via pkg
pkg install jujutsu

Install from building the port:

shell
# To install the latest release via ports
cd /usr/ports/devel/jujutsu/ && make install clean

Runtime Requirements

You will need git 2.42.0 or above. On older systems (e.g. Debian 11, Ubuntu
22.04) you will need to upgrade Git.

Initial configuration

You may want to configure your name and email so commits are made in your name.

shell
$ jj config set --user user.name "Martin von Zweigbergk"
$ jj config set --user user.email "[email protected]"

Command-line completion

Jujutsu provides 2 different command-line completion scripts.

Standard completions

The standard completion script provides completions for jj subcommands and
options.

Dynamic completions

The dynamic completion script provides completions for jj subcommands and
options, as well as additional completions, including bookmarks,
aliases, revisions, operations and files. Dynamic completions can be context
aware, for example they respect the global flags --repository and
--at-operation as well as some command-specific ones like --revision,
--from, and --to.

Dynamic completions are not the default/only option since the underlying engine
is still labeled unstable. We expect to transition to them as the
default once the engine is stabilized. Please let us know if you encounter any
issues with dynamic completions.

!!! info "Which completion script should I use?"

text
Generally, dynamic completions provide a much better completion experience.
Although the underlying engine is deemed unstable, there have not been many
issues in practice. Dynamic completions are the preferred option for many
contributors and users.

We recommend using the dynamic completion script, and falling back to the
standard completion script if there are any issues.

Bash

=== "Standard"

text
```shell
source <(jj util completion bash)
```

=== "Dynamic"

text
```shell
source <(COMPLETE=bash jj)
```

Zsh

=== "Standard"

text
```shell
autoload -U compinit
compinit
source <(jj util completion zsh)
```

=== "Dynamic"

text
```shell
source <(COMPLETE=zsh jj)
```

Fish

=== "Standard"

text
```shell
jj util completion fish | source
```

=== "Dynamic"

text
!!! note

    No configuration is required with fish >= 4.0.2 which loads dynamic completions by default.

```shell
COMPLETE=fish jj | source
```

Nushell

=== "Standard"

text
```nu
jj util completion nushell | save -f completions-jj.nu
use completions-jj.nu *  # Or `source completions-jj.nu`
```

Powershell

=== "Standard"

text
```powershell
jj util completion power-shell | Out-String | Invoke-Expression
```

=== "Dynamic"

text
```powershell
$env:COMPLETE = "powershell"
jj | Out-String | Invoke-Expression
Remove-Item Env:\COMPLETE
```

Insert the above into your $PROFILE file
(usually $HOME\Documents\PowerShell\Microsoft.PowerShell_profile.ps1).

!!! note

text
Note that to execute scripts in PowerShell on Windows, including `$PROFILE`,
the [execution policy][ExecutionPolicies] needs to be set to `RemoteSigned`
at minimum.

Operation Log (`docs/operation-log.md`)

Operation log

Introduction

Jujutsu records each operation that modifies the repo in the "operation log".
You can see the log with jj op log. Each operation object contains a snapshot
of how the repo looked at the end of the operation. We call this snapshot a
"view" object. The view contains information about where each bookmark, tag, and
Git ref (in Git-backed repos) pointed, as well as the set of heads in the repo,
and the current working-copy commit in each workspace. The operation object also
(in addition to the view) contains pointers to the operation(s) immediately
before it, as well as metadata about the operation, such as timestamps,
username, hostname, description.

The operation log allows you to undo operations one-by-one (jj undo) or even
revert a specific one which isn't the most recent operation (jj op revert). It
also lets you restore the entire repo to the way it looked at an earlier point
(jj op restore).

When referring to operations, you can use @ to represent the current
operation.

The following operators are supported:

  • x-: Parents of x (e.g. @-)
  • x+: Children of x

Divergent operations

One benefit of the operation log (and the reason for its creation) is that it
allows lock-free concurrency -- you can run concurrent jj commands without
corrupting the repo, even if you run the commands on different machines that
access the repo via a distributed file system (as long as the file system
guarantees that a write is only visible once previous writes are visible). When
you run a jj command, it will start by loading the repo at the latest
operation. It will not see any changes written by concurrent commands. If there
are conflicts, you will be informed of them by subsequent jj st and/or
jj log commands.

As an example, let's say you had started editing the description of a change and
then also update the contents of the change (maybe because you had forgotten the
editor). When you eventually close your editor, the command will succeed and
e.g. jj log will indicate that the change has diverged.

Loading an old version of the repo

The top-level --at-op/--at-operation option allows you to load the repo at a
specific operation. This can be useful for understanding how your repo got into
the current state. It can be even more useful for understanding why someone
else's repo got into its current state.

When you use --at-op, the automatic snapshotting of the working copy will not
take place. When referring to a revision with the @ symbol (as many commands
do by default), that will resolve to the working-copy commit recorded in the
operation's view (which is actually how it always works -- it's just the
snapshotting that's skipped with --at-op).

As a top-level option, --at-op can be passed to any command. However, you
will typically only want to run read-only commands. For example, jj log,
jj st, and jj diff all make sense. It's still possible to run e.g.
jj --at-op=<some operation ID> describe. That's equivalent to having started
jj describe back when the specified operation was the most recent operation
and then let it run until now (which can be done for that particular command by
not closing the editor). There's practically no good reason to do that other
than to simulate concurrent commands.


Paying employers and their contributing employees

This is the list of companies paying for contributions to Jujutsu. For each
company, all employees who have contributed are listed, whether it's part of
their job to contribute or not.

The purpose of this list is to make it easy to spot conflicts of interest, such
as when an employee approves changes from another employee of the same company.
See contribution docs for details on this policy.

East River Source Control

  • benbrittain
  • bts
  • ConnerPetzold
  • davidbarsky
  • ilyagr
  • martinvonz
  • steveklabnik
  • thoughtpolice

Alphabet/Google

  • 06393993
  • 2079884FDavid
  • aabmass
  • ajaspers
  • algmyr
  • AM5800
  • aspotashev
  • brychanrobot
  • daehyeok
  • drieber
  • durin42
  • edre
  • emesterhazy
  • essiene
  • ffyuanda
  • finque
  • honglooker
  • hooper
  • incognito124
  • jonathantanmy
  • josephlou5
  • kevincliao
  • kiranani
  • lukegb
  • matts1
  • matttproud
  • michaelchirico
  • mlcui-corp
  • orthros
  • pmandloi28
  • prattmic
  • qfel
  • Ralith
  • rdamazio
  • sbarfurth
  • solson
  • spectral54
  • steadmon
  • tbodt
  • vaghinak-vardanyan
  • zygoloid

IMC Trading

  • freundTech
  • HoelzelJon

Similar tools:

  • git-branchless: Helps you use a
    branchless workflow in your Git repo. Supports anonymous branching, undo,
    and faster rebase (git move). Under heavy development and quickly gaining
    new features.
  • Sapling: A heavily modified fork of
    Mercurial developed and used at Meta. It
    is compatible with Git, has undo functionality, and
    a graphical interface.
    See how it is different from Jujutsu.
  • GitUp: A Mac-only GUI for Git. Like Jujutsu, supports
    undo and restoring the repo to an earlier snapshot. Backed by its
    GitUpKit library.
  • Gitless: Another attempt at providing a simpler
    interface for Git. Like Jujutsu, does not have an "index"/"staging area"
    concept. Also doesn't move the working-copy changes between branches (which
    we do simply as a consequence of making the working copy a commit).
  • Breezy: Another VCS that's similar in that it
    has multiple storage backends, including its own format as well as .git
    support.
  • GitButler: A Git client that works with multiple
    virtual branches simultaneously, first-class conflicts, and operations history.

Releasing (`docs/releasing.md`)

How to do a release

Update changelog and Cargo versions

Send a PR similar to https://github.com/jj-vcs/jj/pull/7954. Feel free to
copy-edit the changelog in order to:

  • Populate "Release highlights" if relevant
  • Put more important items first so the reader doesn't miss them
  • Make items consistent when it comes to language and formatting
  • Catch any misplaced changelog items by looking at the CHANGELOG diff.

To get the CHANGELOG diff, you can run

shell
jj log -r 'heads(tags())'  # Check that this shows the previous version
jj diff --from 'heads(tags())' --to main CHANGELOG.md

Make sure to add a corresponding reference link at the bottom of the
CHANGELOG for the new version's tag. It should be the GitHub URL comparing
the previous version tag with the new version tag
(e.g. https://github.com/jj-vcs/jj/compare/v0.32.0...v0.33.0). Also update the
[unreleased] link accordingly, comparing the newest version against HEAD.

Producing the list of contributors is a bit annoying. The current suggestion is
to run something like this:

shell
root=$(jj log -G -r 'heads(tags(glob:"v*.*.*") & ::trunk())' -T commit_id)
filter='
map(.commits[] | select(.author.login | (. != null and endswith("[bot]") | not)))
  | unique_by(if .author.login != null then .author.login else .author.email end)
  | map(if .author.login != null
        then "* \(.commit.author.name) (@\(.author.login))"
        else "* \(.commit.author.name)"
        end)
  | .[]
'
gh api "/repos/jj-vcs/jj/compare/$root...main" --paginate | jq -sr "$filter" | sort -f

https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#compare-two-commits

Alternatively, the list can be produced locally:

shell
jj log -G -r 'heads(tags())..main' -T '"* " ++ author ++ "\n"' | sort -fu

Then try to find the right GitHub username for each person and copy their name
and username from the GitHub page for the person
(e.g. https://github.com/martinvonz).

Get the PR through review and get it merged as usual.

Create a tag and a GitHub release

  1. Go to https://github.com/jj-vcs/jj/releases and click "Draft a new release"
  2. Click "Choose a tag" and enter "v0.<number>.0" (e.g. "v0.26.0") to create a
    new tag
  3. Click "Target", then "Recent commits", and select the commit from your merged
    PR
  4. Use the name (e.g. "v0.26.0") as "Release title". Paste the changelog entries
    into the message body
  5. Check "Create a discussion for this release"
  6. Click "Publish release"

Publish the crates to crates.io

Go to a terminal and create a new clone of the repo [^1]:

shell
cd $(mktemp -d)
jj git clone https://github.com/jj-vcs/jj
cd jj
jj new v0.<number>.0

Publish each crate:

shell
cargo publish --workspace

[^1]: We recommend publishing from a new clone because cargo publish will
archive ignored files if they match the patterns in [include]
(example),
so it's a security risk to run it in an existing clone where you may have
left sensitive content in an ignored file.


Revsets (`docs/revsets.md`)

Revsets

Jujutsu supports a functional language for selecting a set of revisions.
Expressions in this language are called "revsets" (the idea comes from
Mercurial). The language
consists of symbols, operators, and functions.

Most jj commands accept a revset (or multiple). Many commands, such as
jj edit <revset> expect the revset to resolve to a single commit; it is an
error to pass a revset that resolves to more than one commit (or zero commits)
to such commands.

The words "revisions" and "commits" are used interchangeably in this document.

Hidden revisions

Most revsets search only the visible commits.
Other commits are only included if you explicitly mention them (e.g. by commit
ID, <name>@<remote> symbol, or at_operation() function).

If hidden commits are specified, their ancestors also become available to the
search space. They are included in all(), x.., ~x, etc., but not in
..visible_heads(), etc. For example, hidden_id | all() is equivalent to
hidden_id | ::(hidden_id | visible_heads()).

Symbols

The @ expression refers to the working copy commit in the current workspace.
Use <workspace name>@ to refer to the working-copy commit in another
workspace. Use <name>@<remote> to refer to a remote-tracking tag / bookmark.

A full commit ID refers to a single commit. A unique prefix of the full commit
ID can also be used. It is an error to use a non-unique prefix.

A full change ID refers to a visible commit with that change ID. A unique prefix
of the full change ID can also be used. It is an error to use a non-unique
prefix or a divergent change ID. To refer to a
hidden commit or divergent change, a
change offset can be added using <change ID>/<offset> syntax.

Use single or double quotes to prevent a symbol from being
interpreted as an expression. For example, "x-" is the symbol x-, not the
parents of symbol x. Taking shell quoting into account, you may need to use
something like jj log -r '"x-"'.

Priority

Jujutsu attempts to resolve a symbol in the following order:

  1. Tag name
  2. Bookmark name
  3. Commit ID or change ID

To override the priority, use the appropriate revset function. For
example, to resolve abc as a commit ID even if there happens to be a bookmark
by the same name, use commit_id(abc). This is particularly useful in scripts.

Operators

The following operators are supported. x and y below can be any revset, not
only symbols.

Operators are listed in order of binding power from strongest to weakest, e.g.
x | y & z is interpreted as x | (y & z) since & has stronger binding power
than |. Infix operators of the same binding power are parsed from left to
right, e.g. x ~ y & z is interpreted as (x ~ y) & z rather than x ~ (y & z).

As seen above, parentheses can be used to control evaluation order, e.g. (x & y) | z or x & (y | z).

    • f(x): Function call.
    • x-: Parents of x, can be empty.
    • x+: Children of x, can be empty.
    • p:x: String/date pattern or pattern alias named p.
    • x::: Descendants of x, including the commits in x itself. Equivalent to
      x::visible_heads() if no hidden revisions are mentioned.
    • x..: Revisions that are not ancestors of x. Equivalent to ~::x, and
      x..visible_heads() if no hidden revisions are mentioned.
    • ::x: Ancestors of x, including the commits in x itself. Shorthand for
      root()::x.
    • ..x: Ancestors of x, including the commits in x itself, but excluding
      the root commit. Shorthand for root()..x. Equivalent to ::x ~ root().
    • x::y: Descendants of x that are also ancestors of y. Equivalent
      to x:: & ::y. This is what git log calls --ancestry-path x..y.
    • x..y: Ancestors of y that are not also ancestors of x. Equivalent to
      ::y ~ ::x. This is what git log calls x..y (i.e. the same as we call it).
      Note that this is not a "path" between x and y in the commit graph -- x
      and y do not need to be related by ancestry.
    • ::: All visible commits in the repo. Equivalent to all(), and
      root()::visible_heads() if no hidden revisions are mentioned.
    • ..: All visible commits in the repo, but excluding the root commit.
      Equivalent to ~root(), and root()..visible_heads() if no hidden revisions
      are mentioned.
    • ~x: Revisions that are not in x.
    • x & y: Revisions that are in both x and y.
    • x ~ y: Revisions that are in x but not in y.
    • x | y: Revisions that are in either x or y (or both).

Note: The .. operator does not distribute over union (|) on its left
side. For example, (A | B).. is not equivalent to A.. | B... The
expression (A | B).. means "commits that are not ancestors of A and not
ancestors of B", while A.. | B.. means "commits that are not ancestors of A
or not ancestors of B". In fact, (A | B).. = A.. & B... See the examples
below for concrete illustrations.

??? examples

text
Given this history:
```
D
|\
B C
|/
A
|
root()
```

**Operator** `x-`

* `D-` ⇒ `{C,B}`
* `B-` ⇒ `{A}`
* `A-` ⇒ `{root()}`
* `root()-` ⇒ `{}` (empty set)
* `none()-` ⇒ `{}` (empty set)
* `(D|A)-` ⇒ `{C,B,root()}`
* `(C|B)-` ⇒ `{A}`

**Operator** `x+`

* `D+` ⇒ `{}` (empty set)
* `B+` ⇒ `{D}`
* `A+` ⇒ `{B,C}`
* `root()+` ⇒ `{A}`
* `none()+` ⇒ `{}` (empty set)
* `(C|B)+` ⇒ `{D}`
* `(B|root())+` ⇒ `{D,A}`

**Operator** `x::`

* `D::` ⇒ `{D}`
* `B::` ⇒ `{D,B}`
* `A::` ⇒ `{D,C,B,A}`
* `root()::` ⇒ `{D,C,B,A,root()}`
* `none()::` ⇒ `{}` (empty set)
* `(C|B)::` ⇒ `{D,C,B}`

**Operator** `x..`

* `D..` ⇒ `{}` (empty set)
* `B..` ⇒ `{D,C}` (note that, unlike `B::`, this includes `C`)
* `A..` ⇒ `{D,C,B}`
* `root()..` ⇒ `{D,C,B,A}`
* `none()..` ⇒ `{D,C,B,A,root()}`
* `(C|B)..` ⇒ `{D}`

**Operator** `::x`

* `::D` ⇒ `{D,C,B,A,root()}`
* `::B` ⇒ `{B,A,root()}`
* `::A` ⇒ `{A,root()}`
* `::root()` ⇒ `{root()}`
* `::none()` ⇒ `{}` (empty set)
* `::(C|B)` ⇒ `{C,B,A,root()}`

**Operator** `..x`

* `..D` ⇒ `{D,C,B,A}`
* `..B` ⇒ `{B,A}`
* `..A` ⇒ `{A}`
* `..root()` ⇒ `{}` (empty set)
* `..none()` ⇒ `{}` (empty set)
* `..(C|B)` ⇒ `{C,B,A}`

**Operator** `x::y`

* `D::D` ⇒ `{D}`
* `B::D` ⇒ `{D,B}` (note that, unlike `B..D`, this includes `B` and excludes `C`)
* `B::C` ⇒ `{}` (empty set) (note that, unlike `B..C`, this excludes `C`)
* `A::D` ⇒ `{D,C,B,A}`
* `root()::D` ⇒ `{D,C,B,A,root()}`
* `none()::D` ⇒ `{}` (empty set)
* `D::B` ⇒ `{}` (empty set)
* `(C|B)::(C|B)` ⇒ `{C,B}`

**Operator** `x..y`

* `D..D` ⇒ `{}` (empty set)
* `B..D` ⇒ `{D,C}` (note that, unlike `B::D`, this includes `C` and excludes `B`)
* `B..C` ⇒ `{C}` (note that, unlike `B::C`, this includes `C`)
* `A..D` ⇒ `{D,C,B}`
* `root()..D` ⇒ `{D,C,B,A}`
* `none()..D` ⇒ `{D,C,B,A,root()}`
* `D..B` ⇒ `{}` (empty set)
* `(C|B)..(C|B)` ⇒ `{}` (empty set)

**Non-distributivity of `..` over union (left side)**

Using the same graph, observe that `(C|B)..` ⇒ `{D}`, but:
* `C..` ⇒ `{D,B}`
* `B..` ⇒ `{D,C}`
* `C.. | B..` ⇒ `{D,C,B}`
* `C.. & B..` ⇒ `{D}`

So `(C|B)..` ≠ `C.. | B..`, but `(C|B).. = C.. & B..`. The `..` operator
converts union to intersection on its left side.

Functions

You can also specify revisions by using functions. Some functions take other
revsets (expressions) as arguments.

??? note "Function argument syntax"

text
In this documentation, optional arguments are indicated with square
brackets like `[arg]`. Some arguments also have an optional label which can
be used to specify that argument without specifying all previous arguments.

For instance, `remote_bookmarks([name_pattern], [[remote=]remote_pattern])`
indicates that all of the following usages are valid:

* `remote_bookmarks()`
* `remote_bookmarks("main")`
* `remote_bookmarks("main", "origin")`
* `remote_bookmarks("main", remote="origin")`
* `remote_bookmarks(remote="origin")`
  • parents(x, [depth]): parents(x) is the same as x-.
    parents(x, depth) returns the parents of x at the given depth. For
    instance, parents(x, 3) is equivalent to x---.

  • children(x, [depth]): children(x) is the same as x+.
    children(x, depth) returns the children of x at the given depth. For
    instance, children(x, 3) is equivalent to x+++.

  • ancestors(x, [depth]): ancestors(x) is the same as ::x.
    ancestors(x, depth) returns the ancestors of x limited to the given
    depth.

  • descendants(x, [depth]): descendants(x) is the same as x::.
    descendants(x, depth) returns the descendants of x limited to the given
    depth.

  • first_parent(x, [depth]): first_parent(x) is similar to parents(x), but
    for merges, it only returns the first parent instead of returning all parents.
    The depth argument also works similarly, so first_parent(x, 2) is
    equivalent to first_parent(first_parent(x)).

  • first_ancestors(x, [depth]): Similar to ancestors(x, [depth]), but only
    traverses the first parent of each commit. In Git, the first parent of a merge
    commit is conventionally the branch into which changes are being merged, so
    first_ancestors() can be used to exclude changes made on other branches.

  • reachable(srcs, domain): All commits reachable from srcs, traversing all
    parent and child edges, such that the entire path is within domain. This is
    useful for finding all related commits in a branch or feature without
    traversing outside a defined scope. For example, reachable(@, mutable())
    returns the stack of commits you are working on.

  • connected(x): Same as x::x. Useful when x includes several commits.

  • all(): All visible commits and ancestors of commits explicitly mentioned.

  • none(): No commits. This function is rarely useful; it is provided for
    completeness.

  • change_id(prefix): Commits with the given change ID prefix. If the specified
    change is divergent, this resolves to multiple commits. It is an error to use a
    non-unique prefix. Unmatched prefix isn't an error.

  • commit_id(prefix): Commits with the given commit ID prefix. It is an error
    to use a non-unique prefix. Unmatched prefix isn't an error.

  • bookmarks([pattern]): All local bookmark targets. If pattern is specified,
    this selects the bookmarks whose name match the given string
    pattern
    . For example, bookmarks(*push*) would match the
    bookmarks push-123 and repushed but not the bookmark main. If a bookmark
    is in a conflicted state, all its possible targets are included.

  • remote_bookmarks([name_pattern], [[remote=]remote_pattern]): All remote
    bookmarks targets across all remotes. If just the name_pattern is specified,
    the bookmarks whose names match the given string pattern
    across all remotes are selected. If both name_pattern and remote_pattern
    are specified, the selection is further restricted to just the remotes whose
    names match remote_pattern.

    For example, remote_bookmarks(*push*, *ri*) would match the bookmarks
    push-123@origin and repushed@private but not push-123@upstream or
    main@origin or main@upstream. If a bookmark is in a conflicted state, all
    its possible targets are included.

    Git-tracking bookmarks are excluded by default. Use remote="git" or
    remote="*" to select bookmarks including @git ones.

  • tracked_remote_bookmarks([name_pattern], [[remote=]remote_pattern]): All
    targets of tracked remote bookmarks. Supports the same optional arguments as
    remote_bookmarks().

  • untracked_remote_bookmarks([name_pattern], [[remote=]remote_pattern]): All
    targets of untracked remote bookmarks. Supports the same optional arguments as
    remote_bookmarks().

  • tags([pattern]): All tag targets. If pattern is specified, this selects
    the tags whose name match the given string pattern. For
    example, tags(*v1*) would match the tags v123 and rev1 but not the tag
    v2. If a tag is in a conflicted state, all its possible targets are
    included.

  • remote_tags([name_pattern], [[remote=]remote_pattern]): All remote tags
    targets across all remotes. See remote_bookmarks() for arguments.

  • tracked_remote_tags([name_pattern], [[remote=]remote_pattern]): All targets
    of tracked remote tags. Supports the same optional arguments as
    remote_tags().

  • untracked_remote_tags([name_pattern], [[remote=]remote_pattern]): All
    targets of untracked remote tags. Supports the same optional arguments as
    remote_tags().

  • visible_heads(): All visible heads (same as heads(all()) if no hidden
    revisions are mentioned).

  • root(): The virtual commit that is the oldest ancestor of all other commits.

  • heads(x): Commits in x that are not ancestors of other commits in x.
    Equivalent to x ~ ::x-. Note that this is different from
    Mercurial's heads(x)
    function, which is equivalent to x ~ x-.

  • roots(x): Commits in x that are not descendants of other commits in x.
    Equivalent to x ~ x+::. Note that this is different from
    Mercurial's roots(x)
    function, which is equivalent to x ~ x+.

  • latest(x, [count]): Latest count commits in x, based on committer
    timestamp. The default count is 1.

  • fork_point(x): The fork point of all commits in x. The fork point is the
    common ancestor(s) of all commits in x which do not have any descendants
    that are also common ancestors of all commits in x. It is equivalent to
    the revset heads(::x_1 & ::x_2 & ... & ::x_N), where x_{1..N} are commits
    in x. If x resolves to a single commit, fork_point(x) resolves to x.

    fork_point(x | y) is what git merge-base x y calculates (but unlike git merge-base --fork-point, the fork_point() function does not take a
    bookmark's history into account).

  • merge_point(x): The merge point of all commits in x. Similar to the fork
    point, the merge point is the common descendant(s) of all commits in x which
    do not have any ancestors that are also common descendants of all commits in
    x. It is equivalent to the revset roots(x_1:: & x_2:: & ... & x_N::),
    where x_{1..N} are commits in x. If x resolves to a single commit,
    merge_point(x) resolves to x.

  • bisect(x): Finds commits in the input set for which about half of the input
    set are descendants. The current implementation deals somewhat poorly with
    non-linear history.

  • exactly(x, count): Evaluates x, and errors if it is not of exactly size
    count. Otherwise, returns x. This is useful in particular with count=1
    when you want to ensure that some revset expression has exactly one target.

  • merges(): Merge commits.

  • forks(): Fork commits, i.e. those with more than 1 child.

  • description(pattern): Commits that have a description matching the given
    string pattern.

    A non-empty description is usually terminated with newline character. For
    example, description("") matches commits without description, and
    description("foo\n") matches commits with description "foo\n".

  • subject(pattern): Commits that have a subject matching the given string
    pattern
    . A subject is the first line of the description
    (without newline character.)

  • author(pattern): Commits with the author's name or email matching the given
    string pattern. Equivalent to author_name(pattern) | author_email(pattern).

  • author_name(pattern): Commits with the author's name matching the given
    string pattern.

  • author_email(pattern): Commits with the author's email matching the given
    string pattern.

  • author_date(pattern): Commits with author dates
    matching the specified date pattern.

  • mine(): Commits where the author's email matches the email of the current
    user. Equivalent to author_email(exact-i:)

  • committer(pattern): Commits with the committer's name or email matching the
    given string pattern. Equivalent to
    committer_name(pattern) | committer_email(pattern).

  • committer_name(pattern): Commits with the committer's name matching the
    given string pattern.

  • committer_email(pattern): Commits with the committer's email matching the
    given string pattern.

  • committer_date(pattern): Commits with
    committer dates matching the specified date
    pattern
    .

  • signed(): Commits that are cryptographically signed.

  • empty(): Commits modifying no files. This also includes merges() without
    user modifications and root().

  • files(expression): Commits modifying paths matching the given fileset
    expression
    .

    Paths are relative to the directory jj was invoked from. A directory name
    will match all files in that directory and its subdirectories.

    For example, files(foo) will match files foo, foo/bar, foo/bar/baz.
    It will not match foobar or bar/foo.

    Some file patterns might need quoting because the expression must also be
    parsable as a revset. For example, . has to be quoted in files(".").

  • diff_lines(text, [files]): Commits containing diffs matching the given
    text pattern line by line.

    The search paths can be narrowed by the files expression. All modified files
    are scanned by default, but it is likely to change in future version to
    respect the command line path arguments.

    For example, diff_lines("*TODO*", "src") will search revisions where "TODO"
    is added to or removed from files under "src".

  • diff_lines_added(text, [files]): like diff_lines() above, but matches only
    the "added" side of the diff.

  • diff_lines_removed(text, [files]): like diff_lines() above, but matches
    only the "removed" side of the diff.

  • conflicts(): Commits that have files in a conflicted state.

  • divergent(): Commits that are divergent.

  • present(x): Same as x, but evaluated to none() if any of the commits
    in x doesn't exist (e.g. is an unknown bookmark name.)

  • coalesce(revsets...): Commits in the first revset in the list of revsets
    which does not evaluate to none(). If all revsets evaluate to none(), then
    the result of coalesce will also be none().

  • working_copies(): The working copy commits across all the workspaces.

  • at_operation(op, x): Evaluates x at the specified operation. For
    example, at_operation(@-, visible_heads()) will return all heads which were
    visible at the previous operation.

    Since at_operation(op, x) brings all commits that were visible at the
    operation to the search space, at_operation(op, x) | all() is equivalent to
    at_operation(op, x) | ::(at_operation(op, x | visible_heads()) | visible_heads()).

??? examples

text
Given this history:
```
E
| D
|/|
B C
|/
A
|
root()
```

**function** `reachable()`

`reachable(srcs, domain)` finds all commits reachable from `srcs` by
following parent or child edges, but limited to commits within `domain`.

* `reachable(E, A..)` ⇒ `{E,D,C,B}` — All commits after A are reachable
* `reachable(E, B..)` ⇒ `{E}` — E is isolated: its only edge (to B) leaves the domain
* `reachable(C, B..)` ⇒ `{D,C}` — C and D are connected; the C→A edge leaves the domain
* `reachable(D, B..)` ⇒ `{D,C}` — Same result: D reaches C, but D→B leaves the domain
* `reachable(A, A..)` ⇒ `{}` (empty set) — A is not in domain `A..`, so it's ignored

**function** `connected()`

* `connected(E|A)` ⇒ `{E,B,A}`
* `connected(D|A)` ⇒ `{D,C,B,A}`
* `connected(A)` ⇒ `{A}`

**function** `heads()`

* `heads(E|D)` ⇒ `{E,D}`
* `heads(E|C)` ⇒ `{E,C}`
* `heads(E|B)` ⇒ `{E}`
* `heads(E|A)` ⇒ `{E}`
* `heads(A)` ⇒ `{A}`

**function** `roots()`

* `roots(E|D)` ⇒ `{E,D}`
* `roots(E|C)` ⇒ `{E,C}`
* `roots(E|B)` ⇒ `{B}`
* `roots(E|A)` ⇒ `{A}`
* `roots(A)` ⇒ `{A}`

**function** `fork_point()`

* `fork_point(E|D)` ⇒ `{B}`
* `fork_point(E|C)` ⇒ `{A}`
* `fork_point(E|B)` ⇒ `{B}`
* `fork_point(E|A)` ⇒ `{A}`
* `fork_point(D|C)` ⇒ `{C}`
* `fork_point(D|B)` ⇒ `{B}`
* `fork_point(B|C)` ⇒ `{A}`
* `fork_point(A)` ⇒ `{A}`
* `fork_point(none())` ⇒ `{}`

String patterns

Functions that perform string matching support the following pattern syntax (the
quotes are optional).

By default, "string" is parsed as a glob: pattern.

  • exact:"string": Matches strings exactly equal to string.
  • glob:"pattern": Matches strings with Unix-style shell wildcard
    pattern
    .
  • regex:"pattern": Matches substrings with regular
    expression pattern
    .
  • substring:"string": Matches strings that contain string.

You can append -i after the kind to match case‐insensitively (e.g.
glob-i:"fix*jpeg*").

String patterns can be combined by using logical operators
(e.g. bookmarks(~glob:"ci/*")):

  • ~x: Matches everything but x.
  • x & y: Matches both x and y.
  • x ~ y: Matches x but not y.
  • x | y: Matches either x or y (or both).

Date patterns

Functions that perform date matching support the following pattern syntax:

  • after:"string": Matches dates exactly at or after the given date.
  • before:"string": Matches dates before, but not including, the given date.

Date strings can be specified in several forms, including:

  • 2024-02-01
  • 2024-02-01T12:00:00
  • 2024-02-01T12:00:00-08:00
  • 2024-02-01 12:00:00
  • 2 days ago
  • 5 minutes ago
  • yesterday
  • yesterday 5pm
  • yesterday 10:30
  • yesterday 15:30

Aliases

New symbols, functions, and <name>:<value> patterns can be defined in the
config file, by using any combination of the predefined symbols/functions and
other aliases.

Alias functions can be overloaded by the number of parameters. However, builtin
function will be shadowed by name, and can't co-exist with aliases.

For example:

toml
[revset-aliases]
HEAD = '@-'
'user()' = 'user("[email protected]")'
'user(x)' = 'author(x) | committer(x)'
'grep:x' = 'description(regex:x)'

Alias descriptions

Alias descriptions can be surfaced in shell completions by defining the alias
as a table with .doc and .definition properties. For example:

toml
[revset-aliases]
HEAD = { definition = '@-', doc = 'The parent of the working-copy commit' }

You can also use the dotted key syntax:

toml
[revset-aliases]
HEAD.definition = '@-'
HEAD.doc = 'The parent of the working-copy commit'

Built-in Aliases

The following aliases are built-in and used for certain operations. These functions
are defined as aliases in order to allow you to overwrite them as needed.
See revsets.toml
for a comprehensive list.

  • trunk(): Resolves to the head commit for the default bookmark of the default
    remote, or the remote named upstream or origin. This is set at the
    repository level upon initialization of a Jujutsu repository.

    If the default bookmark cannot be resolved during initialization, the default
    global configuration tries the bookmarks main, master, and trunk on the
    upstream and origin remotes. If more than one potential trunk commit
    exists, the newest one is chosen. If none of the bookmarks exist, the revset
    evaluates to root().

    You can override this as appropriate. If you do, make sure it
    always resolves to exactly one commit. For example:

    toml
    [revset-aliases]
    'trunk()' = 'your-bookmark@your-remote'
  • builtin_log(): Resolves to present(@) | ancestors(immutable_heads().., 2) | trunk(). It is used as the default value
    for revsets.log, which is the set of revisions shown by jj log if no
    revisions or paths are specified.

  • builtin_immutable_heads(): Resolves to trunk() | tags() | untracked_remote_bookmarks() | untracked_remote_tags(). It is used as the
    default definition for immutable_heads() below. It is not recommended to
    redefine this alias. Prefer to redefine immutable_heads() instead.

  • immutable_heads(): The heads of the set of immutable commits (not "heads
    that are immutable"). Resolves to trunk() | tags() | untracked_remote_bookmarks() | untracked_remote_tags() by default. It is
    actually defined as builtin_immutable_heads(), and can be overridden as
    required. The full set of immutable commits is ::immutable_heads() (i.e.,
    immutable()). See here for details.

  • immutable(): The set of commits that jj treats as immutable. This is
    equivalent to ::(immutable_heads() | root()). It is not recommended to redefine
    this alias. Note that modifying this will not change whether a commit is immutable.
    To do that, edit immutable_heads().

  • mutable(): The set of commits that jj treats as mutable. This is
    equivalent to ~immutable(). It is not recommended to redefine this alias.
    Note that modifying this will not change whether a commit is immutable.
    To do that, edit immutable_heads().

  • visible(): The set of visible commits. Resolves to ::visible_heads().
    This is equal to all() unless your revset includes
    hidden revisions.

  • hidden(): The set of hidden commits. Resolves to ~visible(). This is empty
    unless your revset includes hidden revisions. Note that
    this is not the set of all previously visible
    commits
    .

Examples

Show the parent(s) of the working-copy commit (like git log -1 HEAD):

shell
jj log -r @-

Show all ancestors of the working copy (like plain git log)

shell
jj log -r ::@

Show commits not on any remote bookmark:

shell
jj log -r 'remote_bookmarks()..'

Show commits not on origin (if you have other remotes like fork):

shell
jj log -r 'remote_bookmarks(remote=origin)..'

Show the initial commits in the repo (the ones Git calls "root commits"):

shell
jj log -r 'root()+'

Show some important commits (like git log --simplify-by-decoration):

shell
jj log -r 'tags() | bookmarks()'

Show local commits leading up to the working copy, as well as descendants of
those commits:

shell
jj log -r '(remote_bookmarks()..@)::'

Show commits authored by "martinvonz" and containing the word "reset" in the

shell
jj log -r 'author(*martinvonz*) & description(*reset*)'

Roadmap (`docs/roadmap.md`)

Roadmap

This documents some of the goals we have. Many of them are quite independent.

Note: Most people contributing to Jujutsu do so in their spare time, which
means that we cannot attach any target dates to any of the goals below.

Support for copies and renames

We want to support copy tracing in a way that leaves it up to the commit backend
to either record or detect copies. That should let us work with existing Git
repos (Git does not record copies, it detects them on the fly) as well as with
very large repos where detection would be too slow. See
design doc.

Forge integrations

We would like to make it easier to work with various popular forges by providing
something like jj github submit and jj gitlab submit. For popular forges, we
might include that support by default in the standard jj binary.

Submodule support

Git submodules are used frequently enough in large Git repos that we will
probably need to support them. There are still big open
questions around UX.

Better Rust API for UIs

UIs like gg currently have to duplicate quite a bit of logic from jj-cli. We
need to make this code not specific to the CLI (e.g. return status objects
instead of printing messages) and move it into jj-lib.

RPC API

One problem with writing tools using the Rust API is that they will only work
with the backends they were compiled with. For example, a regular gg build
will not work on Google repos because it doesn't have the backends necessary to
load them. We want to provide an RPC API for tools that want to work with an
unknown build of jj by having the tool run something like jj api to give it
an address to talk to.

In addition to helping with the problem of unknown backends, having an RPC API
should make it easier for tools like VS Code that are not written in Rust. The
RPC API will probably be at a higher abstraction level than the Rust API.

See design doc.

Open-source cloud-based repos (server and daemon process)

Google has an internal Jujutsu server backed by a database. This server allows
commits and repos (operation logs) to be stored in the cloud (i.e. the database).
Working copies can still be stored locally.

In order to reduce latency, there is a local daemon process that caches reads
and writes. It also prefetches objects it thinks the client might ask for
next. It also helps with write latency by optimistically answering write
requests (it therefore needs to know the server's hashing scheme so it can
return the right IDs).

We (the project, not necessarily Google) want to provide a similar experience
for all users. We would therefore like to create a similar server and daemon.
The daemon might be the same process as for the RPC API mentioned above.

Virtual file system (VFS)

For very large projects and/or large files, it can be expensive to update the
working copy. We want to provide a VFS to help with that. Updating the working
copy to another commit can then be done simply by telling the VFS to use the
other commit as base, without needing to download any large files in the target
commit until the user asks for them via the file system. A VFS can also make it
cheap to snapshot the working copy by keeping track of all changes compared to
the base commit.

Having a VFS can also be very benefial for jj run, since we can then
cheaply create temporary working copies for the commands to run in.

Better support for large files

We have talked about somehow using content-defined chunking (CDC) to reduce
storage and transfer costs for large files. Maybe we will store files in our
future cloud-based server using the same model as XetHub.


Sapling Comparison (`docs/sapling-comparison.md`)

Comparison with Sapling

Introduction

This document attempts to describe how jj is different
from Sapling. Sapling is a VCS developed by Meta. It
was announced about 3 years after development started on jj. It is a heavily
modified fork of Mercurial. Because jj has
copied many ideas from Mercurial, there are many similarities between the two
tools, such as:

  • A user-friendly CLI
  • A "revset" language for selecting revisions
  • Good support for working with stacked commits, including tracking "anonymous
    heads" (no "detached HEAD" state like in Git) and split commands, and
    automatically rebasing descendant commits when you amend a commit.
  • Flexible customization of output using templates

Differences

Here is a list of some differences between jj and Sapling.

  • Working copy: When using Sapling (like most VCSs), the
    user explicitly tells the tool when to create a commit and which files to
    include. When using jj, the working copy
    is automatically snapshotted by every command. New files
    are automatically tracked and deleted files are automatically untracked. This
    has several advantages:

    • The working copy is effectively backed up every time you run a command.
    • No commands fail because you have changes in the working copy ("abort: 1
      conflicting file changes: ..."). No need for sl shelve.
    • Simpler and more consistent CLI because the working copy is treated like any
      other commit.
  • Conflicts: Like most VCSs, Sapling requires the user to
    resolve conflicts before committing. jj lets
    you commit conflicts. Note that it's a representation of the
    conflict that's committed, not conflict markers (<<<<<<< etc.). This also
    has several advantages:

    • Merge conflicts won't prevent you from checking out another commit.
    • You can resolve the conflicts when you feel like it.
    • Rebasing descendants always succeeds. Like jj, Sapling automatically
      rebases, but it will fail if there are conflicts.
    • Merge commits can be rebased correctly (Sapling sometimes fails).
    • You can rebase conflicts and conflict resolutions.
  • Undo: jj's undo is powered by the operation log, which
    records how the repo has changed over time. Sapling has a similar feature
    with its MetaLog.
    They seem to provide similar functionality, but jj also exposes the log to the
    user via jj op log, so you can tell how far back you want to go back.
    Sapling has sl debugmetalog, but that seems to show the history of a single
    commit, not the whole repo's history. Thanks to jj snapshotting the working
    copy, it's possible to undo changes to the working copy. For example, if
    you jj undo a jj commit, jj diff will show the same changes as
    before jj commit, but if you sl undo a sl commit, the working copy will
    be clean.

  • Git interop: Sapling supports cloning, pushing, and pulling from a remote
    Git repo. jj also does, and it also supports sharing a working copy with a Git
    repo, so you can use jj and git interchangeably in the same repo.

  • Polish: Sapling is more polished and feature-complete. Sapling has very
    nice built-in web UI called
    Interactive Smartlog, which lets
    you drag and drop commits to rebase them, among other things.

  • Forge workflow: Sapling has sl pr submit --stack, which lets you
    push a stack of commits as separate GitHub PRs, including setting the base
    branch. It only supports GitHub. jj doesn't have any direct integration with
    GitHub or any other forge. However, it has jj git push --change for
    automatically creating branches for specified commits. You have to specify
    each commit you want to create a branch for by using
    jj git push --change X --change Y ..., and you have to manually set up any
    base branches in GitHub's UI (or GitLab's or ...). On subsequent pushes, you
    can update all at once by specifying something like jj git push -r main..@
    (to push all branches on the current stack of commits from where it forked
    from main).


Style Guide (`docs/style_guide.md`)

Style guide

Panics

Panics are not allowed, especially in code that may run on a server. Calling
.unwrap() is okay if it's guaranteed to be safe by previous checks or
documented invariants. For example, if a function is documented as requiring a
non-empty slice as input, it's fine to call slice[0] and panic.

Markdown

Try to wrap at 80 columns. We don't have a formatter yet.

Prefer lower-level tests to end-to-end tests

When possible, prefer lower-level tests that don't use the jj binary.
End-to-end tests are much slower than similar tests that create a repo using
jj-lib (roughly 100x slower). It's also often easier to test edge cases in
lower-level tests.

It can still be useful to add a test case or two to check that the lower-level
functionality is correctly hooked up in the CLI. For example, the end-to-end
tests for jj log don't need to test that all kinds of revsets are evaluated
correctly (we have tests in jj-lib for that), but they should check that the
-r flag is respected.

Use end-to-end tests for testing the CLI commands themselves.

Logging messages

When messages are logged, such as with writeln!(ui.status(), "Message...")?;,
prefer to end the message with a period as if it were a full sentence. For
example:

rust
// CORRECT:
writeln!(ui.status(), "Rebased {num_rebased} descendant commits.")?;
// INCORRECT:
writeln!(ui.status(), "Rebased {num_rebased} descendant commits")?;

There are of course exceptions to this, such as messages that are printing
interpolated values at the end, or need to print some templated value:

rust
// These are fine:
writeln!(
    ui.status(),
    "Operation left uncommitted because --no-integrate-operation was requested: {}",
    short_operation_hash(self.repo().op_id())
)?;
writeln!(ui.warning_default(), "No matching entries for paths: {paths}")?;

Also try not to put periods right after any printed IDs or symbols (such as
commit IDs), since users may double click to copy the value, which may include
the period.

Plural forms

Do not special-case singular vs. plural forms in messages with a "number
object(s)" format; always use the plural form, regardless of the number being
used:

rust
// CORRECT:
writeln!(ui.status(), "Rebased {num_rebased} descendant commits.")?;
// INCORRECT:
writeln!(ui.status(), "Rebased {num_rebased} descendant commit(s).")?;
writeln!(
  ui.status(),
  "Rebased {num_rebased} descendant commit{}.",
  if num_rebased == 1 { "" } else { "s" }
)?;

If a message is only referring to a single specific object without any numbers
involved, use the singular form.

Rust

  • Prefer Itertools collect_vec() and try_collect() over annotated
    .collect::<Vec<...>>() calls.

    rust
    // CORRECT:
    
    let commits = workspace_helper.parse_union_revsets(&revs)
      .ids()
      .collect_vec();
    
    let fallible_commits = commits.iter().map(|c| store.get(c))
      .try_collect()?;
    rust
    // INCORRECT: The annotation is used
    
    let commits: Vec<_> = workspace_helper.parse_union_revsets(&revs)
      .ids()
      .collect();
    
    let fallible_commits = commits.iter().map(|c| store.get(c))
      .collect::<Vec<_, CommandError>>()?;

Documentation comments

General

In general, try to follow rust-lang/rfcs 1574. The
important points are:

  • Use documentation comments (///) on types, functions, and fields.

  • Use Markdown where appropriate, such as for symbols, code blocks, headings,
    etc.

  • The first line of a doc comment should be a single-line short sentence that
    summarizes the element being described. There is no hard rule for "short", but
    try to make it both clear and succinct.

    • Since doc comments are interpreted as Markdown, there must be a blank
      comment line between this first line and the rest of the comment:

      rust
      // CORRECT:
      
      /// Foos the given `bar` and `baz`.
      ///
      /// More description about this very important function.
      fn foo(bar: &Bar, baz: &Baz) { ... }
      rust
      // INCORRECT: This entire comment will be interpreted as the "first line".
      
      /// Foos the given `bar` and `baz`.
      /// More description about this very important function.
      fn foo(bar: &Bar, baz: &Baz) { ... }
    • For functions, this summary line should be written in third-person singular
      present indicative form ("Returns ..." instead of "Return ..."). You can
      imagine an implicit "This function..." or the function's name before the
      summary line.

  • The rest of the comment after the first line should generally be written in
    complete sentences. But use your own judgment too; for example a sentence that
    says "Returns blah blah." would be fine.

Subcommand comments

Specifically for jj subcommands:

  • Doc comments for subcommands should describe its corresponding *Args struct.
    For example, documentation for jj new goes on struct NewArgs.

  • As above, write an appropriate short first line for the command, but do not
    include a trailing period / full stop. See jj -h for examples (--help will
    show the full doc comment for each command, while the short form -h will
    only show the first line).

    • Use imperative mood, as if you are telling the command what to do (giving it
      a command, if you will).

    • If the subcommand is actually a "collection" of subcommands (such as
      jj config or jj bookmark), prefer to use the format "Manage ...", such
      as "Manage config options" or "Manage bookmarks". But use your own judgment;
      if it sounds awkward, "Commands for ..." or whatever you come up with would
      probably be fine.

    text
    CORRECT:
    $ jj new -h
    Create a new, empty change and (by default) edit it in the working copy
    
    $ jj bookmark create -h
    Create a new bookmark
    text
    INCORRECT:
    $ jj new -h
    Creates a new, empty change. The default behavior is to immediately edit this new change, but pass `--no-edit` to avoid this.
    
    $ jj bookmark create -h
    Creates new bookmarks with the given names and assigns them to the given target revision, which defaults to `@`.
  • The rest of the command documentation should be written in complete sentences.

  • If the command requires complex explanation, such as jj rebase, prefer to
    fully explain it in the command's doc comment rather than in some other online
    documentation page so that all the relevant info can still be accessed via the
    CLI.

    • For example, jj rebase --help.
    • Markdown can be used, but make sure to use at least level 3 headings (###
      or more). The CLI reference page uses level 2 headings for each command
      (e.g., ## jj root), so the Table of Contents might break if any command
      documentation uses level 1 or level 2 headings.
  • Default aliases (i.e., those defined in misc.toml) are not
    automatically shown by clap, since it doesn't know about them. Mention these
    at the end of the first line for the command in question in the format
    [default alias: <alias>]. For example, the first line of jj describe is:

    text
    Update the change description or other metadata [default alias: desc]
  • Very loose guidance, but if you are adding a new command, prefer single
    "action" words (e.g., verbs) over compound words or portmanteaus. It can be a
    good idea to add a new subcommand instead. Please ask for feedback on names if
    you are struggling to come up with something or are unsure about a name.

  • If you are unsure how your comment will look after being processed, run the
    test_generate_markdown_docs_in_docs_dir test to generate
    [email protected].

Command argument / option comments

Specifically for jj command arguments / options:

  • As above, write an appropriate short first line for the arg, but do not
    include a trailing period / full stop. See jj <command> -h for examples
    (--help will show the full doc comment for each arg, while the short form
    -h will only show the first line).

    • Since arguments can be things or actions or whatever, there is no hard rule
      for the form of the first line. If it is a noun, such as -R/--repository
      or --config, a good first line could be the answer to the question "What
      is <arg>?" If it is a verb, such as --quiet, try to stick with
      imperative mood (as if you're telling the command how to behave if this flag
      is present). Use your own judgment for what might be clearest for users.

    • If the argument can be repeated, append "(can be repeated)" to the end of
      the first line.

    • If the argument can be repeated, prefer to make the argument name singular
      instead of plural, but speak about the "things" in the plural in the
      documentation. This makes the experience of specifying the values on the
      command line simpler and more consistent.

      • Note that the name of the struct field can be different from the name of
        the argument according to clap:
        rust
        #[arg(long = "flag")]
        my_values: MyType,
        This section is mostly discussing the user-facing argument / option names;
        how you name variables is up to you.
    text
    CORRECT / PREFERRED:
    --ignore-working-copy  Don't snapshot the working copy, and don't update it
    --quiet                Silence non-primary command output
    --no-pager             Disable the pager
    --config <NAME=VALUE>  Additional configuration options (can be repeated)
    text
    INCORRECT / LESS PREFERRED:
    --ignore-working-copy   No snapshotting (so working copy is ignored).
    --quiet                 Provide this option to skip non-primary command output.
    --no-pager              Disables the pager.
    --configs <NAME=VALUE>  Override some configuration options.
  • Short options and visible aliases are automatically shown by clap, so no
    need to mention them explicitly (for simple cases). However, it may be good to
    mention conflicting arguments / options or how options interact with each
    other if applicable.

  • If an argument has a default value, mention what it is, probably in the full
    description (i.e., not the first line).

    • In cases where this default comes from a configurable setting, such as
      revsets.log for --revisions in jj log, mention the setting. Prefer not
      to also spell out the default value of the setting, since that introduces
      toil and potential inconsistency if the default ever changes in the future.
  • If you are unsure how your comment will look after being processed, run the
    test_generate_markdown_docs_in_docs_dir test to generate
    [email protected].

Fileset / revset / templating language functions

(Note: In the templating language, this guidance also applies to all type
methods.)

Function signatures

  • Function signatures are written similar to Rust:

    text
    function_name(arg1: Type1, arg2: Type2) -> ReturnType

    (Note that types are only applicable to the templating language and can be
    omitted from the fileset and revset language documentation.)

  • For optional parameters, put square brackets around the arg: Type part;
    importantly, do not put the brackets around any surrounding commas.

    text
    CORRECT:
    parents(x, [depth])
    if(condition: Boolean, then: Template, [else: Template]) -> Template
    text
    INCORRECT:
    parents(x[, depth])
    if(condition: Boolean, then: Template[, else]) -> Template
  • For named parameters (rare), put the parameter name in the format [name=]
    before the arg: Type part, with no spaces. Note that this should also be
    included in the square brackets indicating that the parameter is optional.
    name and arg may be the same, but they don't have to be.

    (Note that currently, named parameters must be optional, since they are
    intended as a way for users to skip previous optional parameters and provide a
    later one instead. However, this may change in the future, especially if named
    parameters make their way to the templating language.)

    text
    CORRECT:
    remote_bookmarks([name_pattern], [[remote=]remote_pattern])
    text
    INCORRECT:
    remote_bookmarks([name_pattern], [remote] = [remote_pattern])
    remote_bookmarks([name_pattern], [remote=remote_pattern])
  • For variadic parameters, add an ellipsis (...) after the arg: Type part.

    text
    CORRECT:
    coalesce(revsets...)
    concat(content: Template...) -> Template
    text
    INCORRECT:
    coalesce(...revsets)
    concat(*content: Template) -> Template

Descriptions

  • For the first sentence:

    • In the fileset language, "functions" act as "filters for files". Follow the
      general Rust style of writing the first sentence in third-person singular
      present indicative form.

    • In the revset language, "functions" evaluate to "a set of revisions".
      Writing a "Returns" prefix for every function is redundant and unnecessary;
      instead, write the first sentence as if there is an implicit "The return
      value is..." or "The return values are..." in front of the first sentence.

    • In the templating language, "functions" will "accept inputs, do things, and
      return an output". Follow the general Rust style of writing the first
      sentence in third-person singular present indicative form.

    text
    CORRECT:
    all(): Matches all files.
    
    root(): The virtual commit that is the oldest ancestor of all other commits.
    none(): No commits.
    none(): Nothing.
    
    json(value: Serialize) -> String: Serializes `value` in JSON format.
    text
    INCORRECT:
    all(): All files.
    
    root(): Returns the virtual commit that is the oldest ancestor of all other commits.
    none(): Returns nothing.
    
    json(value: Serialize) -> String: Serialize `value` in JSON format.
    json(value: Serialize) -> String: The `value` in JSON format.
  • All following sentences should be written as complete sentences.

  • We tend to write the entire description as a single paragraph following the
    function signature, but add line breaks for any examples of usages.

  • Use backticks to reference arguments of the function instead of simply
    saying "the content" or "the width".

  • If a parameter's purpose is obvious, it does not need to be explicitly
    mentioned in the description, but try to make sure it is clear what all
    parameters do.

  • Mention default values for optional parameters, or what happens if not given.

  • Call out edge cases if applicable.

  • For type methods in the templating language, try to document every method,
    even if obvious. This will give you the chance to mention any edge cases,
    default behaviors, or differences from other methods.

TOML

Try to wrap lines and comments to 80 columns. We don't have a formatter yet.

For TOML that gets compiled into the binary (in cli/src/config):

  • For [revsets] and [templates], prefer to delegate to a function alias
    (named builtin_...) if the definition is a little longer or more complex.
    This allows users to reference the existing default in their own aliases; this
    is not possible with the values from the [revsets] or [templates] tables
    directly.

  • For [revset-aliases], do not define any non-function aliases, since they may
    conflict with user symbols such as bookmarks or tags. Historically we have not
    added many default revset aliases, so there isn't much else to say.

  • For [template-aliases], all non-function aliases (that are not 'constants')
    must be prefixed with builtin_, and they should preferably have a
    corresponding function overload that the non-function alias calls with self.
    Non-function aliases are provided for CLI convenience since they don't require
    shell quoting, so only add one if it makes sense to (for example, if it is a
    complete value for a --template flag).

  • For [template-aliases], prefer to name functions with the prefix format_.

  • For [colors], prefer to define colors for labels rather than for the color
    or style. That is, do not add a colors.red = "red" entry, since this makes
    it harder for users to customize a specific label in a specific output.
    Instead, make sure labels reflect their context or purpose, such as prefix,
    author, conflict, etc.

  • For [colors], avoid the "bright yellow" color as a default; it is
    difficult to read on xterm's default theme. See
    #528.


Templates (`docs/templates.md`)

Templates

Jujutsu supports a functional language to customize output of commands.
The language consists of literals, keywords, operators, functions, and
methods.

A couple of jj commands accept a template via -T/--template option.

Keywords

Keywords represent objects of different types; the types are described in
the following sections. The top-level object for a template can always be
referenced as self. Its concrete type depends on the command (and is
documented on each command's --template help).

In any template, all 0-argument methods of that top-level self type are
available as keywords. For example:

  • In jj log, self is the Commit type, so commit_id is
    equivalent to self.commit_id().
  • In jj op log, self is the Operation type, so
    current_operation is equivalent to self.current_operation().
  • In jj file list, self is the TreeEntry type, so
    path is equivalent to self.path().

Operators

The following operators are supported. x and y below can be any expression.

Operators are listed in order of binding power from strongest to weakest, e.g.
x ++ y * z is interpreted as x ++ (y * z) since * has stronger binding
power than ++. Infix operators of the same binding power are parsed from left
to right, e.g. x * y / z is interpreted as (x * y) / z.

As seen above, parentheses can be used to control evaluation order, e.g. (x + y) * z.

    • x.f(): Method call.
    • f(x): Function call.
    • -x: Negate integer value.
    • !x: Logical not.
    • p:x: String pattern or pattern alias named p.
    • x * y, x / y, x % y: Multiplication / division / remainder. Operands must
      be Integers.
    • x + y, x - y: Addition / subtraction. Operands must be Integers.
    • x >= y, x > y, x <= y, x < y: Greater than or equal / greater than /
      lesser than or equal / lesser than. Operands must be Integers.
    • x == y, x != y: Equal / not equal. Operands must be either Boolean,
      ByteString, Integer, or String.
    • x && y: Logical and, short-circuiting.
    • x || y: Logical or, short-circuiting.
    • x ++ y: Concatenate x and y templates.

Global functions

The following functions are defined.

  • fill(width: Integer, content: Template, [break_words: bool]) -> Template:
    Fill lines at the given width. If break_words is true, any words longer
    than width will be split to fit in width characters.

  • indent(prefix: Template, content: Template) -> Template: Indent
    non-empty lines by the given prefix.

  • pad_start(width: Integer, content: Template, [fill_char: Template]): Pad (or
    right-justify) content by adding leading fill characters. The content
    shouldn't have newline character.

  • pad_end(width: Integer, content: Template, [fill_char: Template]): Pad (or
    left-justify) content by adding trailing fill characters. The content
    shouldn't have newline character.

  • pad_centered(width: Integer, content: Template, [fill_char: Template]): Pad
    content by adding both leading and trailing fill characters. If an odd number
    of fill characters are needed, the trailing fill will be one longer than the
    leading fill. The content shouldn't have newline characters.

  • truncate_start(width: Integer, content: Template, [ellipsis: Template]):
    Truncate content by removing leading characters. The content shouldn't
    have newline character. If ellipsis is provided and content was truncated,
    prepend the ellipsis to the result.

  • truncate_end(width: Integer, content: Template, [ellipsis: Template]):
    Truncate content by removing trailing characters. The content shouldn't
    have newline character. If ellipsis is provided and content was truncated,
    append the ellipsis to the result.

  • hash(content: Stringify) -> String:
    Hash the input and return a hexadecimal string representation of the digest.

  • label(label: Stringify, content: Template) -> Template: Apply a custom
    color label to the content. The label is evaluated as a
    space-separated string.

  • hyperlink(url: Stringify, text: Template, [fallback: Template]) -> Template:
    Render text as a hyperlink to url using OSC 8 escape sequences
    when outputting with color enabled. Otherwise, renders fallback instead,
    which defaults to text. Use --color=always to force hyperlinks when piping
    output to a terminal emulator that supports OSC 8.

  • raw_escape_sequence(content: Template) -> Template: Preserves any escape
    sequences in content (i.e., bypasses sanitization) and strips labels.
    Note: This function is intended for escape sequences and as such, its output
    is expected to be invisible / of no display width. Outputting content with
    nonzero display width may break wrapping, indentation etc.

  • replace(pattern: StringPattern, content: Template, replacement: |RegexCaptures| -> Template) -> Template:
    Replace every match of pattern in content by formatting the replacement
    lambda for each match.

    This is similar to String.replace(), but operates on
    Template content and preserves formatting of the parts of content that are
    not replaced. The replacement itself is newly formatted template content.

    The lambda argument is a RegexCaptures value for the
    current match. Use .get(index) to access capture groups by index, .name(name)
    to access named capture groups, and .len() to get the number of captures
    including capture group 0 for the full match.

  • stringify(content: Stringify) -> String: Format content to string. This
    effectively removes color labels.

  • json(value: Serialize) -> String: Serialize value in JSON format. To
    serialize multiple values as a JSON object, use json({key => value, ..}).
    For example, json({"commit_id" => commit_id, "tags" => tags}).

  • if(condition: Boolean, then: Any, [else: Any]) -> Any:
    Conditionally evaluates to then / else content.

  • try(expr: Any, fallback: Any...) -> Any: Evaluates each expression in order
    and returns the first successful result. Suppresses preceding runtime errors,
    but lets parsing errors (such as type errors) pass through.

  • coalesce(content: Template...) -> Template: Returns the first non-empty
    content.

  • concat(content: Template...) -> Template:
    Same as content_1 ++ ... ++ content_n.

  • join(separator: Template, content: Template...) -> Template: Insert
    separator between contents.

  • separate(separator: Template, content: Template...) -> Template: Insert
    separator between non-empty contents.

  • surround(prefix: Template, suffix: Template, content: Template) -> Template:
    Surround non-empty content with texts such as parentheses.

  • config(name: Stringify) -> Option<ConfigValue>: Look up configuration
    value by name.

  • revset(revset: Stringify) -> List<Commit>: Evaluates a revset.

    The revset is evaluated once during template parsing if its value evaluates to
    a constant string. Queries built from values that aren't yet known during
    parsing, for example self.change_id(), are evaluated per call at runtime.

  • git_web_url([remote: String]) -> String: Best-effort conversion of a git
    remote URL to an HTTPS web URL. Defaults to the "origin" remote. Returns an
    empty string on failure. SSH host alias resolution is currently unsupported.

Types

`AnnotationLine` type

Conversion: Boolean: no, Serialize: no, Template: no

The following methods are defined.

  • .commit() -> Commit: Commit responsible for changing the relevant line.
  • .content() -> ByteString: Line content including newline character.
  • .line_number() -> Integer: 1-based line number.
  • .original_line_number() -> Integer: 1-based line number in the original commit.
  • .first_line_in_hunk() -> Boolean: False when the directly preceding line
    references the same commit.

`Any` type

Conversion: Boolean: no, Serialize: maybe, Template: maybe

All types can be implicitly converted to Any. No methods are defined.

`AnyList` type

Conversion: Boolean: no, Serialize: maybe, Template: maybe

The following methods are defined.

  • .join(separator: Template) -> Template: Concatenate elements with
    the given separator.

`Boolean` type

Conversion: Boolean: yes, Serialize: yes, Template: yes

No methods are defined. Can be constructed with false or true literal.

`ByteString` type

Conversion: Boolean: yes, Serialize: yes, Template: yes

A byte string, whose encoding is considered ASCII-compatible (e.g. UTF-8), but
isn't guaranteed. This can be implicitly converted to Boolean. The following
methods are defined.

  • .len() -> Integer: Length in bytes.

  • .contains(needle: ByteStringify) -> Boolean: Whether the string contains the
    provided stringifiable value as a substring.

  • .match(needle: StringPattern) -> ByteString: Extract the first matching part
    of the string for the given pattern.

    An empty string is returned if there is no match.

  • .starts_with(needle: ByteStringify) -> Boolean: Whether needle is a
    prefix of the string.

  • .ends_with(needle: ByteStringify) -> Boolean: Whether needle is a suffix
    of the string.

  • .remove_prefix(needle: ByteStringify) -> ByteString: Remove the passed
    prefix, if present.

  • .remove_suffix(needle: ByteStringify) -> ByteString: Remove the passed
    suffix, if present.

  • .trim() -> ByteString: Remove leading and trailing ASCII whitespace.

  • .trim_start() -> ByteString: Remove leading ASCII whitespace.

  • .trim_end() -> ByteString: Remove trailing ASCII whitespace.

  • .substr(start: Integer, [end: Integer]) -> ByteString: Extract substring.
    Indices are 0-based and end is exclusive. Negative values count from the end
    of the string, with -1 being the last byte. If end is not given, returns
    from start to the end of the string.

  • .first_line() -> ByteString

  • .lines() -> List: Split into lines excluding newline characters.

  • .split(separator: StringPattern, [limit: Integer]) -> List:
    Split into substrings by the given separator pattern. If limit is
    specified, it determines the maximum number of elements in the result, with
    the remainder of the string returned as the final element. A limit of 0
    returns an empty list.

  • .replace(pattern: StringPattern, replacement: ByteStringify, [limit: Integer]) -> ByteString: Replace occurrences of the given pattern with the
    replacement string.

    By default, all occurrences are replaced. If limit is specified, at most
    that many occurrences are replaced.

    Supports capture groups in patterns using $0 (entire match), $1, $2 etc.

  • .upper() -> ByteString: Map each ASCII character to upper case.

  • .lower() -> ByteString: Map each ASCII character to lower case.

`ByteStringify` type

An expression that can be converted to a ByteString.

A String can be converted to a ByteString losslessly. Any types that can be
converted to Template can also be ByteStringify. Unlike Template, color
labels are stripped.

`ChangeId` type

Conversion: Boolean: no, Serialize: yes, Template: yes

The following methods are defined.

  • .normal_hex() -> String: Normal hex representation (0-9a-f) instead of the
    canonical "reversed" (z-k) representation.
  • .short([len: Integer]) -> String
  • .shortest([min_len: Integer]) -> ShortestIdPrefix: Shortest unique prefix.

`Commit` type

Conversion: Boolean: no, Serialize: yes, Template: no

This type cannot be printed. The following methods are defined.

  • .description() -> String: Usually ends with a trailing \n, if non-blank.
  • .trailers() -> List: The trailers at the end of the commit
    description that are formatted as <key>: <value>. These are returned in the
    same order as they appear in the description, and there may be multiple
    Trailers with the same key.
  • .change_id() -> ChangeId
  • .commit_id() -> CommitId
  • .parents() -> List<Commit>
  • .author() -> Signature
  • .committer() -> Signature
  • .signature() -> Option<CryptographicSignature>: Cryptographic signature if
    the commit was signed.
  • .mine() -> Boolean: Commits where the author's email matches the email of
    the current user.
  • .working_copies() -> List<WorkspaceRef>: For multi-workspace repositories,
    returns a list of workspace references for each workspace whose working-copy
    commit matches the current commit.
  • .current_working_copy() -> Boolean: True for the working-copy commit of the
    current workspace.
  • .bookmarks() -> List<CommitRef>: Local and remote bookmarks pointing to the
    commit. A tracked remote bookmark will be included only if its target is
    different from the local one.
  • .local_bookmarks() -> List<CommitRef>: All local bookmarks pointing to the
    commit.
  • .remote_bookmarks() -> List<CommitRef>: All remote bookmarks pointing to the
    commit.
  • .tags() -> List<CommitRef>: Local and remote tags pointing to the commit. A
    tracked remote tag will be included only if its target is different from the
    local one.
  • .local_tags() -> List<CommitRef>: All local tags pointing to the commit.
  • .remote_tags() -> List<CommitRef>: All remote tags pointing to the commit.
  • .divergent() -> Boolean: True if the commit's change ID corresponds to multiple
    visible commits.
  • .hidden() -> Boolean: True if the commit is not visible (a.k.a. abandoned).
  • .change_offset() -> Option: The change offset
    of this commit. May not be available for some commits.
  • .immutable() -> Boolean: True if the commit is included in the set of
    immutable commits
    .
  • .contained_in(revset: Stringify) -> Boolean: True if the commit is included in
    the provided revset.
  • .conflict() -> Boolean: True if the commit contains merge conflicts.
  • .empty() -> Boolean: True if the commit modifies no files.
  • .diff([files: StringLiteral]) -> TreeDiff: Changes from the parents within the
    files expression
    . All files are compared by default, but it is
    likely to change in future version to respect the command line path arguments.
  • .files([files: StringLiteral]) -> List: Files that exist in this commit,
    matching the files expression. Use .diff().files() to list
    changed files.
  • .conflicted_files() -> List: Conflicted files in this commit.
  • .root() -> Boolean: True if the commit is the root commit.

`CommitEvolutionEntry` type

Conversion: Boolean: no, Serialize: yes, Template: no

This type cannot be printed. The following methods are defined.

  • .commit() -> Commit: New commit.
  • .operation() -> Operation: Operation where the commit was created or
    rewritten.
  • .predecessors() -> List<Commit>: Predecessor commits of this entry.
  • .inter_diff([files: StringLiteral]) -> TreeDiff: Changes between this commit and its
    predecessor version(s), rebased onto the parents of this commit to avoid unrelated
    changes (similar to jj evolog -p).

`CommitId` type

Conversion: Boolean: no, Serialize: yes, Template: yes

The following methods are defined.

  • .short([len: Integer]) -> String
  • .shortest([min_len: Integer]) -> ShortestIdPrefix: Shortest unique prefix.

`CommitRef` type

Conversion: Boolean: no, Serialize: yes, Template: yes

The following methods are defined.

  • .name() -> RefSymbol: Local bookmark or tag name.
  • .remote() -> Option<RefSymbol>: Remote name if this is a remote ref.
  • .present() -> Boolean: True if the ref points to any commit.
  • .conflict() -> Boolean: True if the bookmark or tag is
    conflicted
    .
  • .normal_target() -> Option<Commit>: Target commit if the ref is not
    conflicted and points to a commit.
  • .removed_targets() -> List<Commit>: Old target commits if conflicted.
  • .added_targets() -> List<Commit>: New target commits. The list usually
    contains one "normal" target.
  • .tracked() -> Boolean: True if the ref is tracked by a local ref. The local
    ref might have been deleted (but not pushed yet.)
  • .tracking_present() -> Boolean: True if the ref is tracked by a local ref,
    and if the local ref points to any commit.
  • .tracking_ahead_count() -> SizeHint: Number of commits ahead of the tracking
    local ref.
  • .tracking_behind_count() -> SizeHint: Number of commits behind of the
    tracking local ref.
  • .synced() -> Boolean: For a local bookmark, true if synced with all tracked
    remotes. For a remote bookmark, true if synced with the tracking local
    bookmark.

`ConfigValue` type

Conversion: Boolean: no, Serialize: yes, Template: yes

This type can be printed in TOML syntax. The following methods are defined.

  • .as_boolean() -> Boolean: Extract boolean.
  • .as_integer() -> Integer: Extract integer.
  • .as_string() -> String: Extract string. This does not convert non-string
    value (e.g. integer) to string.
  • .as_string_list() -> List<String>: Extract list of strings.

`CryptographicSignature` type

Conversion: Boolean: no, Serialize: no, Template: no

The following methods are defined.

  • .status() -> String: The signature's status ("good", "bad", "unknown",
    "invalid").
  • .key() -> String: The signature's key id representation (for GPG and SSH,
    this is the public key fingerprint).
  • .display() -> String: The signature's display string (for GPG, this is the
    formatted primary user ID; for SSH, this is the principal).

!!! warning

text
Calling any of `.status()`, `.key()`, or `.display()` is slow, as it incurs
the performance cost of verifying the signature (for example shelling out
to `gpg` or `ssh-keygen`). Though consecutive calls will be faster, because
the backend caches the verification result.

!!! info

text
As opposed to calling any of `.status()`, `.key()`, or `.display()`,
checking for signature presence through boolean coercion is fast:
```
if(commit.signature(), "commit has a signature", "commit is unsigned")
```

`DiffStatEntry` type

Conversion: Boolean: no, Serialize: no, Template: no

This type holds the diff stats per file. The following methods are defined.

  • .bytes_delta() -> Integer: The difference in size of the file, in bytes.
  • .lines_added() -> Integer: Number of lines added.
  • .lines_removed() -> Integer: Number of lines deleted.
  • .path() -> RepoPath: Path to the entry. If the entry is a copy / rename, this
    points to the target (or right) entry.
  • .display_diff_path() -> String: Format path for display, taking into account copy / rename information.
  • .status() -> String: One of "modified", "added", "removed", "copied", or "renamed".
  • .status_char() -> String: One of "M" (modified), "A" (added), "D" (removed),
    "C" (copied), or "R" (renamed).

`DiffStats` type

Conversion: Boolean: no, Serialize: no, Template: yes

This type can be printed as a histogram of the changes. The following methods
are defined.

  • .files() -> List<DiffStatEntry>: Per-file stats for changed files.
  • .total_added() -> Integer: Total number of insertions.
  • .total_removed() -> Integer: Total number of deletions.

`Email` type

Conversion: Boolean: yes, Serialize: yes, Template: yes

The email field of a signature may or may not look like an email address. It may
be empty, may not contain the symbol @, and could in principle contain
multiple @s.

The following methods are defined.

  • .local() -> String: the part of the email before the first @, usually the
    username.
  • .domain() -> String: the part of the email after the first @ or the empty
    string.

`FsPath` type

Conversion: Boolean: no, Serialize: yes, Template: yes

A filesystem path. Paths can contain bytes that are not valid UTF-8. Rendering
and path formatting methods preserve those bytes. Serialization with json()
follows Rust's Path serialization, which can reject non-UTF-8 paths.

The following methods are defined.

  • .absolute() -> FsPath: Return an absolute filesystem path.
  • .relative() -> FsPath: Return the path relative to the current working
    directory.

`Integer` type

Conversion: Boolean: no, Serialize: yes, Template: yes

No methods are defined.

`List` type

Conversion: Boolean: yes, Serialize: maybe, Template: maybe

A list can be implicitly converted to Boolean. The following methods are
defined.

  • .len() -> Integer: Number of elements in the list.
  • .join(separator: Template) -> Template: Concatenate elements with
    the given separator.
  • .filter(|item| expression) -> List: Filter list elements by predicate
    expression. Example: description.lines().filter(|s| s.contains("#"))
  • .map(|item| expression) -> AnyList: Apply template expression
    to each element. Example: parents.map(|c| c.commit_id().short())
  • .any(|item| expression) -> Boolean: Returns true if any element satisfies
    the predicate expression. Example: parents.any(|c| c.description().contains("fix"))
  • .all(|item| expression) -> Boolean: Returns true if all elements satisfy
    the predicate expression. Example: parents.all(|c| c.mine())
  • .first() -> Option<T>: Returns the first element.
  • .last() -> Option<T>: Returns the last element.
  • .get(index: Integer) -> Option<T>: Returns the element at index (0-based).
  • .reverse() -> List: Returns the list in reverse order.
  • .skip(count: Integer) -> List: Skips the first count elements and
    returns the rest.
  • .take(count: Integer) -> List: Returns only the first count elements.

`List` type

The following methods are defined. See also the List type.

  • .contains_key(key: Stringify) -> Boolean: True if the commit description
    contains at least one trailer with the key key.

`Operation` type

Conversion: Boolean: no, Serialize: yes, Template: no

This type cannot be printed. The following methods are defined.

  • .current_operation() -> Boolean
  • .description() -> String
  • .id() -> OperationId
  • .attributes() -> String
  • .time() -> TimestampRange
  • .user() -> String
  • .snapshot() -> Boolean: True if the operation is a snapshot operation.
  • .workspace_name() -> String: The name of the workspace the operation was
    created from. An empty string if the operation wasn't created from a
    workspace.
  • .root() -> Boolean: True if the operation is the root operation.
  • .parents() -> List<Operation>

`OperationId` type

Conversion: Boolean: no, Serialize: yes, Template: yes

The following methods are defined.

  • .short([len: Integer]) -> String

`Option` type

Conversion: Boolean: yes, Serialize: maybe, Template: maybe

An option can be implicitly converted to Boolean denoting whether the
contained value is set. If set, all methods of the contained value can be
invoked. If not set, an error will be reported inline on method call.

On comparison between two optional values or optional and non-optional values,
unset value is not an error. Unset value is considered less than any set values.

Types convertible to Boolean (e.g. String and List) lack support for
Option because the truthy conversion of an unset value would conflict with
it. Instead, unset values of these types are converted to their empty
equivalents.

`RefSymbol` type

Conversion: Boolean: no, Serialize: yes, Template: yes

A String type, but is formatted as revset symbol by quoting
and escaping if necessary. Unlike strings, this cannot be implicitly converted
to Boolean.

`RegexCaptures` type

Conversion: Boolean: no, Serialize: no, Template: no

This type is passed to the replacement lambda of the global
replace() function.

The following methods are defined.

  • .len() -> Integer: Number of capture groups, including capture group 0 for
    the full match.
  • .get(index: Integer) -> ByteString: Returns the capture group at index.
    Capture group 0 is the full match. Errors if the index is out of bounds.
  • .name(name: Stringify) -> ByteString: Returns the named capture group
    name. Errors if there is no such named capture group.

`RepoPath` type

Conversion: Boolean: no, Serialize: yes, Template: yes

A slash-separated path relative to the repository root. The following methods
are defined.

  • .absolute() -> FsPath: Absolute filesystem path.
  • .display() -> String: Format path for display. The formatted path uses
    platform-native separator, and is relative to the current working directory.
  • .parent() -> Option<RepoPath>: Parent directory path.

`Serialize` type

An expression that can be serialized in machine-readable format such as JSON.

!!! note

text
Field names and value types in the serialized output are usually stable
across jj versions, but the backward compatibility isn't guaranteed. If the
underlying data model is updated, the serialized output may change.

`ShortestIdPrefix` type

Conversion: Boolean: no, Serialize: yes, Template: yes

The following methods are defined.

  • .prefix() -> String
  • .rest() -> String
  • .upper() -> ShortestIdPrefix
  • .lower() -> ShortestIdPrefix

`Signature` type

Conversion: Boolean: no, Serialize: yes, Template: yes

The following methods are defined.

  • .name() -> String
  • .email() -> Email
  • .timestamp() -> Timestamp

`SizeHint` type

Conversion: Boolean: no, Serialize: yes, Template: no

This type cannot be printed. The following methods are defined.

  • .lower() -> Integer: Lower bound.
  • .upper() -> Option: Upper bound if known.
  • .exact() -> Option: Exact value if upper bound is known and it
    equals to the lower bound.
  • .zero() -> Boolean: True if upper bound is known and is 0. Equivalent to
    .upper() == 0.

`String` type

Conversion: Boolean: yes, Serialize: yes, Template: yes

A UTF-8-encoded string. This can be implicitly converted to Boolean. The
following methods are defined.

  • .len() -> Integer: Length in UTF-8 bytes.

  • .contains(needle: Stringify) -> Boolean: Whether the string contains the
    provided stringifiable value as a substring.

  • .match(needle: StringPattern) -> String: Extracts
    the first matching part of the string for the given pattern.

    An empty string is returned if there is no match.

  • .starts_with(needle: Stringify) -> Boolean

  • .ends_with(needle: Stringify) -> Boolean

  • .remove_prefix(needle: Stringify) -> String: Removes the passed prefix, if
    present.

  • .remove_suffix(needle: Stringify) -> String: Removes the passed suffix, if
    present.

  • .trim() -> String: Removes leading and trailing whitespace

  • .trim_start() -> String: Removes leading whitespace

  • .trim_end() -> String: Removes trailing whitespace

  • .substr(start: Integer, [end: Integer]) -> String: Extract substring. The
    start / end indices should be specified in units of UTF-8 bytes. Indices are
    0-based and end is exclusive. Negative values count from the end of the
    string, with -1 being the last byte. If the start index is in the middle
    of a UTF-8 codepoint, the codepoint is fully part of the result. If the end
    index is in the middle of a UTF-8 codepoint, the codepoint is not part of the
    result. If end is not given, returns from start to the end of the string.

  • .first_line() -> String: Equivalent to .lines().first().

  • .lines() -> List<String>: Split into lines excluding newline characters.

  • .split(separator: StringPattern, [limit: Integer]) -> List<String>: Split
    into substrings by the given separator pattern. If limit is specified, it
    determines the maximum number of elements in the result, with the remainder of
    the string returned as the final element. A limit of 0 returns an empty
    list.

  • .replace(pattern: StringPattern, replacement: Stringify, [limit: Integer]) -> String: Replace occurrences of the given pattern with the replacement
    string.

    By default, all occurrences are replaced. If limit is specified, at most
    that many occurrences are replaced.

    Supports capture groups in patterns using $0 (entire match), $1, $2 etc.

  • .upper() -> String

  • .lower() -> String

  • .escape_json() -> String: Serializes the string in JSON format. This
    function is useful for making machine-readable templates. For example, you
    can use it in a template like '{ "foo": ' ++ foo.escape_json() ++ ' }' to
    return a JSON/JSONL.

`Stringify` type

An expression that can be converted to a String.

Any types that can be converted to Template can also be Stringify. Unlike
Template, color labels are stripped. Invalid UTF-8 sequences are rejected.

`StringLiteral` type

A string literal known at parse time. Unlike Stringify, this cannot be a
dynamic expression - it must be a literal value like "main" or "format".

String literals must be surrounded by single or double quotes (' or ").
A double-quoted string literal supports the following escape sequences:

  • \": double quote
  • \\: backslash
  • \t: horizontal tab
  • \r: carriage return
  • \n: new line
  • \0: null
  • \e: escape (i.e., \x1b)
  • \xHH: byte with hex value HH

Other escape sequences are not supported. Any UTF-8 characters are allowed
inside a string literal, with two exceptions: unescaped "-s and uses of \
that don't form a valid escape sequence.

A single-quoted string literal has no escape syntax. ' can't be expressed
inside a single-quoted string literal.

String literals have their own type so that the value can be validated at parse
time.

`StringPattern` type

Conversion: Boolean: no, Serialize: no, Template: no

These are the exact same as the String pattern type in revsets, except that
quotes are mandatory.

Literal strings may be used, which are interpreted as case-sensitive substring
matching.

Currently StringPattern values cannot be passed around as values and may
only occur directly in the call site they are used in.

`Template` type

Conversion: Boolean: no, Serialize: no, Template: yes

Most types can be implicitly converted to Template. No methods are defined.

`Timestamp` type

Conversion: Boolean: no, Serialize: yes, Template: yes

The following methods are defined.

  • .ago() -> String: Format as relative timestamp.
  • .format(format: Stringify) -> String: Format with the specified strftime-like
    format string
    .
  • .utc() -> Timestamp: Convert timestamp into UTC timezone.
  • .local() -> Timestamp: Convert timestamp into local timezone.
  • .after(date: StringLiteral) -> Boolean: True if the timestamp is exactly at or
    after the given date. Supported date formats are the same as the revset
    Date pattern type.
  • .before(date: StringLiteral) -> Boolean: True if the timestamp is before, but
    not including, the given date. Supported date formats are the same as the
    revset Date pattern type.
  • .since(start: Timestamp) -> TimestampRange: The TimestampRange between start and
    self. It may be used in conjunction with TimestampRange::duration to obtain
    a human-friendly duration between two Timestamps.

`TimestampRange` type

Conversion: Boolean: no, Serialize: yes, Template: yes

The following methods are defined.

  • .start() -> Timestamp
  • .end() -> Timestamp
  • .duration() -> String

`Trailer` type

Conversion: Boolean: no, Serialize: no, Template: yes

The following methods are defined.

  • .key() -> String
  • .value() -> String

`TreeDiff` type

Conversion: Boolean: no, Serialize: no, Template: no

This type cannot be printed. The following methods are defined.

  • .files() -> List: Changed files.
  • .color_words([context: Integer]) -> Template: Format as a word-level diff
    with changes indicated only by color.
  • .git([context: Integer]) -> Template: Format as a Git diff.
  • .stat([width: Integer], [max_bar_width: Integer]) -> DiffStats: Calculate
    stats of changed lines. width sets the entire width of the diff stat
    including paths, and max_bar_width limits the maximum character length of
    the ++-- bar.
  • .summary() -> Template: Format as a list of status code and path pairs.

`TreeDiffEntry` type

Conversion: Boolean: no, Serialize: no, Template: no

This type cannot be printed. The following methods are defined.

  • .path() -> RepoPath: Path to the entry. If the entry is a copy / rename, this
    points to the target (or right) entry.
  • .display_diff_path() -> String: Format path for display, taking into account copy / rename information.
  • .status() -> String: One of "modified", "added", "removed",
    "copied", or "renamed".
  • .status_char() -> String: Single-character status indicator: "M" for modified,
    "A" for added, "D" for removed, "C" for copied, or "R" for renamed.
  • .source() -> TreeEntry: The source (or left) entry.
  • .target() -> TreeEntry: The target (or right) entry.

`TreeEntry` type

Conversion: Boolean: no, Serialize: no, Template: no

This type cannot be printed. The following methods are defined.

  • .path() -> RepoPath: Path to the entry.
  • .normal_value() -> Option: The resolved value of the entry, or none
    if the entry is absent or conflicted. Here, "normal" means resolved, not just a
    regular file: symlinks, trees, and Git submodules are also included.
  • .conflict() -> Boolean: True if the entry is a merge conflict.
  • .conflict_side_count() -> Integer: Number of sides in the merge conflict (1 if not
    conflicted, 2 or more for multi-way merges).
  • .file_type() -> String: One of "file", "symlink", "tree",
    "git-submodule", or "conflict".
  • .executable() -> Boolean: True if the entry is an executable file.

`TreeValue` type

Conversion: Boolean: no, Serialize: no, Template: yes

A file, symlink, tree, or Git submodule value. Its default formatting is
its full hexadecimal object ID. For a Git submodule, this is the recorded commit
ID. No methods are defined.

`WorkspaceRef` type

Conversion: Boolean: no, Serialize: yes, Template: yes

The following methods are defined.

  • .name() -> RefSymbol: Returns the workspace name as a symbol.

  • .target() -> Commit: Returns the working-copy commit of this workspace.

  • .root() -> Option<FsPath>: Returns the workspace root path, if the root path
    is recorded.

    This is optional because workspaces created before jj 0.38.0 did not record
    workspace root paths.

Color labels

You can customize the output colors by using color labels. jj
adds some labels automatically; they can also be added manually.

Template fragments are usually automatically labeled with the command name,
the context (or the top-level object), and the method names. For example, the
following template is labeled as op_log operation id short automatically:

sh
jj op log -T 'self.id().short()'

The exact names of such labels are often straightforward, but are not currently
documented. You can discover the actual label names used with the
--color=debug option:

sh
jj op log -T 'self.id().short()' --color=debug

This can be useful if you are trying to figure out whether or not a label is
present or which labels you should define or override colors for to get the
output that you want.

Additionally, you can manually insert arbitrary labels using the
label(label, content) function. For example,

sh
jj op log -T '"ID: " ++ self.id().short().substr(0, 1) ++ label("id short", "<redacted>")'

will print "ID:" in the default style, and the string <redacted> in the same
style as the first character of the id. It would also be fine to use an
arbitrary template instead of the string "<redacted>", possibly including
nested invocations of label().

You are free to use custom label names as well. This will only have a visible
effect if you also customize their colors explicitly.

To see all the default colors and what customizations apply from your own
config, run:

sh
jj config list --include-defaults --include-overridden colors

The default colors can be found in colors.toml, and the default
explicitly added labels (such as mutable or divergent) can be found in
templates.toml. Labels that appear in colors.toml but that
are not explicitly present in templates.toml will have been automatically
added by jj (such as author or change_id).

Configuration

The default templates and aliases() are defined in the [templates] and
[template-aliases] sections of the config respectively. The exact definitions
can be seen in the templates.toml file.

New keywords, functions, and <name>:<value> patterns can be defined as
aliases, by using any combination of the predefined keywords/functions and other
aliases.

Alias functions can be overloaded by the number of parameters. However, builtin
functions will be shadowed by name, and can't co-exist with aliases.

For example:

toml
[template-aliases]
sh = "commit_id.short()"
'commit_change_ids' = '''
concat(
  format_field("Commit ID", commit_id),
  format_field("Change ID", change_id),
)
'''
'format_field(key, value)' = 'key ++ ": " ++ value ++ "\n"'
'json:x' = 'json(x) ++ "\n"'

Alias descriptions

Alias descriptions can be surfaced in shell completions by defining the alias
as a table with .doc and .definition properties. For example:

toml
[template-aliases]
sh = { definition = 'commit_id.short()', doc = 'Short commit ID' }

You can also use the dotted key syntax:

toml
[template-aliases]
sh.definition = 'commit_id.short()'
sh.doc = 'Short commit ID'

Examples

Print the object IDs on both sides of a diff:

sh
jj diff -T 'path ++ " " ++ self.source().normal_value() ++ " -> " ++ self.target().normal_value() ++ "\n"'

Get short commit IDs of the working-copy parents:

sh
jj log -G -r @ -T 'parents.map(|c| c.commit_id().short()).join(",")'

Show machine-readable list of full commit and change IDs:

sh
jj log -G -T 'commit_id ++ " " ++ change_id ++ "\n"'

Print the description of the current commit, defaulting to (no description set):

sh
jj log -G -r @ -T 'coalesce(description, "(no description set)\n")'

For each revision, print the name of its first bookmark or change ID:

sh
jj log -T 'try(bookmarks.first().name(), change_id.shortest(8)) ++ "\n"'

Testimonials (`docs/testimonials.md`)

Testimonials

You might not be ready to make the jump to Jujutsu yet. It's understandable; new
tools come with new lessons, failures, and ideas to absorb. They require
practice. In order to provide some motivation, we've collected a number of real,
100% authentic testimonials — from our loving users, our silly developers
— all to tip the scales and get you on our side!

What the users have to say

I've spent many years of my career working on version control. What I like
most about Jujutsu is how it has non-obvious solutions to UX problems that
we've run into in the past. What most people may not realize is that there are
many novel features which all interlock to make it easy to use.

For example, consider Jujutsu's support for automatically rebasing descendants
of amended revisions. When we implemented that in Mercurial, we ran into an
issue: what if there's a merge conflict? Our solution was to warn users and
just not perform the auto-rebase. Now, suddenly, users have to understand that
there can be old versions of the same revision visible in their log, and learn
how to fix this state.

In contrast, Jujutsu's solution is to simply make merge conflicts first-class.
This is not just an improvement in general, it is also specifically an
improvement for auto-rebase — users no longer have to learn about old
versions of a revision unless they want to look at the obslog.

Over and over, I'm struck by how well Jujutsu demonstrates this kind of
evolved thinking, which as an experienced version control developer I deeply
appreciate.

— Rain, engineer at Oxide Computer Company, former VCS developer

Jujutsu is amazing... I couldn't have come up with anything remotely as
elegant.

It's so rare that a solution attacks the innermost core of a problem so
thoroughly, I genuinely feel blessed to be in its presence. And also a bit
vindicated in not even trying to learn to use any of the tools that felt like
more crutches stacked upon a sand castle

— Anonymous user, speaking from the shadows

It's the easiest time I've ever had learning a tool this deeply this quickly,
because of the ability to experiment and undo, instead of triple-checking
before trying a new scary command.

— Scott Olson, advanced Git user and now a Jujutsu user

I initially started to use Jujutsu for personal repos, and it has quickly
gone from "neat, let's try this more" to "very neat, added to my permanent
config and automatically installed for new machines".

— Poliorcetics, on GitHub

when i worked on the rust compiler, my job was to chain together a bunch of
strange and cursed tools that broke often. jujutsu breaks about half as
much, so that's pretty good i guess

— jyn514, Rust contributor

Jujutsu is pretty cool, you can even keep most of your existing workflows

— Ben, who doesn't want you keeping your existing workflow

Wait, it's not called Jujitsu?

— Phil, Mercurial contributor (who doesn't have to learn Git, now that
Jujutsu exists)

When I heard about Jujutsu I decided to try it out before forming an opinion.
Technically it never formed, because I haven't considered going back.

— gul banana, computer programmer

muwhahaha, I have corrupted my cofounder and now 100% of brontosource dev is on JJ.

— Matt Kulunkundis, cofounder BrontoSource

jj is genuinely the first tool since Rust that has me excited.

— David Barsky, rust-analyzer and tokio-rs/tracing developer

Today I did some complex surgery on a legacy repo that has been committed to
for years by a number of individuals with varying hygienic habits. [..]

Now, this still took me about two hours using jj. But at no point did I
seriously consider physical violence to humans or inanimate objects.
Everything progressed calmly and methodically. I was able to introspect
changes before I made them, and when I did screw up… jj undo to the
rescue.

— An unknown scientist, who couldn't have done it with Git.

jj was my first exposure to revsets and at first I was
"why would I ever need that", but now after exercising that math part of my
brain that's been stale since high school, they're indispensable

— Marijan Smetko, who previously only knew Git.

I think my favorite thing about jj is it makes the way I abused Git via
lazygit the standard way of doing things

— Isaac Corbrey, reformed Git user

What the developers have to say

I've been a FOSS contributor using Git for over 16 years, and Jujutsu
continues to amaze me every day. It has that sweet simplicity I was fond of in
Darcs, but it boils down all my most core and fundamental workflows —
developed over years of experience — into a simple set of primitives.
The internal design is simple and beautiful; it looks like a database, making
the implementation elegant, safe, and extensible. All this, using the same Git
repositories my coworkers use.

It's like if you found out one day that you built your entire home on a vein
of rich gold. Every day I seem to find new and beautiful emergent behaviors,
all adding up to a tool that is greater than the sum of its parts.

— Austin Seipp, "No 1. Jujutsu Fan"

Honestly, I implemented signing support mostly for that sweet dopamine hit
that you get from the green checkmark on GitHub. Yeah.

— Anton Bulakh, contributor and dopamine enthusiast

I'm sometimes still surprised that navigating with jj next and jj prev
works.

— Philip Metzger, author of jj next and jj prev

I'm surprised when it works.

— Martin von Zweigbergk, project creator and leader

Spread the word yourself

Are you satisfied with Jujutsu? Ready to recommend it to a Jujillion of your
friends and coworkers? Great! The easiest way to help the project grow is word
of mouth. So make sure to talk to them about it and show off your hip new tool.
Maybe post a link to it on your other favorite tool that you love using, Slack?

If you're not sure what to say, we hired the cheapest marketing team we could
find to design a list of Pre-Approved Endorsements in their laboratory. Just
copy and paste these right into a text box! Shilling for an open source project
has never been easier than this.

Jujutsu is an alright tool. I guess.

Jujutsu is my favorite software tool of all time. I am saying this for no
particular reason, definitely not because I was paid to.

I love Jujutsu. I love Jujutsu. I love Jujutsu. I love Jujutsu. I love
Jujutsu. I love Jujutsu. I love Jujutsu. I love Jujutsu.


Tutorial (`docs/tutorial.md`)

Tutorial

Hint: This tutorial covers the basics accurately, but since it was
written Jujutsu has gained many new features. For a tutorial covering a wider
range of features, you may find the (not quite finished) tutorial by Steve
Klabnik
helpful. If you're
looking for a tutorial that doesn't require experience with Git, take a look
at Jujutsu for everyone.

This text assumes that the reader is familiar with Git.

Preparation

If you haven't already, make sure you
install and configure Jujutsu.

Cloning a Git repository

Hint: Most identifiers used in this tutorial will be different when you
try this at home!

Let's start by cloning GitHub's Hello-World repo using jj:

shell
# Note the "git" before "clone" (there is no support for cloning native jj
# repos yet)
$ jj git clone https://github.com/octocat/Hello-World
Fetching into new repo in "/tmp/tmp.O1DWMiaKd4/Hello-World"
remote: Enumerating objects: 13, done.
remote: Total 13 (delta 0), reused 0 (delta 0), pack-reused 13 (from 1)
bookmark: master@origin          [new] untracked
bookmark: octocat-patch-1@origin [new] untracked
bookmark: test@origin            [new] untracked
Setting the revset alias `trunk()` to `master@origin`
Working copy  (@) now at: kntqzsqt d7439b06 (empty) (no description set)
Parent commit (@-)      : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1
Added 1 files, modified 0 files, removed 0 files
$ cd Hello-World

Running jj st (short for jj status) now yields something like this:

shell
$ jj st
The working copy has no changes.
Working copy  (@) : kntqzsqt d7439b06 (empty) (no description set)
Parent commit (@-): orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1

Let's look at that output as it introduces new concepts. You can see two
commits: Parent and working copy. Both are identified using two separate
identifiers: the "change ID" and the "commit ID".

The parent commit, for example, has the change ID orrkosyo and the commit ID
7fd1a60b.

Git users: The commit ID/hash is what you're used to from Git and should
match what you see when you look at the repository using git log in a Git
checkout of the repository.
The change ID however, is a new concept, unique to Jujutsu.

We can also see from the output above that our working copy is an actual commit
with a commit ID (d7439b06 in the example). When you make a change in the
working copy, the working-copy commit gets automatically amended by the next
jj command.

Git users: This is a huge difference from Git where the working copy is a
separate concept and not yet a commit.

Changes

A change is a commit that can evolve while keeping a stable identifier (similar
to Gerrit's Change-Id). In other words: You can make changes to files in a
change, resulting in a new commit hash, but the change ID will remain the same.

You can see that our clone operation automatically created a new change:

shell
Working copy  (@) : kntqzsqt d7439b06 (empty) (no description set)

This new change has the ID kntqzsqt and it is currently empty (contains no
changes compared to the parent) and has no description.

Creating our first change

Let's say we want to edit the README file in the repo to say "Goodbye"
instead of "Hello". Start by describing the change (adding a commit message) so
we don't forget what we're working on:

shell
# This brings up $EDITOR (or `nano` or `Notepad` by default).
# Enter something like "Say goodbye" in the editor and then save the file and close
# the editor.
$ jj describe
Working copy  (@) now at: kntqzsqt e427edcf (empty) Say goodbye
Parent commit (@-)      : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1

Now make the change in the README:

shell
# Adjust as necessary for compatibility with your flavor of `sed`
$ sed -i 's/Hello/Goodbye/' README
$ jj st
Working copy changes:
M README
Working copy  (@) : kntqzsqt 5d39e19d Say goodbye
Parent commit (@-): orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1

Note that you didn't have to tell Jujutsu to add the change like you would with
git add. You actually don't even need to tell it when you add new files or
remove existing files. To untrack a path, add it to your .gitignore and run
jj file untrack .

Also note that the commit hash for our current change (kntqzsqt) changed from
e427edcf to 5d39e19d!

To see the diff, run jj diff:

shell
$ jj diff --git  # Feel free to skip the `--git` flag
diff --git a/README b/README
index 980a0d5f19..1ce3f81130 100644
--- a/README
+++ b/README
@@ -1,1 +1,1 @@
-Hello World!
+Goodbye World!

Jujutsu's diff format currently defaults to inline coloring of the diff (like
git diff --color-words), so we used --git above to make the diff readable in
this tutorial.

As you may have noticed, the working-copy commit's ID changed both when we
edited the description and when we edited the README. However, the parent commit
stayed the same. Each change to the working-copy commit amends the previous
version. So how do we tell Jujutsu that we are done amending the current change
and want to start working on a new one? That is what jj new is for. That will
create a new commit on top of your current working-copy commit. The new commit
is for the working-copy changes.

So, let's say we're now done with this change, so we create a new change:

shell
$ jj new
Working copy  (@) now at: mpqrykyp aef4df99 (empty) (no description set)
Parent commit (@-)      : kntqzsqt 5d39e19d Say goodbye
$ jj st
The working copy has no changes.
Working copy  (@) : mpqrykyp aef4df99 (empty) (no description set)
Parent commit (@-): kntqzsqt 5d39e19d Say goodbye

If we later realize that we want to make further changes, we can make them in
the working copy and then run jj squash. That command squashes (moves) the
changes from a given commit into its parent commit. Like most commands, it acts
on the working-copy commit by default. When run on the working-copy commit, it
behaves very similar to git commit --amend.

Alternatively, we can use jj edit <commit> to resume editing a commit in the
working copy. Any further changes in the working copy will then amend the
commit. Whether you choose to create a new change and squash, or to edit,
typically depends on how done you are with the change; if the change is almost
done, it makes sense to use jj new so you can easily review your adjustments
with jj diff before running jj squash.

To view how a change has evolved over time, we can use jj evolog to see each
recorded change for the current commit. This records changes to the working
copy, message, squashes, rebases, etc.

The log command and "revsets"

You're probably familiar with git log. Jujutsu has very similar functionality
in its jj log command:

shell
$ jj log
@  mpqrykyp [email protected] 2023-02-12 15:00:22 aef4df99
│  (empty) (no description set)
○  kntqzsqt [email protected] 2023-02-12 14:56:59 5d39e19d
│  Say goodbye
◆  orrkosyo [email protected] 2012-03-06 15:06:50 master 7fd1a60b
│  (empty) Merge pull request #6 from Spaceghost/patch-1
~

The @ indicates the working-copy commit. The first ID on a line
(e.g. "mpqrykyp" above) is the change ID. The second ID is the commit ID. You
can give either ID to commands that take revisions as arguments. We will
generally prefer change IDs because they stay the same when the commit is
rewritten.

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 --revisions/-r flag to select a different set
of revisions to list. The flag accepts a "revset", which is an
expression in a simple language for specifying revisions. For example, @
refers to the working-copy commit, root() refers to the root commit,
bookmarks() refers to all commits pointed to by bookmarks (similar to Git's
branches). We can combine expressions with | for union, & for intersection
and ~ for difference. For example:

shell
$ jj log -r '@ | root() | bookmarks()'
@  mpqrykyp [email protected] 2023-02-12 15:00:22 aef4df99
│  (empty) (no description set)
~  (elided revisions)
◆  orrkosyo [email protected] 2012-03-06 15:06:50 master 7fd1a60b
│  (empty) Merge pull request #6 from Spaceghost/patch-1
~  (elided revisions)
◆  zzzzzzzz root() 00000000

The 00000000 commit (change ID zzzzzzzz) is a virtual commit that's called
the "root commit". It's the root commit of every repo. The root()
function in the revset matches it.

There are also operators for getting the parents (foo-), children (foo+),
ancestors (::foo), descendants (foo::), DAG range (foo::bar, like
git log --ancestry-path), range (foo..bar, same as Git's). See
the revset documentation for all revset operators and functions.

Hint: If the default jj log omits some commits you expect to see, you
can always run jj log -r :: (or, equivalently, jj log -r 'all()') to see
all the commits.

Conflicts

Now let's see how Jujutsu deals with merge conflicts. We'll start by making some
commits. We use jj new with the --message/-m option to set change
descriptions (commit messages) right away.

shell
# Start creating a chain of commits off of the `master` bookmark
$ jj new master -m A; echo a > file1
Working copy  (@) now at: nuvyytnq 00a2aeed (empty) A
Parent commit (@-)      : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1
Added 0 files, modified 1 files, removed 0 files
$ jj new -m B1; echo b1 > file1
Working copy  (@) now at: ovknlmro 967d9f9f (empty) B1
Parent commit (@-)      : nuvyytnq 5dda2f09 A
$ jj new -m B2; echo b2 > file1
Working copy  (@) now at: puqltutt 8ebeaffa (empty) B2
Parent commit (@-)      : ovknlmro 7d7c6e6b B1
$ jj new -m C; echo c > file2
Working copy  (@) now at: qzvqqupx 62a3c6d3 (empty) C
Parent commit (@-)      : puqltutt daa6ffd5 B2
$ jj log
@  qzvqqupx [email protected] 2023-02-12 15:07:41 2370ddf3
│  C
○  puqltutt [email protected] 2023-02-12 15:07:33 daa6ffd5
│  B2
○  ovknlmro [email protected] 2023-02-12 15:07:24 7d7c6e6b
│  B1
○  nuvyytnq [email protected] 2023-02-12 15:07:05 5dda2f09
│  A
│ ○  kntqzsqt [email protected] 2023-02-12 14:56:59 5d39e19d
├─╯  Say goodbye
◆  orrkosyo [email protected] 2012-03-06 15:06:50 master 7fd1a60b
│  (empty) Merge pull request #6 from Spaceghost/patch-1
~

We now have a few commits, where A, B1, and B2 modify the same file, while C
modifies a different file. Let's now rebase B2 directly onto A. We use the
--source/-s option on the change ID of B2, and --onto/-o option on A.

shell
$ jj rebase -s puqltutt -o nuvyytnq  # Replace the IDs by what you have for B2 and A
Rebased 2 commits to destination
Working copy  (@) now at: qzvqqupx 1978b534 (conflict) C
Parent commit (@-)      : puqltutt f7fb5943 (conflict) B2
Added 0 files, modified 1 files, removed 0 files
Warning: There are unresolved conflicts at these paths:
file1    2-sided conflict
New conflicts appeared in 2 commits:
  qzvqqupx 1978b534 (conflict) C
  puqltutt f7fb5943 (conflict) B2
Hint: To resolve the conflicts, start by creating a commit on top of
the first conflicted commit:
  jj new puqltutt
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you can inspect the result with `jj diff`.
Then run `jj squash` to move the resolution into the conflicted commit.

$ jj log
@  qzvqqupx [email protected] 2023-02-12 15:08:33 1978b534 (conflict)
│  C
×  puqltutt [email protected] 2023-02-12 15:08:33 f7fb5943 (conflict)
│  B2
│ ○  ovknlmro [email protected] 2023-02-12 15:07:24 7d7c6e6b
├─╯  B1
○  nuvyytnq [email protected] 2023-02-12 15:07:05 5dda2f09
│  A
│ ○  kntqzsqt [email protected] 2023-02-12 14:56:59 5d39e19d
├─╯  Say goodbye
◆  orrkosyo [email protected] 2012-03-06 15:06:50 master 7fd1a60b
│  (empty) Merge pull request #6 from Spaceghost/patch-1
~

There are several things worth noting here. First, the jj rebase command said
"Rebased 2 commits". That's because we asked it to rebase commit B2 with the
-s option, which also rebases descendants (commit C in this case). Second,
because B2 modified the same file (and word) as B1, rebasing it resulted in
conflicts, as the output indicates. Third, the conflicts did not prevent the
rebase from completing successfully, nor did it prevent C from getting rebased
on top.

Now let's resolve the conflict in B2. We'll do that by creating a new commit on
top of B2. Once we've resolved the conflict, we'll squash the conflict
resolution into the conflicted B2. That might look like this:

text
/* Detailed source-code truncated for AI context efficiency. */

Note that commit C automatically got rebased on top of the resolved B2, and that
C is also resolved (since it modified only a different file).

By the way, if we want to get rid of B1 now, we can run jj abandon ovknlmro. That will hide the commit from the log output and will rebase any
descendants to its parent.

The operation log

Jujutsu keeps a record of all changes you've made to the repo in what's called
the "operation log". Use the jj op (short for jj operation) family of
commands to interact with it. To list the operations, use jj op log:

shell
$ jj op log
@  d3b77addea49 [email protected] 3 minutes ago, lasted 3 milliseconds
│  squash commits into f7fb5943a6b9460eb106dba2fac5cac1625c6f7a
│  args: jj squash
○  6fc1873c1180 [email protected] 3 minutes ago, lasted 1 milliseconds
│  snapshot working copy
│  args: jj st
○  ed91f7bcc1fb [email protected] 6 minutes ago, lasted 1 milliseconds
│  new empty commit
│  args: jj new puqltutt
○  367400773f87 [email protected] 12 minutes ago, lasted 3 milliseconds
│  rebase commit daa6ffd5a09a8a7d09a65796194e69b7ed0a566d and descendants
│  args: jj rebase -s puqltutt -o nuvyytnq
[many more lines]

The most useful command is jj undo, which will undo your last operation.

shell
$ jj undo
Reverted operation: d3b77addea49 (2025-05-12 00:27:27) squash commits into f7fb5943a6b9460eb106dba2fac5cac1625c6f7a
Working copy  (@) now at: zxoosnnp 63874fe6 (no description set)
Parent commit (@-)      : puqltutt f7fb5943 (conflict) B2
New conflicts appeared in 2 commits:
  qzvqqupx 1978b534 (conflict) C
  puqltutt f7fb5943 (conflict) B2
Hint: To resolve the conflicts, start by creating a commit on top of
the first conflicted commit:
  jj new puqltutt
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you can inspect the result with `jj diff`.
Then run `jj squash` to move the resolution into the conflicted commit.

$ jj log
@  zxoosnnp [email protected] 2023-02-12 19:34:09 63874fe6
│  (no description set)
│ ×  qzvqqupx [email protected] 2023-02-12 15:08:33 1978b534 (conflict)
├─╯  C
×  puqltutt [email protected] 2023-02-12 15:08:33 f7fb5943 (conflict)
│  B2
│ ○  ovknlmro [email protected] 2023-02-12 15:07:24 7d7c6e6b
├─╯  B1
○  nuvyytnq [email protected] 2023-02-12 15:07:05 5dda2f09
│  A
│ ○  kntqzsqt [email protected] 2023-02-12 14:56:59 5d39e19d
├─╯  Say goodbye
◆  orrkosyo [email protected] 2012-03-06 15:06:50 master 7fd1a60b
│  (empty) Merge pull request #6 from Spaceghost/patch-1
~

As you can perhaps see, that undid the jj squash invocation we used for
squashing the conflict resolution into commit B2 earlier. Notice that it also
updated the working copy.

You can also view the repo the way it looked after some earlier operation. For
example, if you want to see jj log output right after the jj rebase
operation, try jj log --at-op=367400773f87 but use the hash from your own
jj op log.

Moving content changes between commits

You have already seen how jj squash can combine the changes from two commits
into one. There are several other commands for changing the contents of existing
commits.

We'll need some more complex content to test these commands, so let's create a
few more commits:

shell
$ jj new master -m abc; printf 'a\nb\nc\n' > file
Working copy  (@) now at: ztqrpvnw f94e49cf (empty) abc
Parent commit (@-)      : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1
Added 0 files, modified 0 files, removed 1 files

$ jj new -m ABC; printf 'A\nB\nc\n' > file
Working copy  (@) now at: kwtuwqnm 6f30cd1f (empty) ABC
Parent commit (@-)      : ztqrpvnw 51002261 abc

$ jj new -m ABCD; printf 'A\nB\nC\nD\n' > file
Working copy  (@) now at: mrxqplyk a6749154 (empty) ABCD
Parent commit (@-)      : kwtuwqnm 30aecc08 ABC

$ jj log -r master::@
@  mrxqplyk [email protected] 2023-02-12 19:38:21 b98c607b
│  ABCD
○  kwtuwqnm [email protected] 2023-02-12 19:38:12 30aecc08
│  ABC
○  ztqrpvnw [email protected] 2023-02-12 19:38:03 51002261
│  abc
◆  orrkosyo [email protected] 2012-03-06 15:06:50 master 7fd1a60b
│  (empty) Merge pull request #6 from Spaceghost/patch-1
~

We "forgot" to capitalize "c" in the second commit when we capitalized the other
letters. We then fixed that in the third commit when we also added "D". It would
be cleaner to move the capitalization of "c" into the second commit. We can do
that by running jj squash with the --interactive/-i option on the third
commit. Remember that jj squash moves all the changes from one commit into its
parent. jj squash -i moves only part of the changes into its parent. Now try
that:

shell
$ jj squash -i
Hint: Using default editor ':builtin'; run `jj config set --user ui.diff-editor :builtin` to disable this message.
Rebased 1 descendant commits
Working copy  (@) now at: mrxqplyk 52a6c7fd ABCD
Parent commit (@-)      : kwtuwqnm 643061ac ABC

That will bring up the built-in diff editor[^alternative_diff_editors] with a
diff of the changes in the "ABCD" commit. Expand the file by clicking on (+)
or with right arrow, then select the sections/line to include by clicking or
using space. Once complete, press c to confirm changes, or q to exit without
saving. You can also use the mouse to click on the menu items to see more
options (keyboard navigation is currently limited).

[^alternative_diff_editors]: There are many other diff editors you could use.
For example, if you have Meld installed and in the
PATH, you can use it via jj squash -i --tool meld or a fancier config with jj squash -i --tool meld-3. You can configure the default with the
ui.diff-editor option; those docs also explain how
to specify a path to an executable if it is not in the PATH.

If we look at the diff of the second commit, we now see that all three lines got
capitalized:

shell
$ jj diff -r @- --git
diff --git a/file b/file
index de980441c3..b1e67221af 100644
--- a/file
+++ b/file
@@ -1,3 +1,3 @@
-a
-b
-c
+A
+B
+C

The child change ("ABCD" in our case) will have the same content state after
the jj squash command. That means that you can move any changes you want into
the parent change, even if they touch the same word, and it won't cause any
conflicts.

Let's try one final command for changing the contents of an existing commit. That
command is jj diffedit, which lets you edit the changes in a commit without
checking it out.

shell
$ jj diffedit -r @-
Hint: Using default editor ':builtin'; run `jj config set --user ui.diff-editor :builtin` to disable this message.
Rebased 1 descendant commits
Working copy  (@) now at: mrxqplyk 1c72cd50 (conflict) ABCD
Parent commit (@-)      : kwtuwqnm 70985eaa ABC
Added 0 files, modified 1 files, removed 0 files
Warning: There are unresolved conflicts at these paths:
file    2-sided conflict
New conflicts appeared in 1 commits:
  mrxqplyk 1c72cd50 (conflict) ABCD
Hint: To resolve the conflicts, start by creating a commit on top of
the conflicted commit:
  jj new mrxqplyk
Then use `jj resolve`, or edit the conflict markers in the file directly.
Once the conflicts are resolved, you can inspect the result with `jj diff`.
Then run `jj squash` to move the resolution into the conflicted commit.

In the diff editor, use the arrow keys and spacebar to select all lines but the
last. Press 'c' to save the changes and close it. You can now inspect the
rewritten commit with jj diff -r @- again, and you should see your deletion of
the last line. Unlike jj squash -i, which left the content state of the commit
unchanged, jj diffedit (typically) results in a different state, which means
that descendant commits may have conflicts.

Another command for rewriting contents of existing commits is jj split. Now
that you've seen how jj squash -i and jj diffedit work, you can hopefully
figure out how it works (with the help of the instructions in the diff).

Using an existing Git repository

If you already have a local Git checkout, you can start using Jujutsu in that
directory instead of cloning:

shell
$ cd path/to/my-git-repo
$ jj git init

This creates a colocated
workspace
(.jj/ next to
.git/), so you can use both jj and git commands in the same directory. See
Creating a repo backed by an existing Git
repo
for
more details.


Windows (`docs/windows.md`)

Working on Windows

Jujutsu works the same on all platforms, but there are some caveats that Windows
users should be aware of.

Line endings conversion

Jujutsu has a setting,
working-copy.eol-conversion, similar to
Git's core.autocrlf[^1], but does not currently honor
.gitattributes and the core.autocrlf Git config, so it is recommended to
keep the working-copy.eol-conversion setting and the core.autocrlf Git
config in sync[^1].

!!! note

text
If you created a colocated workspace and forgot to keep these two settings
in sync, resulting in a dirty working copy with only EOL diffs, you can set
the `working-copy.eol-conversion` setting correctly and run `jj abandon` to
fix it.

Regardless of this setting, the line endings conversion is skipped on binary
files based on a heuristic. This behavior is
subject to change when we support the text Git attribute.

Jujutsu may incorrectly determine whether a file is a binary file and apply line
endings conversion incorrectly, but currently, Jujutsu doesn't support per-file
configuration of line endings conversion. If this occurs, you can disable line
endings conversion by setting the working-copy.eol-conversion config to none
or just removing the config altogether.

powershell
PS> git config core.autocrlf input
# Use `none` instead of `input` to avoid applying EOL conversion.
PS> jj config set --repo working-copy.eol-conversion none
# Abandoning the working copy will cause Jujutsu to overwrite all files with
# CRLF line endings with the line endings they were committed with, probably LF.
PS> jj abandon

This means that line endings will be checked out exactly as they are committed
and committed exactly as authored, ensuring Git will check out files with LF
line endings without converting them to CRLF. You'll want to make sure any
tooling you use, especially IDEs, preserve LF line endings.

[^1]: This poses the question of whether we should support reading the
core.autocrlf setting in colocated workspaces. See details at the
issue.

Pagination

On Windows, jj will use its integrated pager called streampager by default,
unless the config ui.pager is explicitly set. See the pager section of the
config docs
for more details.

If the built-in pager doesn't meet your needs and you have Git installed, you
can switch to using Git's pager as follows:

powershell
PS> jj config set --user ui.pager '["C:\\Program Files\\Git\\usr\\bin\\less.exe", "-FRX"]'
PS> jj config set --user ui.paginate auto

Typing `@` in PowerShell

PowerShell uses @ as part the array sub-expression operator, so it
often needs to be escaped or quoted in commands:

powershell
PS> jj log -r `@
PS> jj log -r '@'

One solution is to create a revset alias. For example, to make HEAD an alias
for @:

powershell
PS> jj config set --user revset-aliases.HEAD '@'
PS> jj log -r HEAD

WSL sets the execute bit on all files

When viewing a Windows drive from WSL (via /mnt/c or a similar path), Windows
exposes all files with the execute bit set. Since Jujutsu automatically records
changes to the working copy, this sets the execute bit on all files committed in
your repository.

If you only need to access the repository in WSL, the best solution is to clone
the repository in the Linux file system (for example, in
~/my-repo).

If you need to use the repository in both WSL and Windows, one solution is to
create a workspace in the Linux file system:

powershell
PS> jj workspace add --name wsl ~/my-repo

Then only use the ~/my-repo workspace from Linux.

jj supports symlinks on Windows only when they are enabled by the operating
system. This requires Windows 10 version 14972 or higher, as well as Developer
Mode. If those conditions are not satisfied, jj will materialize symlinks as
ordinary files.

For colocated workspaces, Git support must also be enabled using the
git config option core.symlinks=true.


Working Copy (`docs/working-copy.md`)

Working copy

Introduction

The working copy is where the current working-copy commit's files are written so
you can interact with them. It is also where files are read from in order to
create new commits (though there are many other ways of creating new commits).

Unlike most other VCSs, Jujutsu will automatically create commits from the
working-copy contents when they have changed. Most jj commands you run will
commit the working-copy changes if they have changed. The resulting revision
will replace the previous working-copy revision.

Also unlike most other VCSs, added files are implicitly tracked by default. That
means that if you add a new file to the working copy, it will be automatically
committed once you run e.g. jj st. Similarly, if you remove a file from the
working copy, it will implicitly be untracked.

The snapshot.auto-track config option controls which paths get automatically
tracked when they're added to the working copy. See the
fileset documentation for the syntax. Files with paths matching
ignore files are never tracked automatically.

If you set snapshot.auto-track to a non-default value, untracked files can be
tracked with jj file track.

You can use jj file untrack to untrack a file while keeping it in the working
copy. However, first ignore them or remove them from the
snapshot.auto-track patterns; otherwise they will be immediately tracked again.

Conflicts

When you check out a commit with conflicts, those conflicts need to be
represented in the working copy somehow. However, the file system doesn't
understand conflicts. Jujutsu's solution is to add conflict markers to
conflicted files when it writes them to the working copy. It also keeps track of
the (typically 3) different parts involved in the conflict. Whenever it scans
the working copy thereafter, it parses the conflict markers and recreates the
conflict state from them. You can resolve conflicts by replacing the conflict
markers by the resolved text. You don't need to resolve all conflicts at once.
You can even resolve part of a conflict by updating the different parts of the
conflict marker.

To resolve conflicts in a commit, use jj new <commit> to create a working-copy
commit on top. You would then have the same conflicts in the working-copy
commit. Once you have resolved the conflicts, you can inspect the conflict
resolutions with jj diff. Then run jj squash to move the conflict
resolutions into the conflicted commit. Alternatively, you can edit the commit
with conflicts directly in the working copy by using jj edit <commit>. The
main disadvantage of that is that it's harder to inspect the conflict
resolutions.

With the jj resolve command, you can use an external merge tool to resolve
conflicts that have 2 sides and a base. There is not yet a good way of
resolving conflicts between directories, files, and symlinks
(https://github.com/jj-vcs/jj/issues/19). You can use jj restore to choose
one side of the conflict, but there's no way to even see where the involved
parts came from.

Ignored files

You probably don't want build outputs and temporary files to be under version
control. You can tell Jujutsu to not automatically track certain files by using
.gitignore files (there's no such thing as .jjignore yet). See
https://git-scm.com/docs/gitignore for details about the format. .gitignore
files are supported in any directory in the working copy, as well as in
$XDG_CONFIG_HOME/git/ignore and $GIT_DIR/info/exclude.

Ignored files are never tracked automatically (regardless of the value of
snapshot.auto-track), but files that were already tracked will remain tracked
even if they match ignore patterns. You can untrack such files with the
jj file untrack command.

Workspaces

You can have multiple working copies backed by a single repo. Use
jj workspace add to create a new working copy. The working copy will have a
.jj/ directory linked to the main repo. The working copy and the .jj/
directory together is called a "workspace". Each workspace can have a different
commit checked out.

Having multiple workspaces can be useful for running long-running tests in one
while you continue developing in another, for example. If needed,
jj workspace root --name <workspace> prints the root path of the specified
workspace (defaults to the current one).

jj workspace list shows every workspace together with its available root path.

When you're done using a workspace, use jj workspace forget to make the repo
forget about it. The workspace directory and files are left on disk. This is
useful when the workspace path is no longer accessible or when you want to keep
the files around.

Use jj workspace remove to forget a workspace and remove its directory from
disk. Tracked changes are snapshotted before the directory is removed, but
ignored files in that directory are deleted. The main workspace cannot be
removed. Removing the current workspace works on Unix, but Windows won't remove
a directory that a process is running in; run the command from another directory
using jj -R instead.

Stale working copy

Almost all commands go through three main steps:

  1. Snapshot the working copy (which gets recorded as an operation)
  2. Create new commits etc. "in memory" and record that as a new operation
  3. Update the working copy to match the new operation, i.e. to the commit that
    the operation says that @ should point to

If step 3 doesn't happen for some reason, the working copy is considered
"stale". We can detect that because the working copy (.jj/working_copy/)
keeps track of which operation it was last updated to. When the working copy is
stale, use jj workspace update-stale to update the files in the working copy.

A common reason that step 3 doesn't happen for a working copy is that you
rewrote the commit from another workspace. When you modify workspace A's
working-copy commit from workspace B, workspace A's working copy will become
stale.

A working copy can also become stale because some error, such as ^C prevented
step 3 from completing. It's also possible that it was successfully updated in
step 3 but the operation has then been lost (e.g. by jj op abandon or
"spontaneously" by certain storage backends). If the operation has been lost,
then jj workspace update-stale will create a recovery commit with the
contents of the working copy but parented to the current operation's
working-copy commit.


Design Copy Tracking (`docs/design/copy-tracking.md`)

Copy Tracking and Tracing Design

Authors: Daniel Ploch, Martin von Zweigbergk

Summary: This Document documents an approach to tracking and detecting copy
information in jj repos, in a way that is compatible with both Git's detection
model and with custom backends that have more complicated tracking of copy
information. This design affects the output of diff commands as well as the
results of rebasing across remote copies.

Objective

Add support for copy information that is sufficient for at least the following
use cases:

  • Diffing: If a file has been copied, show a diff compared to the source version
    instead of showing a full addition.
  • Merging: When one side of a merge (or rebase) has renamed a file and the other
    side has modified it, propagate the changes to the other side. (There are many
    other cases to handle too.)
  • Log: It should be possible to run something like jj log -p <file> and follow
    the file backwards when it had been created by copying.
  • Annotate (blame): Similar to the log use case, we should follow the file
    backwards when it had been created by copying.

The solution should support recording and retrieving copy info in a way that
is performant both for Git, which synthesizes copy info on the fly between
arbitrary trees, and for custom backends which may explicitly record and
re-serve copy info over arbitrarily large commit ranges.

The APIs should be defined in a way that makes it easy for custom backends to
ignore copy info entirely until they are ready to implement it.

Desired UX

The following sections describe some scenarios and how we would ideally handle
them.

We have not seen much reason to distinguish copies from renames, so a rename
is simply the same thing as a copy plus a deletion. This means that we cannot
distinguish "copy foo to bar and rename foo to baz" from "copy foo
to baz and rename foo to bar".

Restoring from a commit should preserve copies

For example, jj new X--; jj restore --from X should restore any copies
made in X- and X into the new working copy. Transitive copies should
be "flattened". For example, if X- renamed foo to bar and X renamed
bar to baz, then the restored commit should rename foo to baz.

This also applies to reparenting in general, such as for
"verbatim rebase".

Diff after restore

jj restore --from X; jj diff --from X should be empty, at least when it comes
to file contents. It may indicate that renamed file have different history.

Lossless round-trip of rebase

Except for the A+(A-B)=A rule, rebasing is currently never
lossy; rebasing a commit and then rebasing it back yields the same content. We
should ideally preserve this property when possible.

For example:

console
$ jj log
C rename bar->baz
|
B rename foo->bar
|
A add foo

$ jj rebase -r C -o A
$ jj rebase -r C -o B # Takes us back to the state above

Reverting the parent commit should be a no-op

Patches should be reversible so you can make a change and then revert it, and
end up with an empty diff across both commits.

For example:

console
$ jj log
B rename foo->bar
|
A add foo

$ jj revert -r B -o B
$ jj diff --from B- --to B+ # Should be empty

Parallelize/serialize

This is a special case of the lossless rebase.

console
$ jj log
E edit qux
|
D rename baz->qux
|
C rename bar->baz
|
B rename foo->bar
|
A add foo

$ jj parallelize B::D
# There should be no conflict in E and it should look like a
# regular edit just like before
$ jj rebase -r C -A B
$ jj rebase -r D -A C
# Now we're back to the same graph as before.

Copies inside merge commit

We should be able to resolve a naming conflict:

console
$ jj log
D  resolve naming conflict by choosing `foo` as the source
|\
C | rename bar->baz
| |
| B rename foo->baz
|/
A add foo and bar

$ jj file annotate baz # Should not include changes from C

We should also be able to back out that resolution and get back into the
name-conflicted state.

We should be able to rename files that exist on only one side:

console
$ jj log
D  rename foo2->foo3 and bar2->bar3
|\
C | rename bar->bar2
| |
| B rename foo->foo2
|/
A add foo and bar

Copies across merge commit

console
$ jj log
D delete baz
|\
C | rename foo->baz
| |
| B rename foo->bar
|/
A add foo

jj diff --from C --to D should now show a baz->bar rename (just like
jj diff --from C --to B would). jj diff --from B --to D should show
no renames. That's despite there being a rename in C.

High-level Design

Jujutsu uses a snapshot-based model similar to Git's. The algebra for our
first-class conflicts is also based on snapshots and being able to calculate
patches as differences between states. That means that we have to fit copy
information into that snapshot-based model too [^martinvonz_slow].

The proposal is to update tree objects to also contain information about a
file's past names. For example, if file foo gets renamed to bar in one
commit and then to baz in another commit, we will record that baz previously
had names bar and foo.

To support merging two files into one, the list of past names is actually a DAG.
Merging can happen in a merge commit when two sides copy/rename different
source files to the same target file. By having support for it in the model, we
can also support merging multiple files into one in a regular non-merge commit.

To avoid having to store all past paths in the tree object entry, we will write
the copy history as an object and the tree will refer to the object by ID. Each
ID refers to a node in the copy history DAG, similar to how commit IDs refer to
a node in the commit DAG.

Each node in the copy history DAG stores the path. Having the path in the copy
graph can be useful for finding copy sources without having to scan the whole
tree or having to ask the backend.

If we use only the file name as only input to the ID, then we get deterministic
tree IDs. On the other hand, if we add a salt to the copy graph node, then we
can represent that a file was rewritten from scratch. For example, a foo might
have copy ID 123 in the previous commit and when the file gets rewritten in
the current commit, it gets copy ID 456 even though there was no copy from an
existing file involved. That makes logical sense, but I'm not sure how useful
it will be.

The data structure might look like this:

rust
// Current `TreeValue::File` variant:
File { id: FileId, executable: bool },
// New `TreeValue::File` variant:
File { id: FileId, executable: bool, copy_id: CopyId },

// A CopyId is a hash of this struct:
struct CopyHistory {
    path: RepoPath,
    parents: Vec<CopyId>
}

Should we support copy tracking for symlinks? Their history is not very useful
for annotation purposes, but knowing the history may at least be useful for
detecting directory renames (if all files and symlinks in a directory were
renamed).

We probably should not support tracking copied directories because it seems
complicated. I haven't spent much thinking about it, so it's also possible that
it's not that complicated.

Diffing

When diffing two trees, we first diff the trees without considering copy info.
For any copy IDs that changed in that diff, we walk all of their copy graphs to
figure out how they're related and which source file to associate with which
destination file.

The details of the algorithm is left for the implementation. The following
sections provide some examples to hopefully show that it's feasible.

Example: Divergent copy and rename

Let's look at an example of how this model would look in this scenario:

console
M rename foo->baz, create bar
|
| L copy foo->bar, create baz
|/
K add foo

Assuming the new files are different in each commit, we get the following trees.
Notation:

  • idis the hash of the contents (the FileId)
  • The 2:bar->1:foo means that copy ID 2 (i.e. hash of the CopyHistory
    struct) has file bar, which was copied from copy ID 1, where it was
    called foo.
console
Commit K:
name: foo, id: K, copy_id: 1:foo

Commit L:
name: bar, id: L, copy_id: 2:bar->1:foo
name: baz, id: L, copy_id: 3:baz
name: foo, id: K, copy_id: 1:foo

Commit M:
name: bar, id: M, copy_id: 4:bar
name: baz, id: M, copy_id: 5:baz->1:foo

This graph also shows the relationship between the copy IDs and which commits
they appear in:

mermaid
graph LR

    subgraph L["Commit L"]
        2["2:bar"]
        3["3:baz"]
        subgraph K["Commit K"]
            1["1:foo"]
        end
    end

    subgraph M["Commit M"]
        4["4:bar"]
        5["5:baz"]
    end

    2 --> 1
    5 --> 1

Let's first consider the diff from K to M. Looking at just the trees, that
diff finds that copy IDs 1,4,5 were affected. By walking their graphs, we find
1 and 5 are related, while 4 is not. Considering that copy graph (involving IDs
1 and 5), since foo doesn't exist in the destination and baz doesn't exist
in the source, we consider it a rename.

Let's now consider the diff from L to M. This is the same as the diff of the
commit M2 we'd get by running
jj new L; jj bookmark create M2; jj restore --from M --to M2 (which would
result in the commit M2 having the same tree as M). Diffing from L to
M (or M2) finds 1,2,3,4,5 as changed copy IDs. By walking their graphs, we
find that 1,2, and 5 are related, while 3 and 4 are not.

The bar and baz files have unrelated copy graphs, i.e. the copy graphs for
the bar file in commit L and the bar file in commit M are disjoint, and
the same is true for the baz file. Therefore, we break up their diffs into two
separate diffs for each file.

Among the remaining copy IDs, the shortest path in the copy graph is between
foo on the source side and baz on the destination side, so we start with these.
Since foo doesn't exist on the destination side and baz doesn't exist on the
source side (with a related copy ID), we consider it a rename.

The remaining file is bar on the source side. Its closest relative on the
destination side is baz. Since we already used baz as a rename target for
foo, we won't consider bar renamed to it. So we consider bar as copied
into baz.

So we get these diffs:

  • baz is deleted (deleting content L)
  • bar is created (with content M)
  • foo is renamed to baz (showing diff from K to M)
  • bar is merged into baz (showing diff from L to M)

Example: Divergent copy and rename (best rename target)

console
N copy baz->qux
|
M rename foo->baz
|
| L rename foo->bar
|/
K add foo

When diffing L to N, we find that all files are related. Since bar does
not exist in the destination, we should find a rename target to match it with.
We pick baz because it's closer in the graph than qux is. So the diff is:

  • bar is renamed to baz
  • bar is copied to qux

Example: Copy onto deleted file

console
M copy foo->bar
|
L delete bar
|
K add foo, bar

When diffing from K to M, we notice that bar has different and unrelated
copy IDs. We present one record saying that bar was deleted, and one record
saying that bar was copied from foo.

When diffing from M to K, we will instead present one record that says that
bar was created, and one record that says that bar was merged into foo.

Merging

When merging, we need to add a phase before the content-level merging where we
handle copies. As before, we start by creating the completely unresolved merged
tree based on the input trees. To find the relevant copy information, we look at
the files changed in each diff and then look up the full copy graph for each.
For each copy graph, we can then walk the copy graph to find possible target
paths, which we then look up in the other side of the merge. If the path exists
in the tree and has the right copy ID, then we know that the files are related.

We assume that the differences between the bases and the first term in the
conflict can be very large, so we don't look at that diff. Assuming that the
commit backend can look up the full copy graph based on a given copy ID, we
don't need that diff for correctness.

Once we have found all copies involved in the merge, we analyze them to find
conflicts, such as when two sides of the merge rename a file to the same
target. If there are conflicts, we leave the trees unchanged. The user can then
resolve the name conflicts using jj resolve (once we've added support for
that). Depending on how slow the naming conflict phase turns out to be, we may
want to write a flag to commits indicating that they have unresolved naming
conflicts, so subsequent calls can avoid that phase.

When merging trees, we start by rewriting each diff to match any different names
in the destination tree. For example, if the tree conflict is A+(B-C)+(D-E),
then we will rewrite the (B-C) diff and the (D-E) diff to the paths in A.
To translate the (B-C) diff, we calculate renames from C to A and then we
apply those renames to both C and B. This may result in conflicts.

If a file has a conflict in the copy ID, it will appear as if it doesn't exist
when materialized. It will therefore not show up in the working copy until the
user has resolved the conflict.

For example:

console
M set foo="bye"
|
| L rename foo->bar
|/
K add foo="hello"

When rebasing M onto L, we apply the foo->bar rename to the trees in M
and its parent.

Another example:

console
N rename foo->bar
|
| M create foo="M"
| |
| L delete foo
|/
K add foo="K"

When rebasing M onto N, we find the foo->bar rename in N, but since it
is unrelated to the foo file in M (assuming the foo file created in M
used a different salt), we will not perform any renames. The new foo file
is then simply created in the rebased M just like it was before the rebase.

Propagating changes across copies?

Should we propagate changes to copies? For example, if you've modified file
foo and then rebase it onto a commit that copied foo to bar, should we
apply your change to bar too? Mercurial does that but Git doesn't. It's
particularly useful when a file has been split in two. For example, let's say
you've made various changes in file foo and then rebase those change onto a
commit that split foo into foo1 and foo2 (or foo and bar). If we
propagate the changes to both files, each change will apply successfully in one
file (assuming the changes do not overlap with the split boundary). Each change
will have a modify/delete conflicts in the other file. Those can relatively
easily be resolved in favor of the deleted hunk. If we do not propagate changes,
then changes that belong in one of the files will instead only appear as
modify/delete conflicts in the first file and you will have to manually copy
over the changes to the copied file.

Propagating changes to copies means that rebasing a commit and then rebasing it
back is no longer a no-op even when ignoring the "same-change rule". For
example, if your commit modifies file foo and you rebase that commit onto a
commit that copied foo to bar, and then you rebase it back, the same change
will be applied twice to foo. However, thanks to the same-change rule, we
won't consider it a conflict, so maybe it actually works well in practice.

A third option is to not leave it up to the user whether to propagate the
change across the copy. We can do this by leaving the relevant paths in the
input trees unchanged in the conflicted commit. Then we will redo the copy
tracking process every time the commit is inspected. We can have jj resolve
ask the user if they want to propagate the changes to the copy target with a
simple yes/no question per copy target.

Decision: Asking the user about propagating copies seems like the best option.
It avoids surprises, and it makes the conflict algebra work in more cases.

Example: Propagate changes to copied file, then rebase back

console
M foo="M"
|
| L copy foo->bar
|/
K add foo="K"

Let's say we rebase M onto L. Since we decided to not automatically
propagate changes to copies, we will leave the M+(L-K) tree unresolved (i.e.
without making any changes to the three trees). If the user does not resolve
the conflict, and instead rebases M back onto K, the conflict will be
resolved automatically per the usual conflict simplification.

Example: Multiple copies

console
N foo="N"
|
| M foo="M, foo2="M2", foo3="M3"
| |
| L copy foo->foo2, copy foo->foo3
|/
K add foo="K"

Let's say we rebase M onto N. The changes to foo, foo2, and foo3 will
then all apply to foo, which means we get a 4-sided conflict.

Example: Convergent renames

Consider this "convergent copy/rename" scenario:

console
$ jj log
C rename bar->baz
|
| B rename foo->baz
|/
A add foo, add bar

$ jj new B C

It seems clear that baz's copy graph should inherit from both foo and bar,
producing a merge in copy graph. The trees would look like this:

text
Commit A:
name: foo, id: aaa111, copy_id: 1:foo
name: bar, id: aaa111, copy_id: 2:bar

Commit B:
name: bar, id: aaa111, copy_id: 2:bar
name: baz, id: aaa111, copy_id: 3:baz->1:foo

Commit C:
name: foo, id: aaa111, copy_id: 1:foo
name: baz, id: aaa111, copy_id: 4:baz->2:bar

Merge commit:
name: baz, id: aaa111, copy_id: 5:baz->{3:baz->1:foo,4:baz->2:bar}

We used the same content for both foo and bar above to simplify. If they
had been different, we would have had a conflict in the contents but the copy
ID would still have been clear.

Example: Rebasing

console
$ jj log
C rename bar->baz
|
B rename foo->bar
|
A add foo

$ jj rebase -r C -o A
console
$ jj log
C rename foo->baz
|
| B rename foo->bar
|/
A add foo

$ jj rebase -r C -o B

Example: Rename added file

A well-known and thorny problem in Mercurial occurs in the following scenario:

console
$ jj log
C rename foo->bar
|
| B modify foo
|/
A add foo

$ jj squash --from C --into A

The problem here for Mercurial is that after squashing C into A, the new A has
file bar but no record that it used to be called foo. The design proposed
above handles this case because we keep the copy ID of bar after squashing,
so we can detect that the modifications to foo in commit B should be
propagated to bar.

Example: Divergent renames

Consider this "divergent rename" scenario:

console
$ jj log
C rename foo->baz
|
| B rename foo->bar
|/
A add foo

$ jj new B C

In this scenario, the regular 3-way merge of the trees without considering copy
info results in a tree without conflicts. However, the user might reasonably
expect to have to choose between the bar and baz names. Here's what Git says
in this scenario:

console
$ git merge main
CONFLICT (rename/rename): foo renamed to baz in HEAD and to bar in main.
Automatic merge failed; fix conflicts and then commit the result.

$ git st
HEAD detached from ab0b8e3
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add/rm <file>..." as appropriate to mark resolution)
        added by them:   bar
        added by us:     baz
        both deleted:    foo

Interestingly, Git seems to represent this state by using index states that
would not normally end up in the index as a result of conflicts.

Here's what Mercurial says:

console
$ hg merge main
note: possible conflict - foo was renamed multiple times to:
 bar
 baz
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
(branch merge, don't forget to commit)

Mercurial doesn't have a place to record this state, so it just prints that
note and leaves it at that.

The model and algorithm described in this document would result in a conflict
in the copy ID at both paths after propagating the renames.

Example: @jonathantanmy's test case:

TODO: fill this out

console
$ jj log
E baz="baz" (resolves conflict)
|
D <conflict>
|\
C | rename bar->baz
| |
| B rename foo->baz
|/
A add foo="foo" and bar="bar"

$ jj rebase -r E -o C
$ jj new D E -m F

If F is empty (auto-merged), it should have the same state as E before.

Log

The copy graph contains all past paths and copy IDs of a file, so when doing
jj log <filename>, we might want to translate that to a revset that's similar
to files() but matches specific (path, copy ID) pairs instead of specific
paths.

Annotate

TBD

Representation in Git

Do we ever want to record renames in the Git backend? If we do, we would
presumably store it outside the Git object, similar to how we store the change
id for commits.

What do we use for trees where we don't have any copy graph recorded? If we
simply create a new copy graph based on the current path, then the caller will
never find any copies. Do we need an indexing pass to detect all renames in a
repo when running jj git init? That can be very expensive for large repos.
For reference, git log --summary --find-copies-harder takes about 165 seconds
in the git.git repo on my computer, and about 13 hours in the Nixpkgs repo.

An alternative is to do copy indexing in the background after cloning a repo.
That would mean that copy information would not show up until some time later.
It would also be more work to implement it this way.

How to deal with two trees having the same content but different file ids?
Actually store the additional data linked from the commit object? That would
not work if we point to trees from somewhere that's not a commit. We point to a
tree from the working-copy state.

One could imagine not storing any copy info in Git and instead making the model
described above an implementation detail of the backend. Then it could be used
by the native backend and the Google backend, while we still use on-the-fly
copy detection in the Git backend. However, if we want to be able to tell the
user about details of conflicting copy IDs so they can decide how to resolve
such conflicts, then we would have to somehow represent that abstractly too.

Representation in cloud repo (e.g. Google)

Let's say you have a commit with some files you've modified. You now want to
sync (rebase) that to an updated main branch. If some of the files you modified
no longer exist on the main branch, we want to figure out if they were renamed
so we should propagate your changes to the new file location. As described
earlier, we can do that by finding files that have a different copy ID since the
last time you synced with the main branch. However, if there are 10 million new
commits on the main branch, there's perhaps tens of thousands of such files
spread across the entire tree. That can therefore be very expensive to
calculate. We therefore need to be able to get help from a custom backend
implementation with this query.

Since we are only interested in copy graphs that involve files modified in the
rebased commit, it should be sufficient if the backend provides a method to
fetch the whole copy graph for a given copy ID (or list of copy IDs). We would
then first find all copy IDs involved in the diff of the rebased commit. Then
we query the backend to get the full copy graphs. We then need to walk the copy
graphs to see if a node exists in the destination tree.

A weakness of this solution is that the search gets expensive if there are very
many related files. That's probably not much of a problem in practice. The
server might want to populate the index only for public/immutable commits.
Otherwise, a user could poison the index by creating tons of copies
(intentionally or by mistake), which would make all future queries about those
files expensive.

Implementation plan

A rough implementation plan may look like this:

  1. Implement support for copy-tracking in the test backend
  2. Implement diff algorithm and test it
  3. Implement merge algorithm and test it
  4. Implement blame algorithm and test it
  5. Implement file-following log algorithm and test it
  6. Extract some queries to the commit backend trait so cloud-based backends
    (like the Google backend) can provide versions implemented using database
    indexes
  7. Implement support for copy-tracking in the Git backend. This may involve
    backfilling, possibly lazily. Or it may involve new abstractions in the
    commit backend trait.
  8. Implement CLI for recording copies and for resolving conflicts in copies

Alternatives considered

Detect copies (like Git)

Git doesn't record copy info. Instead, it infers it when comparing two trees.

It's hard to make this model scale to very large repos. For example, let's say
you're rebasing your local commit to a new upstream commit that's 1 million
commits ahead. We would then want to find if any of the files in your local
commit has been copied upstream. That's very expensive to do by comparing the
old and the new base trees. However, since the query APIs defined above take
commits (not trees) as input, we allow the backend to take the history into
account when calculating the copies. A backend can then create an index based
on the input files (in your local commit) and find if it's been copied without
comparing the full trees.

Record logical file identifiers in trees (BitKeeper-like model)

BitKeeper records a file ID (which identifies a logical file, unlike our FileId
type) for each path (or maybe it's a path for each file ID). That way you can
compare two arbitrary trees, find the added and deleted files and just compare
the file IDs to figure out which of them are renames.

This model doesn't seem to be easily extensible to support copies (it only
supports renames).

To perform a rebase across millions of commits, we would not want to diff the
full trees because that would be too expensive (probably millions of modified
files). We could perhaps instead find renames by bisecting to find commits that
deleted any of the files modified in the commit we're rebasing.

Another problem is how to synthesize the file IDs in the Git backend. That could
perhaps be done by walking from the root commits and persisting an index.

Include copy info in the FileId (Mercurial-like model)

Mercurial stores copy info in a metadata section in the file content itself
[^mercurial_changeset_copies]. That means that a file will get a new file
(content) ID if its copy history changes. That's quite similar to the proposal
in this document. One difference is that Mercurial's model stores information
only about the most recent copy. If the file is then modified, it will get a new
file ID. One therefore has to walk the history of the file to find the previous
name (which is usually not much of a problem because Mercurial stores a revision
DAG per file in addition to the revision DAG at the commit level).

Hybrid snapshot/patch model with copy info stored in commits

We considered storing copy info about the copies/renames in the commit object.
That has some significant impact on the data model:

  • Without copy info, if there's a linear chain of commits A..D, you can find
    the total diff by diffing just D-A. That works because (B-A)+(C-B)+(D-C)
    simplifies to just D-A. However, if there is copy info, the total diff will
    involve copy info. If that's associated with the individual commits, we will
    need to aggregate it somehow.
  • Restoring from another tree is no longer just a matter of copying that tree;
    we also need to figure out copies between the old tree and the new tree.
  • Conflict states are represented by a series of states to add and remove. This
    does not work with the patch-based copy info. We spent a lot of time trying
    to figure out a solution that works, but it seems like the snapshot-based
    conflict model and the patch-based copy info model are not reconcilable.
    Therefore, we won't track conflicted copy info, such as between a foo->baz
    rename and a bar->baz rename.
  • Since copy records are relative to the auto-merged parents, that unfortunately
    means that the records will depend on the merge algorithm, so it's possible
    that a future change to the merge algorithm will make some copy records
    invalid. We will therefore need to not assume that the copy source exists.

For the state in conflicted commits, we considered using a representation like
this:

rust
struct MergedTree {
    snapshot: Tree,
    diffs: Diff
}

struct Diff {
    before: Tree,
    after: Tree,
    /// Copies from `before` to `after`
    copies: Vec<CopyInfo>,
    /// Copies from `before` to `snapshot`
    copies_to_snapshot: Vec<CopyInfo>,
}

struct CopyInfo {
    source: RepoPathBuf,
    target: RepoPathBuf,
    // Maybe more fields here for e.g. "do not propagate"
}

That works for calculating the resulting tree, but it does not seem to allow for
doing the conflict algebra we currently do. That means that things like
parallelizing commits and then serializing them again would lose copy
information.

[^martinvonz_slow]: This took me (@martinvonz) months to really understand.
[^mercurial_changeset_copies]: From around
https://repo.mercurial-scm.org/hg/rev/49ad315b39ee, Mercurial also
supports storing copy info in commits. That made it the kind of
snapshot/patch model we described above as not working well.


Design Git Submodule Storage (`docs/design/git-submodule-storage.md`)

Git submodule storage

Objective

Decide what approach(es) to Git submodule storage we should pursue.
The decision will be recorded in ./git-submodules.md.

Use cases to consider

The submodule storage format should support the workflows specified in the
submodules roadmap. It should be obvious how "Phase 1"
requirements will be supported, and we should have an idea of how "Phases 2,3,X"
might be supported.

Notable use cases and workflows are noted below.

Fetching submodule commits

Git's protocol is designed for communicating between copies of the same
repository. Notably, a Git fetch calculates the list of required objects by
performing reachability checks between the refs on the local and the remote
side. We should expect that this will only work well if the submodule repository
is stored as a local Git repository.

Rolling our own Git fetch is too complex to be worth the effort.

"jj op restore" and operation log format

We want jj op restore to restore to an "expected" state in the submodule.
There is a potential distinction between running jj op restore in the
superproject vs in the submodule, and the expected behavior may be different in
each case, e.g. in the superproject, it might be enough to restore the submodule
working copy, but in the submodule, refs also need to be restored.

Currently, the operation log only references objects and refs in the
superproject, so it is likely that proposed approaches will need to extend this
format. It is also worth considering that submodules may be added, updated or
removed in superproject commits, thus the list of submodules is likely to change
over the repository's lifetime.

Nested submodules

Git submodules may contain submodules themselves, so our chosen storage schemes
should support that.

We should consider limiting the recursion depth to avoid nasty edge cases (e.g.
cyclical submodules.) that might surprise users.

Supporting future extensions

There are certain extensions we may want to make in the future, but we don't
have a timeline for them today. Proposed approaches should take these
extensions into account (e.g. the approach should be theoretically extensible),
but a full proposal for implementing them is not necessary.

These extensions are:

  • Non-git subrepos
  • Colocated Git workspace
  • The superproject using a non-git backend

Proposed design

Git submodules will be stored as full jj repos. In the code, jj commands will
only interact with the submodule's repo as an entire unit, e.g. it cannot query
the submodule's commit backend directly. A well-abstracted submodule will extend
well to non-git backends and non-git subrepos.

The main challenge with this approach is that the submodule repo can be in a
state that is internally valid (when considering only the submodule's repo), but
invalid when considering the superproject-submodule system. This will be managed
by requiring all submodule interactions go through the superproject so that
superproject-submodule coordination can occur. For example, jj will not allow
the user to work on the submodule's repo without going through the superproject
(unlike Git).

The notable workflows could be addressed like so:

Fetching submodule commits

The submodule would fetch using the equivalent of jj git fetch. It remains to
be decided how a "recursive" fetch should work, especially if a newly fetched
superproject commit references an unfetched submodule commit. A reasonable
approximation would be to fetch all branches in the submodule, and then, if the
submodule commit is still missing, gracefully handle it.

"jj op restore" and operation log format

As full repos, each submodule will have its own operation log. We will continue
to use the existing operation log format, where each operation log tracks their
own repo's commits. As commands are run in the superproject, corresponding
commands will be run in the submodule as necessary, e.g. checking out a
superproject commit will cause a submodule commit to also be checked out.

Since there is no association between a superproject operation and a submodule
operation, jj op restore in the superproject will not restore the submodule to
a previous operation. Instead, the appropriate submodule operation(s) will be
created. This is sufficient to preserve the superproject-submodule relationship;
it precludes "recursive" restore (e.g. restoring branches in the superproject
and submodules) but it seems unlikely that we will need such a thing.

Nested submodules

Since submodules are full repos, they can contain submodules themselves. Nesting
is unlikely to complicate any of the core features, since the top-level
superproject/submodule relationship is almost identical to the submodule/nested
submodule relationship.

Extending to colocated Git workspaces

Git expects submodules to be in .git/modules, so it will not understand this
storage format. To support colocated Git workspaces, we will have to change Git
to allow a submodule's gitdir to be in an alternate location (e.g. we could add
a new submodule.<name>.gitdir config option). This is a simple change, so it
should be feasible.

Alternatives considered

Git repos in the main Git backend

Since the Git backend contains a Git repository, an 'obvious' default would be
to store them in the Git superproject the same way Git does, i.e. in
.git/modules. Since Git submodules are full repositories that can have
submodules, this storage scheme naturally extends to nested submodules.

Most of the work in storing submodules and querying them would be well-isolated
to the Git backend, which gives us a lot of flexibility to make changes without
affecting the rest of jj. However, the operation log will need a significant
rework since it isn't designed to reference submodules, and handling edge cases
(e.g. a submodule being added/removed, nested submodules) will be tricky.

This is rejected because handling that operation log complexity isn't worth it
when very little of the work extends to non-Git backends.

Store Git submodules as alternate Git backends

Teach jj to use multiple commit backends and store Git submodules as Git
backends. Since submodules are separate from the 'main' backend, a repository
can use whatever backend it wants as its 'main' one, while still having Git
submodules in the 'alternate' Git backends.

This approach extends fairly well to non-Git submodules (which would be stored
in non-Git commit backends). However, this requires significantly reworking the
operation log to account for multiple commit backends. It is also not clear how
nested submodules will be supported since there isn't an obvious way to
represent a nested submodule's relationship to its superproject.


Design Git Submodules (`docs/design/git-submodules.md`)

Git submodules

This is an aspirational document that describes how jj will support Git
submodules. Readers are assumed to have some familiarity with Git and Git
submodules.

This document is a work in progress; submodules are a big feature, and relevant
details will be filled in incrementally.

Objective

This proposal aims to replicate the workflows users are used to with Git
submodules, e.g.:

  • Cloning submodules
  • Making new submodule commits and updating the superproject
  • Fetching and pushing updates to the submodule's remote
  • Viewing submodule history

When it is convenient, this proposal will also aim to make submodules easier to
use than Git's implementation.

Non-goals

  • Non-Git 'submodules' (e.g. native jj submodules, other VCSes)
  • Non-Git backends (e.g. Google internal backend)
  • Changing how Git submodules are implemented in Git

Background

We mainly want to support Git submodules for feature parity, since Git
submodules are a standard feature in Git and are popular enough that we have
received user requests for them. Secondarily (and distantly so), Git submodules
are notoriously difficult to use, so there is an opportunity to improve the UX
over Git's implementation.

Intro to Git Submodules

Git submodules are a feature of Git
that allow a repository (submodule) to be embedded inside another repository
(the superproject). Notably, a submodule is a full repository, complete with its
own index, object store and ref store. It can be interacted with like any other
repository, regardless of the superproject.

In a superproject commit, submodule information is captured in two places:

  • A gitlink entry in the commit's tree, where the value of the gitlink entry
    is the submodule commit id. This tells Git what to populate in the working
    tree.

  • A top level .gitmodules file. This file is in Git's config syntax and
    entries take the form submodule.<submodule-name>.*. These include many
    settings about the submodules, but most importantly:

    • submodule<submodule-name>.path contains the path from the root of the tree
      to the gitlink being described.

    • submodule<submodule-name>.url contains the url to clone the submodule
      from.

In the working tree, Git notices the presence of a submodule by the .git entry
(signifying the root of a Git repository working tree). This is either the
submodule's actual Git directory (an "old-form" submodule), or a .git file
pointing to <superproject-git-directory>/modules/<submodule-name>. The latter
is sometimes called the "absorbed form", and is Git's preferred mode of
operation.

Roadmap

Git submodules should be implemented in an order that supports an increasing set
of workflows, with the goal of getting feedback early and often. When support is
incomplete, jj should not crash, but instead provide fallback behavior and warn
the user where needed.

The goal is to land good support for pure Jujutsu workspaces, while colocated
workspaces will be supported when convenient.

This section should be treated as a set of guidelines, not a strict order of
work.

Phase 1: Readonly submodules

This includes work that inspects submodule contents but does not create new
objects in the submodule. This requires a way to store submodules in a jj
repository that supports readonly operations.

Outcomes

  • Submodules can be cloned anew
  • New submodule commits can be fetched
  • Submodule history and branches can be viewed
  • Submodule contents are populated in the working copy
  • Superproject gitlink can be updated to an existing submodule commit
  • Conflicts in the superproject gitlink can be resolved to an existing submodule
    commit

Phase 2: Snapshotting new changes

This allows a user to write new contents to a submodule and its remote.

Outcomes

  • Changes in the working copy can be recorded in a submodule commit
  • Submodule branches can be modified
  • Submodules and their branches can be pushed to their remote

Phase 3: Merging/rebasing/conflicts

This allows merging and rebasing of superproject commits in a content-aware way
(in contrast to Git, where only the gitlink commit ids are compared), as well as
workflows that make resolving conflicts easy and sensible.

This can be done in tandem with Phase 2, but will likely require a significant
amount of design work on its own.

Outcomes

  • Merged/rebased submodules result in merged/rebased working copy content
  • Merged/rebased working copy content can be committed, possibly by creating
    sensible merged/rebased submodule commits
  • Merge/rebase between submodule and non-submodule gives a sensible result
  • Merge/rebase between submodule A and submodule B gives a sensible result

Phase ?: An ideal world

I.e. outcomes we would like to see if there were no constraints whatsoever.

  • Rewriting submodule commits rewrites descendants correctly and updates
    superproject gitlinks.
  • Submodule conflicts automatically resolve to the 'correct' submodule commits,
    e.g. a merge between superproject commits creating a merge of the submodule
    commits.
  • Nested submodules are as easy to work with as non-nested submodules.
  • The operation log captures changes in the submodule.

Design

Guiding principles

TODO

Storing submodules

Possible approaches under discussion. See
./git-submodule-storage.md.

Snapshotting new submodule changes

TODO

Merging/rebasing with submodules

TODO


Design Jj Converge Command (`docs/design/jj-converge-command.md`)

jj converge (aka resolve-divergence) Command Design

Authors: David Rieber,
Martin von Zweigbergk

Summary: This document is a proposal for a new jj converge command to help
users resolve (or reduce) divergence. The command will use heuristics -- and
sometimes will prompt the user for input -- to rewrite the N visible commits for
a given change with a single new commit, without introducing new divergence in
the process. jj resolve-divergence will be an alias for jj converge.

Objective

A divergent change occurs when multiple visible commits have the same change
ID. Divergence is not a desirable state, but is not a bad state either. In this
regard divergence is similar to conflicts: the user can choose when and how to
deal with divergence. The Handling divergent commits guide has some useful
tips, but nevertheless divergence is confusing to our users. We can do better
than that. It should be possible to solve divergence (after the fact) in many
scenarios with the help of this command. Solving divergence means rewriting the
commit graph to end up with a single visible commit for the given change id. For
the purposes of this design doc we call this commit the "solution".

The command should produce informative messages to summarize any changes made,
and will prompt for user input in some situations. The user may of course not
like the solution. jj undo can be used in that case.

Divergent changes

Divergent commits (for the same change-id) can differ:

  • In their commit description (including tags)
  • In their commit trees
  • In the parent(s) of the commits (commits B/0 and B/1 for change B have
    different parents)
  • In the commit author
  • It is also possible divergence involves two commits with different
    timestamps that are otherwise identical

As you read this design doc it is important to not confuse the
predecessor / successor relationship versus the ancestor / descendant
relationship.

Some divergence scenarios

Divergence can be introduced in many ways. This document does not aim to explain
any / all of those scenarios accurately, this section is only meant to be rough
background material. Here are some examples:

  • In one terminal you type jj describe to edit a commit description and
    while the editor is open you take a coffee break, when you come back you
    open another terminal and do something that rewrites the commit (for example
    you modify a file and run jj log, causing a snapshot). When you save the
    new description jj describe completes and you end up with 2 visible
    commits with the same change id.

  • In general any interactive jj command (jj split -i, jj squash -i, etc.)
    can lead to divergence in a similar way.

  • You can introduce divergence by making some hidden predecessor of your
    change visible again. There are many ways this could happen.

  • Divergence can happen when mutating two workspaces. For example, assume you
    have workspaces w1 and w2 with working copy commits A and B
    respectively, where B is a child of A. In w2 you run jj git fetch and
    then rebase the whole branch onto main. Go back to w1 (which is now stale),
    modify some file on disk and take a snapshot (e.g. run jj log). This
    introduces divergence.

  • When using the Git backend jj propagates change-id. The change-id is stored
    in the commit header, so after jj git fetch you can end up with a second
    commit with the same change-id.

  • There is a Google-specific jj upload command to upload a commit to Google's
    review / test / submit system, and there is an associated Google-specific
    command to "download" a change from that system back to your jj repo. This
    can introduce divergence very much like in the Git scenario.

  • At Google, snapshotting operations happen concurrently on different machines
    (e.g. two terminals, or more commonly, a terminal and an IDE). Often times
    they end up snapshotting the same content. Google's backend does not hold
    locks while snapshotting because it's a distributed filesystem, so locking
    would be slow.

Strawman proposal

We look at some examples to illustrate what the command should do, starting with
simple cases and moving on to more complex ones.

Examples and expected behavior (with basic evolution graph)

The first few examples assume commits B/0 and B/1 are visible commits for
change B. First we assume B/0 and B/1 evolve directly from a common
predecessor commit P, which is now hidden (no longer visible). Later we look
at more complex evolution graphs. Note that P's change id is also B.

text
Evolution graph for examples 1, 2, 3 and 4.
B/0 and B/1 may have other predecessors for unrelated change-ids, P may have
predecessors (even for change-id B):

B/0
|
| B/1
|/
P

We will write A⁻ to denote the parent trees of commit A.

Example 1: two commits for change *B*, same parent

console
$ jj log
B/0
|
| B/1
|/
A

In this simple case it is clear the solution should be a child of A:

console
$ jj log
 B (solution)
 |
 | B/0 (not visible)
 |/
 | B/1 (not visible)
 |/
 A

Let's now consider two cases: when P's parent is A, and when P has some
other parent. First, if P's parent is A we have:

console
$ jj log
B/0
|
| B/1
|/
| P (not visible)
|/
A

Here P, B/0 and B/1 are siblings. The command needs to determine the
description, parents, tree and author of the solution. It uses a simple data
structure for this purpose:

rust
struct MergedState {
  author: Merge<Signature>,
  description: Merge<String>,
  parents: Merge<Vec<CommitId>>,
  tree: Merge<MergedTree>,
}

Each of the fields of MergedState are populated by doing a merge of the
corresponding fields of P, B/0 and B/1. Loosely speaking each merge can be
expressed as P + (B/0 - P) + (B/1 - P) for each of the fields. The command
attempts to resolve the various Merge objects trivially, using same_change: SameChange::Accept (later on in this design doc we will tweak the merge
algorithm a bit).

The description is merged as a String value. If the description does not
trivially resolve, the user's merge tool will be invoked, with conflict markers.
If author does not trivially resolve, the user will be presented with the
options to choose from. Once that's all done we have our solution commit B.
All descendants of B/0 and B/1 are rebased onto B. The command records the
operation in the operation log with a new View where B is a visible commit
with {B/0, B/1} as predecessors. B/0 and B/1 become hidden commits.

Note that in some cases the solution may be identical to either B/0 or B/1
(in all regards except the commit timestamp): we choose to create a new commit
B to make the evolution graph and op log more clearly show that jj converge
was invoked. Alternatively we could keep the matching commit instead of creating
a new commit (this could result in cycles in the evolog).

Example 2: two commits for change *B* with same parent (predecessor has a different parent)

Now lets consider the case where P has a different parent:

console
$ jj log
B/0
|
| B/1
|/
A
| P (not visible)
|/
X

In this case we first rebase P onto A (in-memory) to produce P' = A + (P - P⁻). This essentially reduces the problem to the previous case. We now produce
the solution as before: B = P' + (B/0 - P') + (B/1 - P'). Note that again the
parent of the solution is A.

Example 3: divergent commits with different parents

console
$ jj log
B/0
|
|   B/1
|  /
| C
|/
A

In this case it is not immediately obvious which commit should be the parent of
the solution. Let's first consider the case where P is a child of A.

console
$ jj log
B/0
|
|   B/1
|  /
| C
|/
| P (not visible)
|/
A

We determine the parent(s) of the solution as follows:

text
parents = P⁻ + (B/0⁻ - P⁻) + (B/1⁻ - P⁻)

In this example the expression evaluates to {A} + ({A} - {A}) + ({C} - {A}) = {C}. Since this expression resolves trivially to {C}, we use that as the
parents of the solution.

Note that this simple algorithm produces the desired output in example 1 and
example 2. In example 2, the expression looks like this:

text
parents = P⁻ + (B/0⁻ - P⁻) + (B/1⁻ - P⁻)
        = {X} + ({A} - {X}) + ({A} - {X})
        = {A} + ({A} - {X})

That expression resolves trivially to {A} when using SameChange::Accept.

Example 4: divergent commits with different parents, must prompt user to choose parents

If instead P is a child of some other commit X, the story is a bit
different:

console
$ jj log
B/0
|
| B/1
| |
| C
|/
A
| P (not visible)
|/
X

In this case parents will be

text
{X} + ({A} - {X}) + ({C} - {X}) = {A} + ({C} - {X})

Since this does not trivially resolve, the command prompts the user to select
the desired parents for the solution: either {A} or {C}.

Assume the user chooses {C}. The command then rebases (in memory) B/0, B/1
and P onto the chosen parents:

text
In-memory commits after rebasing B/0, B/1 and P on top of C (edges represent
parent/child relationship):

# B/0' = C + (B/0 - A)
# B/1' = C + (B/1 - C) = B/1
# P' = C + (P - X)

B/0'
|
| B/1'
|/
| P'
|/
C

As a result we obtain B/0', B/1' and P', and these are sibling commits. At
this point the command does a 3-way merge of MergedTree objects to produce
MergedState::tree (in reality it is enough to rebase the commit trees).

Example 5: more than 2 divergent commits

There can be more than 2 visible commits for a given change-id. We are assuming
here B/0, B/1 and B/2 are all direct successors of commit P (which is
invisible).

console
$ jj log
B/0
|
| B/1
| |
| | B/2
| |/
|/
A

This is completely analogous to the first example, we simply have more terms on
all merges. The same thing applies to all previous examples, in all cases we can
deal with any number of divergent commits for change B.

Examples and expected behavior (with arbitrary evolution graph)

So far we only considered simple cases where all divergent commits are direct
successors of a common predecessor P. Now we extend the ideas to arbitrary
evolution history. To that end we introduce the "truncated evolution graph for
B/0, B/1, ... , B/n"
, where B/0, B/1, ... , B/n are two or more commits with
the same change-id B. This is a directed graph. Its nodes are commits for
change-id B and the edges are from a commit to its (immediate) predecessor(s),
ignoring predecessors with unrelated change-ids. The graph is built by
traversing the operation log and associated View objects, adding nodes and
predecessor edges as needed. Nodes are added this way until a single most-recent
common predecessor commit is found. We call the most-recent common predecessor
the "evolution fork point of B/0, ... , B/n". The traversal keeps track of
visited commits to avoid infinite loops [^footnote-about-evolog-cycles]
[^virtual-evolution-fork-point].

Example 6: a two-level evolution graph

We continue by looking at a truncated evolution graph that is slightly more
complex than the basic 3-commit case. This will serve as motivation for the
general case. Here is our truncated evolution graph (remember the edges here
represent change evolution, not parent-child relations):

text
Truncated evolution graph. B/0, B/1 and Q may have other predecessors for
unrelated change-ids. P is the evolution fork point (it may have predecessors,
even for change-id B):

B/0     (description: "v3")
|
|  B/1  (description: "v2")
Q /     (description: "v2")
|/
P       (description: "v1")

Commit P evolved into Q and B/1, and Q evolved into B/0. As before
B/0 and B/1 are visible, P and Q are not. Since both sides of the
evolution transitioned from "v1" to "v2", and then one side further transitioned
to "v3", it seems a good heuristic to take "v3" as the description of the
solution. Note that this observation would not be possible if the algorithm only
considered the leafs (B/0, B/1) and their evolution fork point (P).

Note: Why do we care about divergence producing two commits with the exact same
change? It may seem this would be a very uncommon scenario, however, as
mentioned in the last bullet point in the "Some divergence scenarios" section,
this is in fact fairly common at Google due to the distributed nature of
Google's backend filesystem.

To implement the heuristic we outlined above in example 6 (i.e. to produce
"v3"), we propose introducing a new try_resolve_deduplicating_same_diffs
method to Merge<T>, and using that in calls to attempt to resolve the
MergedState::description, MergedState::parent, MergedState::author and
MergedState::trees. try_resolve_deduplicate_same_diffs is similar to
resolve_trivial, but it counts multiple identical (X - Y) terms exactly once,
otherwise it follows the same flow as resolve_trivial with SameChange::Accept.

We illustrate what try_resolve_deduplicating_same_diffs does when resolving
the description merge for the case in example 6. We build a Merge<String>,
starting with the description of the evolution fork point P, then adding
desc(Y) - desc(X) terms for each X->Y edge in the truncated evolution graph.
Then we call try_resolve_deduplicating_same_diffs to get:

text
P + (Q - P) + (B/1 - P) + (B/0 - Q) =
       = v1 + (v2 - v1) + (v2 - v1) + (v3 - v2)    <== collapse duplicate
                                                       (v2 - v1) edge/term
       = v1 + (v2 - v1) + (v3 - v2)
       = v3

try_resolve_deduplicating_same_diffs returns our desired value "v3". Note that
resolve_trivial (with either SameChange value) would return none. Here is
another example:

text
Truncated evolution graph:

B/0     ( foo.txt contents: "v3" )
|
|  B/1  ( foo.txt contents: "v2" )
Q /     ( foo.txt contents: "v1" )
|/
P       ( foo.txt contents: "v1" )

In this case try_resolve_deduplicating_same_diffs produces none. jj converge
cannot automatically resolve this merge so the user has to merge the
"v2"/"v3".

Edge cases when choosing the parents of the solution

When attempting to produce the solution parents, the command applies
try_resolve_deduplicating_same_diffs to MergedState::parents (of type
Merge<Vec<CommitId>>). If the result is Some<Vec<CommitId>> we have a set of
possible parents for the solution. If these candidate parents are all visible
commits with change-ids other than B, and none of those are descendants of
B/0, B/1, ... B/n, then we have the desired parents for the solution commit.

On the other hand,

  • if any candidate parent is a descendant of one of the divergent commits we
    are trying to solve, or
  • if any of the candidate parents are hidden and the chain starting at visible
    roots and leading up to and including the parent has any commit for the
    divergent change we are trying to solve (B), or for any other visible
    commit (divergent or not), or the chain itself has two or more commits with
    the same change-id,

then we would introduce new divergence or cycles in the commit graph if we based
on the solution on such candidate parents. In these edge cases the command will
simply discard the candidate parents and will instead ask the user to choose
which parents to use (or quit without making any changes). Care must be
taken when picking the options to present to the user for choosing parents:
essentially the user will choose between the parents of B/0, the parents of
B/1, and so on, but we will skip any B/i if any of the parents of B/i
descends from any B/j. Since the commit graph is a DAG, at least one option is
viable.

Multiple divergent change-ids

If there are multiple divergent change-ids, the command could prompt the user to
choose one, or apply heuristics to choose one programmatically. In the first
version it is OK to prompt the user.

If the command successfully resolves divergence in the first divergent
change-id, it could continue to process the next divergent change-id, and so on.
To avoid complexity the first implementation will only deal with one divergent
change per invocation.

Rebasing descendants and persisting

The last step is to rebase all descendants of the divergent commits on top of
the new solution commit, persist the changes and record the operation in the op
log. The command will move local bookmarks pointing to any of the rewritten
divergent commits to point to the solution commit.

Other edge cases

When the command starts it needs to find the divergent change-ids and their
corresponding visible commits. If the portion of the visible commit graph
leading up to immutable heads is too big, the command should error out.

There could be pathological cases where the evolution history is too long. When
building the truncated evolution graph, if we have traversed too many nodes (say
50) and we have not yet completed the traversal, the algorithm will not traverse
any more commits. We could simply error out, or we could use an incomplete
truncated evolution graph by adding a virtual evolution fork point. It is
probably best to error out.

Open questions

  • Do we ever have divergence of committer? Is it safe to mess with committer?

Alternatives considered

Automatically resolving divergence

It would be nice if divergence was avoided in the first place, at least in some
cases, at the point where jj is about to introduce the second (or third or
fourth, etc.) visible commit for a given change id. This should be investigated
separately.

Resolve divergence two commits at a time

The algorithm in this proposal should work when there are any number of
divergent commits (for a given change id). In practice we expect most often
there will be just 2 or perhaps a few divergent commits. We could design an
algorithm for just 2 commits, but we chose to think about the more general case.

Only considering the evolution fork point and visible commits

As explained in example 6 this proposal uses the truncated evolution graph and
try_resolve_deduplicating_same_diffs to produce the solution. That example
shows why we think this leads to a better heuristic. We could instead only
consider P and B/0, B/1, ... , B/n. That would be slightly simpler.

[^footnote-about-evolog-cycles]: It is unclear if the evolution history can
contain cycles today, but there has been some
discussion about jj undo possibly producing
cycles. In any case, it is very easy to deal
with that possibility, so we may as well handle
it?
[^virtual-evolution-fork-point]: Today there should always be a single evolution
fork point. However, we could handle cases
where a change-id "emanates from multiple
initial commits" by adding a single virtual
evolution fork point
commit with empty state:
empty description, empty tree and empty author,
and having the root commit as its parent, and
treating it as a predecessor of all initial
commits. Again, we probably don't need to worry
about this, but it is good to know we could
handle it.


Design Managed Config (`docs/design/managed-config.md`)

Repo-managed JJ configuration

Author: Matt Stark

Status: Pending implementation

Background

The design doc Secure JJ Config introduces a mechanism,
metadata.binpb, through which information about a repository / workspace can
be stored. It also discusses how allowing an external user to have control over
your config is a security risk.

Overview

There is a need for a repository to impose requirements upon users. Examples of
these things include, but are not limited to:

  • Formatter configuration (eg. [Chromium Formatter Config])
  • Pre-upload checks (eg. [Chromium Pre-upload Checks])
  • Syncing scripts (eg. [Chromium Syncing Scripts])
  • Custom aliases / revsets documented by some kind of “getting started with
    <project>” document

It should be fairly obvious that there is a strong benefit to doing so. However,
controlling a user’s config is sufficient to get root access to their machine,
so we require a mechanism more complex than just blindly loading the config
file.

This is currently achieved by projects such as chromium by instructing the user
to symlink .jj/repo/config.toml to a file in the repository. This has several
drawbacks:

  • It doesn’t work out-of-the-box. I need to manually symlink it
  • It has no security guarantees. If I update the file in the repo, the user has no
    opportunity to review it.
  • This prevents a user from having their own repo configuration on top of it.

Objective

  • Create a new layer of configuration between user configuration and repo
    configuration.
  • This configuration will be stored in version control and henceforth be
    referred to as “managed” configuration.
  • Implement it in a secure manner so that an attacker cannot take control over
    the managed config.

Detailed Design

The managed configuration will be read from $REPO/.config/jj/config.toml. This
is intentionally designed to be very similar to $HOME/.config/jj/config.toml
for the user configuration.

Any data stored here will be added to the metadata.binpb that was created
in secure config (that is, we will add additional fields to
the Metadata struct). This will ensure that we don't suffer from the "zip file
problem".

Security

Trust levels

We will add the following fields to metadata:

text
enum TrustLevel {
    // There's no "optional" in protos. It just returns the zero value.
    UNSET = 0;
    // The user wishes to completely ignore the managed config file.
    IGNORED = 1;
    // In trusted mode, we directly read from the managed config file.
    // This presents a security risk, so the user is expected to only do this
    // for a repo they trust.
    TRUSTED = 2;
    // In notify mode, the user is expected to manually replicate any changes
    // they want from the managed config to the repo config.
    NOTIFY = 3;
    // In review mode, the user has to review every config individually.
    REVIEW = 4;
}

message Metadata {
    // previous fields

    // The trust level associated with this repo.
    TrustLevel trust_level = 2;

    // When trust_level is review, the following fields are used.
    set<Hash> approved_configs = 3;
    set<Hash> rejected_configs = 4;
    // This is a can of worms and thus out of scope of the MVP.
    // There's no one correct way to choose the last approved.
    // Sometimes, the user wants be the last appproved config in the workspace.
    // At other times, the user wants be the last appproved config in the repo.
    string last_approved_config = 5;
}

Note that although the Metadata struct is stored for both the repo and the
workspace, this will only be used for the repository, not the workspace.

This is because trust is associated with repositories, not workspaces. If I
were to add an approved config in one workspace, for example, there is no
reason I wouldn't trust it in another.

User interface

Everything here only becomes relevant when a repo has a managed config. If it
does not have a managed config, we skip everything here.

Unset trust

If a repository has a managed config and unset trust, we first use
ui.can_prompt() to determine whether we are in a TUI.

  • If we are in a TUI, we ask the user what trust level they would like.
  • If we are not in a TUI, we warn them that we are ignoring the repo config (we
    return IGNORE), and that they can run jj config managed --ignore/notify/trust in a terminal to configure this.

Ignored

Nice and easy. We just completely ignore the managed config file.

Trusted

Nice and easy. We just read the managed config file.

Notify

Roughly speaking, if mtime(repo_config_file) > mtime(managed_config), then
the config is up to date.

If it isn't up to date, we print a warning. The user is then expected to update
their repo config with any options they want from the managed config, which
will stop the warning from printing as the mtime has changed.

This notify option has a rather painful UX when it comes to keeping it in sync
(particularly for a user who constantly switches between different branches
synced to different versions), but I choose it for several reasons. The first is
that the vast majority of users will simply select to blindly trust the repo.
The only people who will choose this option are the very security conscious, and
this is by far the most secure mechanism. Secondly, it is so much simpler than
an approval based mechanism, as we don’t need to worry about things such as
workspaces being synced to different places. It provides far fewer edge cases.

In the future we may consider improving on the UX, doing things such as printing
a diff since the last seen config (per last_approved_config), but that isn't
a part of the MVP.

Review

This is, by far, the most difficult option. There is no doubt that this
provides value to some users, but the precise details of how this should behave
is unclear (and much harder to implement than the other options).

The biggest problem with this is that there is not a clear "correct" set of
semantics. If a config is not approved / rejected, we have 3 options:

  • Fail the command. This seems like a bad idea since users have an assumption
    that most jj commands will not fail.
  • Use no config. This can potentially have quite bad effects on the repo.
  • Use best-effort to use an approved config (probably most recently approved).
  • Force the user to make a selection before running a command
    • This is impossible in general due to ui.can_prompt() returning false
      • We would need to fall back to another option if we can't prompt.
    • But ui.can_prompt() does not mean that we should prompt
      • E.g., a script that runs jj to do a bisection probably doesn't want
        you to prompt eve