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 ofgit rebase --update-refscombined withgit 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
likersyncon 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
jjRepository has moved to thejj-vcsGitHub
organization. - November 2024: Version 0.24 is released which adds
jj file annotate,
which is equivalent togit blameorhg 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 asjj init --git, which is now just calledjj 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 accidentallyedit-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.
Related Media
- Mar 2024: Chris Krycho started a YouTube series about Jujutsu.
- Feb 2024: Chris Krycho published an article about Jujutsu called jj init
and Steve Klabnik followed up with the Jujutsu Tutorial. - Jan 2024: Jujutsu was featured in an LWN.net article called
Jujutsu: a new, Git-compatible version control system. - Jan 2023: Martin's Talk about Jujutsu at Git Merge 2022, video
and the associated slides.
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 helpcommand (e.g.jj help rebase). - The
jj help -k <keyword>command (e.g.jj help -k config). Usejj 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.
Related work
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
mainbranch 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. SeeLICENSE 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. Seerun_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:
cd demos
./run_scripts.sh demo_*.sh |lessA 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 inrun_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
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 havejj installed.
You may want to jump to:
- Documentation for the latest released version of
jj. - Documentation for the unreleased version of
jj. This version of the docs corresponds to themainbranch of thejjrepo.
Some useful links
- GitHub repo for
jj - Overview of
jjin the repo's README - Installation and setup
- Tutorial and bird's eye view
- Working with Gerrit
- Working with GitHub
- Development roadmap
- Changelog
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 havejj installed.
You may want to jump to:
- Documentation for the latest released version of
jj. - Documentation for the unreleased version of
jj. This version of the docs corresponds to themainbranch of thejjrepo.
Some useful links
- GitHub repo for
jj - Overview of
jjin the repo's README - Installation and setup
- Tutorial and bird's eye view
- Working with Gerrit
- Working with GitHub
- Development roadmap
- Changelog
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 --changewill automatically create a bookmark and push it. - Using
jj bookmarkcommands 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:
$ jj log -r 'tyl|mus'
○ musnqzvt [email protected] 1 minute ago 9a09f8a5
│ Revision C
~ (elided revisions)
○ tylynnzk [email protected] 1 minute ago f26967c8
│ Revision AOnly 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:
$ 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 AHow 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:
watch --color jj --ignore-working-copy log --color=alwaysThis 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:
watchexec --quiet --clear --restart --watch=.jj/repo/op_heads/heads --ignore-nothing --wrap-process=none -- jj --ignore-working-copy logAlternatively, 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
gitandjjcommands 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 howjjstores
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 togit add -p && git commit/git commit -p/hg commit -i is to split the
working-copy commit with jj split -i (or the practically identicaljj 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 matchesscratchpad.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":
$ 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:
$ 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.jsonNow create a merge commit with the branch you're working on and the private
commit:
$ 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):
# 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 workTo avoid pushing change wwwwwwww by mistake, use the configuration
git.private-commits:
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:
$ 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 BThe 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 thatjj 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:
$ # 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 copySince 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:
$ # 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 AIn 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:
$ jj log
@ lnvvtrzo [email protected] 2025-02-28 21:01:10 31a347e0
│ featureA
◆ zzzzzzzz root() 00000000We'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:
$ jj new b80 -m "featureB"
Working copy (@) now at: pvnrkl 47171aa (empty) featureB
Parent commit (@-) : lnvvtr/1 b8004ab (divergent) featureAThere 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:
$ 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 BStep 3: Move any bookmarks to the original revision
$ jj bookmark move --from 31a347e0 --to b8004ea8Step 4: Abandon the unwanted revision
$ jj abandon 31a347e0Now, we have achieved the exact state we desired:
$ 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 identicalHow 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:
@
|
C
| \
B D
|/
ATo 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
jjterminal commands - Errors with 3rd party visual tools like
jjkorvisual-jj - Corrupted
working_copy.lockfiles
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 theserver.watch.ignored array inside your Vite configuration, for example:
// 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 awatch command, you can runjj 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-libavoids parsing command output and makes error handling easier. jj-libis 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
jjbinaries,
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. Usejj 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"
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.
jjcan find out its
actual state only when it's actively communicating with the remote. However,jjdoes store the last-seen position of the remote bookmark; this is the
commitjj 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 thejj bookmark trackcommand, for
example. - A tracking (local) bookmark is the local bookmark that
jjtries to keep
in sync with the tracked remote bookmark. For example, afterjj bookmark track mybookmark --remote=origin, there will be a local bookmarkmybookmark
that's tracking the remotemybookmark@originbookmark. 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:
$ # 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:
$ # 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.
jjwill contact the remote and check that the actual state of the remote
bookmark matchesjj's record of its last known position. If there is a
conflict,jjwill refuse to push the bookmark. In this case, you need to runjj git fetch --remote <remote name>and resolve the resulting bookmark
conflict. Then, you can tryjj git pushagain.If you are familiar with Git, this makes
jj git pushsimilar togit push --force-with-lease.There are a few cases where
jj git pushwill succeed even though the remote
bookmark is in an unexpected location. These are the cases wherejj 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.The local bookmark must not be conflicted. If it is, you would
need to usejj bookmark move, for example, to resolve the conflict.This makes
jj git pushsafe even ifjj git fetchis performed on a timer
in the background (this situation is a known issue[^known-issue] with some
forms ofgit push --force-with-lease). If the bookmark moves on a remote in a
problematic way,jj git fetchwill create a conflict. This should ensure
that the user becomes aware of the conflict before they canjj git pushand
override the bookmark on the remote.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 asjj 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
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 injj'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 withjj config path --user.The repo settings. These can be edited with
jj config edit --repo, or found
withjj 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 withjj 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 withjj 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:
[user]
name = "YOUR NAME"For future reference, here are a couple of more complicated examples,
# 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:
[template-aliases]
# basic_template = "'hello' ++ \"\n\""
literal_template = '"hello" ++ "\n"'basic_template is actually equivalent to this string:
'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:
"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. Sotemplate-aliases.my_template = "'hello\\n'" would probably not be what you
want.
User settings
[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):
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).
[ui]
color = "never" # Turn off colorCustom colors and styles
You can customize the colors used for various elements of the UI. For example:
[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:
[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:
[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:
[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:
[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 theui.default-command setting will be used instead. Possible values are any valid
subcommand name, subcommand alias, or user-defined alias (defaults to "log").
[ui]
default-command = ["log", "--reversed"]Default description
The editor content of a commit description can be populated by thedraft_commit_description template. self is a Commit
object.
[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.:
[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.
[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:
[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:
[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:
[revsets]
bookmark-advance-from = 'heads(::to & bookmarks())' # The closest bookmarks
bookmark-advance-to = '@' # To the current working copyThe 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:
[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 thetrailers.contains_key(key) method within the template.
[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:
[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.
[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:
[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
[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 ifmax-inline-alternation >= 1.<added> ... <removed> ... <added>sequence has 3 alternation.0: disable inlining, making--color-wordsmore similar to--git1: inline removes-only or adds-only lines2,3, ..: inline up to2,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 is3.
[diff.color-words]
max-inline-alternation = 3
context = 3Git 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 is3.show-path-prefix: Whether to show thea/andb/path prefixes indiff --gitoutput. The default istrue.
[diff.git]
context = 3
show-path-prefix = trueDiff 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.
[diff.stat]
max-bar-width = 10By 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.
[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.
[merge-tools.<name>]
# program = "<name>" # Defaults to the name of the tool if not specified
diff-args = ["--color=always", "$left", "$right"]$leftand$rightare replaced with the paths to the left and right
directories to diff respectively.$widthis replaced with the number of terminal columns available to the diff
content.If
diff-argsis not specified,["$left", "$right"]will be used by default.If
diff-args = [],jjwill 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:
[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:
[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:
[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 viarevset-aliases."immutable_heads()". The default set of immutable heads isbuiltin_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:
[revset-aliases]
"immutable_heads()" = "builtin_immutable_heads() | release@origin"To prevent rewriting commits authored by other users:
# 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
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 andnext 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:
[ui.movement]
edit = trueYou 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_listforjj config listtemplates.workspace_listforjj 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:
[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.
[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 usebuiltin_log() as a starting point for your own custom log revset.
!!! warning
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.
[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:
[revsets]
log-graph-prioritize = 'coalesce(description("megamerge\n"), trunk())'Default Template
You can configure the template used when no -T is specified.
templates.evologforjj evologtemplates.logforjj logtemplates.showforjj showtemplates.op_logforjj op logtemplates.op_showforjj op show
[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:
[templates]
log = "builtin_log_compact_full_description"Graph style
[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_nodefor commits (withOption<Commit>keywords)templates.op_log_nodefor operations (withOperationkeywords)
For example:
[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.
[ui]
log-word-wrap = trueDisplay of commit and change ids
Can be customized by the format_short_id() template alias.
[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() andformat_short_change_id() aliases:
[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:
[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:
[revsets]
# Prioritize the current bookmark
short-prefixes = "(main..@)::"Relative timestamps
Can be customized by the format_timestamp() template alias.
[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.
[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.
[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().
[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) andformat_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.
[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 genericPAGER environment variable is ignored.
Examples:
# 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:
[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 onstreampager but is configured withinjj'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.
[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.
[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
[ui.streampager]
# Start with the ruler showing
show-ruler = true # (default)
# Start with the ruler hidden
show-ruler = falseProcessing 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:
[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:
[ui]
pager = "delta"
diff-formatter = ":git"Aliases
You can define aliases for commands, including their arguments. For example:
[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:
[aliases]
l = {
definition = ["log", "-r", "(main..@):: | (main..@)-"],
doc = "Log pending changes"
}You can also use the dotted key syntax:
[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
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:
[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, setaliases.<name>.enabled to false:
[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.
[ui]
editor = "pico"To use NeoVim instead:
[ui]
editor = ["nvim", "+$line"]For GUI editors you possibly need to use a -w or --wait. Some examples:
[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" #IntelliJObviously, 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.
[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:
$leftis a directory containing the original contents before any changes.$rightis the directory containing the changed contents.
Edits are read back from here, so this is where tools should make changes.If no
edit-argsare specified,["$left", "$right"]are set by default.If
edit-args = [],jjwill 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:
[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:
[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 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):
ssh -L 17376:localhost:17376 \
-L 17377:localhost:17377 \
-L 17378:localhost:17378 \
-L 17379:localhost:17379 \
-L 17380:localhost:17380 \
myhost.example.comdiffedit3-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:
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:17380With 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, setui.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.appand the command-linemeldcommand thatjjuses. 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:
[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:
:builtinlaunches a TUI tool for 3-way merges (the default).:editoropens 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.:oursand:theirspick 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.)
[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 explanationjj makes the following substitutions:
$output(REQUIRED) is replaced with the name of the file that the merge tool
should output.jjwill read this file after the merge tool exits.$leftand$rightare replaced with the paths to two files containing the
content of each side of the conflict.$baseis 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_lengthis 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,jjuses conflict markers of
length 7, but they can be longer if necessary to make parsing unambiguous.$pathis 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 telljj 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 themerge-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 themerge-tools.TOOL.conflict-marker-style option, which takes the same values asui.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 orprettier. 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:$rootwill be replaced with the workspace root path (the directory
containing the .jj directory).$pathwill 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$firstand$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$firstand$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"]
- Example:
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 tofalse, 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-argsfrom making overly broad
changes (formatting the whole file) when no line ranges are available.
Setting this to
trueis 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.
[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.
[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:
[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 = falseTools 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:
[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 andhead, but execution order is important.
[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:
[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:
$ 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:
[run]
jobs = 8The value must be a positive integer. The --jobs / -j CLI flag overrides
this setting for a single invocation:
jj run -j 4 -- cargo fmtCommit 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
[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:
[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:
[signing]
backends.gpg.allow-expired-keys = truePKCS#12 Certificates
PKCS#12 certificates can be used to sign commits using the gpgsm backend.
[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:
[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:
[signing]
backends.gpgsm.allow-expired-keys = trueSSH Signing
[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:
[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:
[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.
[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
`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" andgit.sign-on-push = true:
# 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 = trueWith 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, andgit.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 usecommit.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 thejj 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:
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:
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:
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.
[remotes.origin]
fetch-bookmarks = "~gh-pages"
fetch-tags = "v*"Automatic tracking of bookmarks
You can configure which bookmarks to track automatically per remote, using theremotes.<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:
[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 useauto-track-created-bookmarksinstead:toml[remotes.origin] auto-track-created-bookmarks = "*"This pattern will only apply to bookmarks you create with
jj bookmark create
andjj 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").
[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
[git]
track-default-bookmark-on-clone = falseAbandon 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:
[git]
abandon-unreachable-commits = falseReplicating 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:
[git]
record-synthetic-predecessors = falseGenerated 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 thetemplates.git_push_bookmark template. For example:
[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.
[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:
[git]
executable-path = "/path/to/git"Gerrit settings
Default remote
To configure the default remote that jj gerrit upload will push to, setgerrit.default-remote. This can be overridden with the --remote option.
[gerrit]
default-remote = "gerrit"Default remote branch
To configure the default branch that jj gerrit upload will push to, setgerrit.default-remote-branch. This can be overridden with the--remote-branch option.
[gerrit]
default-remote-branch = "main"`Link` trailer
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 aLink trailer instead of a Change-Id trailer, set gerrit.review-url to the
URL preview you'd like to use.
[gerrit]
review-url = "https://review.gerrithub.io/your/project"Note that if a Change-Id or Link trailer already exists in a change, thenjj 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
[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, setsame-change = "keep".
keep: leave same-change conflict unresolvedaccept: resolve same-change conflict to new value (default)
[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 defaultjj converge uses the revsets.converge revset:
[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, setfsmonitor.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 settingfsmonitor.watchman.register-snapshot-trigger = true.
A full configuration for watchman can look like this:
[fsmonitor]
backend = "watchman"
[fsmonitor.watchman]
register-snapshot-trigger = trueYou 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 likejj status to take longer than expected. If you experience this runjj 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:
[snapshot]
max-new-file-size = "10MiB"
# the following is equivalent
max-new-file-size = 10485760The 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:
[snapshot]
auto-update-stale = trueThis 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.
[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 ofgitoxide 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.
[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:
jj config path --userOn 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:
# Ignores any settings specified in `{PLATFORM,/etc}/jj/{config.toml,conf.d/*.toml}`,
# but still loads repo and workspace configs.
JJ_CONFIG= jj logThere are also the --config-file and --config <NAME=VALUE>
global options which work with anyjj 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 usingjj 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:
#:schema https://docs.jj-vcs.dev/latest/config-schema.jsonThis 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:
VS Code
- Install Even Better TOML
Neovim/Vim
- Use with nvim-lspconfig and taplo
Helix
- Install taplo
JetBrains IDEs (IntelliJ, PyCharm, etc)
- Install TOML plugin
Emacs
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,
# Must not have spaces around the `=`
jj --config ui.color=always --config ui.diff-editor=meld splitThe 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:
# 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:
jj --config-file=extra-config.toml logConditional 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.
[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.
# In $XDG_CONFIG_HOME/jj/config.toml
[user]
name = "YOUR NAME"
email = "[email protected]"# 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 inconf.d/ directly using the --file option (the file will be created if it
doesn't already exist):
jj config set --file ~/.config/jj/conf.d/work.toml user.email "[email protected]"
jj config edit --file ~/.config/jj/conf.d/work.tomlAvailable 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 rootto see the workspace root directory. Note that the repository path
is in the main workspace if you're using multiple workspaces withjj workspace.--when.workspaces: List of paths to match the workspace path prefix.The same concerns about the path as for
--when.repositoriesapplies.Use
jj rootto see the workspace root directory.--when.hostnames: List of hostnames to match against theoperation.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
andstd::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), orNAMEto
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:
apple
grape
orangeOne 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:
<<<<<<< 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 endsThe 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:
APPLE
GRAPEFRUIT
ORANGEIn 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":
<<<<<<< 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 endsSome 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":
<<<<<<< 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:
<<<<<<<<<<<<<<< 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 endsConflicts 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):
<<<<<<< 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 endsTherefore, 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 thejj-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:
- 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. - 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, andcargo test --workspace. If you are preparing a PR, there are some additional
recommended steps.
Summary
One-time setup:
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-nextestDuring development (adapt according to your preference):
bacon clippy-all
cargo +nightly fmt # Occasionally
cargo nextest run --workspace # Occasionally
cargo insta test --workspace --test-runner nextest # Occasionally!!! warning
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.
Nearly any change to
jj's CLI will require writing or updating snapshot
tests that use theinstacrate. To make this
convenient, install thecargo-instabinary.
Usecargo insta test --workspaceto run tests,
andcargo insta review --workspaceto update the snapshot tests.
The--workspaceflag is needed to run the tests on all crates; by default,
only the crate in the current directory is tested.GitHub CI checks require that the code is formatted with the nightly
version ofrustfmt. To do this on your computer, install the nightly
toolchain and usecargo +nightly fmt.Your code will be rejected if it cannot be compiled with the minimal
supported version of Rust ("MSRV"). Currently,jjfollows a rather
casual MSRV policy: "The currentrustcstable version, minus one."
As of this writing, that version is 1.97.1.Your code needs to pass
cargo clippy. You can also
usecargo +nightly clippyif you wish to see more warnings.You may also want to install and use
bacon,
to automatically build, check, and / or run tests.To run tests more quickly, use
cargo nextest run --workspace. To
usenextestwithinsta, usecargo insta test --workspace --test-runner nextest.On Linux, you may be able to speed up
nextesteven further by using
themoldlinker, as explained below.
Configuring `jj fix` to run `rustfmt`
Run this in the jj repo:
jj config set --repo fix.tools.rustfmt '{ command = ["rustfmt", "+nightly"], patterns = ["glob:**/*.rs"] }'Note: users of Nix and
direnvshould drop the"+nightly"argument above
since the devShell is already configured to pull in a nightly version of
rustfmt:shelljj 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 upcargo 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.:
mold -run cargo insta test --workspace --test-runner nextestThere 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 runningreadelf -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:
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 FinderYou 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,
TMPDIR=/Volumes/RAMDisk cargo insta test --workspace --test-runner nextestYou can double-check whether or not the RAM disk is mounted with
mount | grep tmpfsSome 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 andfdatasync. 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:
{
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"[rust]": {
"files.trimTrailingWhitespace": false
}
}Zed
// .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
# .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 providedflake.nix to get a development environment with all necessary tools
pre-configured:
nix developOnce 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"
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"
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 usingmise. 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 squashwould run tests with "squash" in their name.mise build: Compilesjj.mise build:release: Compilesjjin release mode.mise build:docs: Builds the documentation forjj.
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"
```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"
```shell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```=== "Homebrew"
```shell
brew install uv
```=== "Cargo"
```shell
# This might take a while
cargo install --git https://github.com/astral-sh/uv uv
```=== "Other options"
* 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:
uv run mkdocs serveOpen 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:
MKDOCS_OFFLINE=true uv run mkdocs buildBuilding the entire website
!!! tip
Building the entire website is not usually necessary. If you are editing
documentation, the previous section is enough.
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 withmike, which can be run withuv 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):
Check out
jj(jj git clone), cloned from your fork ofjj(e.g.
github.com/jjfan/jj). You can also use a pure Git repo if you prefer.Make sure
github.com/jjfan/jjincludes thegh-pagesbookmark of the jj repo
and rungit fetch origin gh-pages.Go to the GitHub repository settings, enable GitHub Pages, and configure them
to use thegh-pagesbookmark (this is usually the default).Install
uvas explained in Previewing the HTML
documentation, and run the sameshscript
that is used in GitHub CI (details below):shell.github/scripts/docs-build-deploy prerelease main --pushThis should build the version of the docs from the current commit,
deploy it as a new commit to thegh-pagesbookmark,
and push thegh-pagesbookmark to the origin.Now, you should be able to see the full website, including your latest changes
to theprereleaseversion, athttps://jjfan.github.io/jj/prerelease/.(Optional) The previous steps actually only rebuild
https://jjfan.github.io/jj/prerelease/and its aliashttps://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.shelljj 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(Optional) When you are done, you may want to reset the
gh-pagesbookmark 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-pagesIf you want to preserve some of the changes you made, you can do
jj bookmark set my-changes -r gh-pagesBEFORE 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 todocs-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 thesite_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
protoccompiler. This usually means eitherapt-get install protobuf-compileror downloading an official release. Theprostlibrary docs have additional advice. - Run
cargo run -p gen-protosregularly (or after every edit to a.proto
file). This is the same as runningcargo runfromlib/gen-protos. Thegen-protosbinary will use theprost-buildlibrary to compile the.protofiles into.rsfiles. - If you are adding a new
.protofile, you will need to edit the list of
these files inlib/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:
- Tracing-based profiling - Uses the Rust
tracingcrate to record spans
and events. Good for understanding control flow and finding where time is spent
in instrumented code. - 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:
# 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 stTo include function argument values in the trace (useful for detailed
debugging):
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:
cargo install samply
samply record jj diffThen just open the link it prints.
Using cargo-flamegraph
cargo-flamegraph generates flamegraph visualizations using system profilers:
cargo install flamegraph
cargo flamegraph --bin jj -- diffYou 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.
Related Work (optional)
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
- Add a new markdown document to
docs/design, named after your improvement
or project. - Describe the current state of the world and the things you want to improve.
- Wait for the Maintainers and Stakeholders to show up.
- 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"orcwd-file:"path": Matches cwd-relative file (or exact) path.glob:"pattern"orcwd-glob:"pattern": Matches file paths with cwd-relative
Unix-style shell wildcardpattern. For example,glob:"*.c"will
match all.cfiles in the current working directory non-recursively.prefix-glob:"pattern"orcwd-prefix-glob:"pattern": Likeglob:, but also
matches path prefix (file or files under directory recursively.) For example,prefix-glob:"*.d"is equivalent toglob:"*.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 wildcardpattern.root-prefix-glob:"pattern": Likeroot-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 namedp.
~x: Matches everything butx.
x & y: Matches bothxandy.x ~ y: Matchesxbut noty.
x | y: Matches eitherxory(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:
[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:
[fileset-aliases]
LOCK = {
definition = '**/Cargo.lock | **/package-lock.json | **/uv.lock',
doc = 'Lockfiles'
}You can also use the dotted key syntax:
[fileset-aliases]
LOCK.definition = '**/Cargo.lock | **/package-lock.json | **/uv.lock'
LOCK.doc = 'Lockfiles'Examples
Show diff excluding Cargo.lock.
jj diff '~Cargo.lock'List files in src excluding Rust sources.
jj file list 'src ~ glob:"**/*.rs"'Split a revision in two, putting foo into the second commit.
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:
# 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/projectIf 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):
[gerrit]
default-remote = "gerrit" # name of the Git remote to push to
default-remote-branch = "main" # target branch in GerritBasic 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 existingChange-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-Idtrailer. When
you re-upload a commit with the sameChange-Id, Gerrit creates a new patch
set.
Upload a single change
# 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.
$ jj gerrit upload -r '@-' --remote-branch main --dry-runTarget 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 ofjj gerrit upload. - Otherwise, a remote named
gerritwill 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-branchto set
your default branch across all repos. - Run
jj config set --repo gerrit.default-remote-branchto set
your default branch for this specific repo. - Use
--remote-branchto use a specific branch in one invocation
ofjj 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:
# 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:
[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 sameChange-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 withjj 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.
Alternative `Link` trailer
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'sjj gerrit upload will do the same if you setjj 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()'
orjj 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--allby--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
orrg --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 ...
orgit 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 withjj edit X.Use case: Start working on a new change based on the
bookmark/branch
Git command: >
git switch -c topic main
orgit 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 (seejj 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 ingit 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 ingit 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
orjj b lfor 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>
orjj 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 matchingjj redocommand 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 betweenHEADand 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 mostgit rerereuse cases.Descendant commits are automatically rebased. Whenever you rewrite a
commit (e.g. by runningjj 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 showdoesn't show them well (without--remerge-difffrom 2022), andgit rebasedrops the changes (without--rebase-mergesfrom 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 amainbranch, you'll get a
bookmark by that name in your local repo. If you then move it and push back to
the remote, themainbranch 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. DetailsThere'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 ofgit 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 usegit 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.
(gitis used for remote operations) core.excludesFile
- The configuration of remotes (
- Authentication: Yes.
gitis 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
.gitignorefiles are supported. So are
ignores in.git/info/excludeor configured via Git'score.excludesFile
config. Since working-copy files are snapshotted by almost everyjjcommand,
you might need to runjj file untrackto exclude newly ignored files from the
working-copy commit. It's recommended to set up the ignore patterns earlier.
The.gitignoresupport uses a native implementation, so please report a bug
if you notice any difference compared togit. - .gitattributes: No. There's #53
about adding support for at least theeolattribute. - 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 diffwill 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 gcin 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 thejj workspacefamily of commands. - Sparse checkouts: No. However, there's native support for sparse
checkouts. See thejj sparsecommand. - Signed commits: Yes.
You can sign commits automatically by configuration,
or use thejj signcommand. - 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:
$ cd path/to/git-repo
$ jj git initJujutsu 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 bothjj 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:
$ 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.) Seejj 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. Usejj 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
jjandgitcommands 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 rungit fetchin the background from time to
time.In colocated workspaces with a very large number of branches or other refs,
jjcommands can get noticeably slower because of the automaticjj git importexecuted on each command. This can be mitigated by
occasionally runningjj util gcto speed up the import (that command
includes packing the Git refs).Git tools will have trouble with revisions that contain conflicted files.
Whilejjrenders 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
jjbranch 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, unfinishedgit rebasestates, 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
jjandgitcommands,
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 thejj git colocation command.
To check the current colocation status of your workspace:
jj git colocation statusTo convert to a colocated workspace:
jj git colocation enableTo convert to a non-colocated workspace:
jj git colocation disableThe jj git colocation enable command automates the following manual process:
# 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
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 runjj 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 thegit.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, usegit-cat-file like this:
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.
# 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 -iAutomatic 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:
git add file1 file2
git commit --fixup abc
git rebase -i --autosquashWith 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:
jj squash --into abc file1 file2Undo 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 undoreverts the last operation in one step, without needing to figure
out which ref to reset. You can repeatjj undoto continue stepping backwards
in time.jj op log -pshows operations with diffs so you can inspect what happened.--at-operation IDlets 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 rungit 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.
# 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 shortUsing a named bookmark
In this example, we create a bookmark named bar and then push it to the remote.
# 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 pushWhile 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 tojj 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.
$ 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 pushWorking 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.
$ # Do your work
$ jj commit
$ # Push change "mw", letting Jujutsu automatically create a bookmark called
$ # "push-mwmpwkwknuz"
$ jj git push -c mwAddressing 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:
$ # 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 pushNotably, the above workflow creates a new commit for you. The same can be
achieved without creating a new commit.
!!! note
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.$ # 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 pushRewriting commits
If your project prefers that you keep commits clean, you can do that by doing
something like this:
$ # 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-featureThe 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 usuallymain 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, setremotes.<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:
$ GIT_DIR=$(jj git root) gh issue listYou 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:
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:
$ jj log -r 'bookmarks() & ~(main | remote_bookmarks())'Log all revisions that you authored, across all bookmarks that aren't on any
remote:
$ jj log -r 'mine() & bookmarks() & ~remote_bookmarks()'Log all remote bookmarks that you authored or committed to:
$ jj log -r 'remote_bookmarks() & (mine() | committer([email protected]))'Log all ancestors of the current working copy that aren't on any remote:
$ 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.
$ jj git clone --remote upstream https://github.com/upstream-org/repo
$ cd repo
$ jj git remote add origin [email protected]:your-org/your-repo-forkThis 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):
[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:
[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 -oorjj 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 asxyz/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.
Head
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:
# Will put the jj binary for the latest release in ~/.cargo/bin by default
cargo binstall --strategies crate-meta-data jj-cliWithout the --strategies option, you may get equivalent binaries that should
be compiled from the same source code.
!!! note
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 thebuild-essential package is installed by running something like this:
sudo apt-get install build-essentialNow run either:
# To install the *prerelease* version from the main branch
cargo install --git https://github.com/jj-vcs/jj.git --locked --bin jj jj-clior:
# To install the latest release
cargo install --locked --bin jj jj-cliArch Linux
You can install the jujutsu package from the official extra repository:
pacman -S jujutsuOr install from the AUR repository with an AUR Helper:
yay -S jujutsu-gitNixOS
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:
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:
# Installs the prerelease version from the main branch
nix profile install 'github:jj-vcs/jj'Homebrew
If you use Homebrew, you can run:
# Installs the latest release
brew install jjGentoo 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:
emerge -av dev-vcs/jjopenSUSE Tumbleweed
jujutsu can be installed from the official openSUSE-Tumbleweed-Oss repository:
zypper install jujutsuFedora Linux
jujutsu has an unofficial COPR
repo for Fedora:
sudo dnf copr enable aldantanneo/jj-vcs
sudo dnf install jj-cliMac
From Source
First make sure that you have a Rust version >= 1.97.1. You may also need to run:
xcode-select --installNow run either:
# To install the *prerelease* version from the main branch
cargo install --git https://github.com/jj-vcs/jj.git \
--locked --bin jj jj-clior:
# To install the latest release
cargo install --locked --bin jj jj-cliHomebrew
If you use Homebrew, you can run:
# Installs the latest release
brew install jjMacPorts
You can also install jj via the MacPorts jujutsu
port:
# Installs the latest release
sudo port install jujutsuWindows
First make sure that you have a Rust version >= 1.97.1. Now run either:
# To install the *prerelease* version from the main branch
cargo install --git https://github.com/jj-vcs/jj.git --locked --bin jj jj-clior:
# To install the latest release
cargo install --locked --bin jj jj-clivia winget:
# To install the latest release via winget
winget install jj-vcs.jjvia scoop:
# To install the latest release via scoop
scoop install main/jjFreeBSD
devel/jujutsu is available in the ports.
Install pre-built pkg:
# To install the latest release via pkg
pkg install jujutsuInstall from building the port:
# To install the latest release via ports
cd /usr/ports/devel/jujutsu/ && make install cleanRuntime 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.
$ 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?"
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"
```shell
source <(jj util completion bash)
```=== "Dynamic"
```shell
source <(COMPLETE=bash jj)
```Zsh
=== "Standard"
```shell
autoload -U compinit
compinit
source <(jj util completion zsh)
```=== "Dynamic"
```shell
source <(COMPLETE=zsh jj)
```Fish
=== "Standard"
```shell
jj util completion fish | source
```=== "Dynamic"
!!! note
No configuration is required with fish >= 4.0.2 which loads dynamic completions by default.
```shell
COMPLETE=fish jj | source
```Nushell
=== "Standard"
```nu
jj util completion nushell | save -f completions-jj.nu
use completions-jj.nu * # Or `source completions-jj.nu`
```Powershell
=== "Standard"
```powershell
jj util completion power-shell | Out-String | Invoke-Expression
```=== "Dynamic"
```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
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 ofx(e.g.@-)x+: Children ofx
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/orjj 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 startedjj 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.
Paid Contributors (`docs/paid_contributors.md`)
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
Related Work (`docs/related-work.md`)
Related work
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
jj log -r 'heads(tags())' # Check that this shows the previous version
jj diff --from 'heads(tags())' --to main CHANGELOG.mdMake 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:
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 -fhttps://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#compare-two-commits
Alternatively, the list can be produced locally:
jj log -G -r 'heads(tags())..main' -T '"* " ++ author ++ "\n"' | sort -fuThen 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
- Go to https://github.com/jj-vcs/jj/releases and click "Draft a new release"
- Click "Choose a tag" and enter "v0.<number>.0" (e.g. "v0.26.0") to create a
new tag - Click "Target", then "Recent commits", and select the commit from your merged
PR - Use the name (e.g. "v0.26.0") as "Release title". Paste the changelog entries
into the message body - Check "Create a discussion for this release"
- Click "Publish release"
Publish the crates to crates.io
Go to a terminal and create a new clone of the repo [^1]:
cd $(mktemp -d)
jj git clone https://github.com/jj-vcs/jj
cd jj
jj new v0.<number>.0Publish each crate:
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 asjj 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 tohidden_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:
- Tag name
- Bookmark name
- 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 ofx, can be empty.x+: Children ofx, can be empty.
p:x: String/date pattern or pattern alias namedp.
x::: Descendants ofx, including the commits inxitself. Equivalent tox::visible_heads()if no hidden revisions are mentioned.x..: Revisions that are not ancestors ofx. Equivalent to~::x, andx..visible_heads()if no hidden revisions are mentioned.::x: Ancestors ofx, including the commits inxitself. Shorthand forroot()::x...x: Ancestors ofx, including the commits inxitself, but excluding
the root commit. Shorthand forroot()..x. Equivalent to::x ~ root().x::y: Descendants ofxthat are also ancestors ofy. Equivalent
tox:: & ::y. This is whatgit logcalls--ancestry-path x..y.x..y: Ancestors ofythat are not also ancestors ofx. Equivalent to::y ~ ::x. This is whatgit logcallsx..y(i.e. the same as we call it).
Note that this is not a "path" betweenxandyin the commit graph --x
andydo not need to be related by ancestry.::: All visible commits in the repo. Equivalent toall(), androot()::visible_heads()if no hidden revisions are mentioned...: All visible commits in the repo, but excluding the root commit.
Equivalent to~root(), androot()..visible_heads()if no hidden revisions
are mentioned.
~x: Revisions that are not inx.
x & y: Revisions that are in bothxandy.x ~ y: Revisions that are inxbut not iny.
x | y: Revisions that are in eitherxory(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
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"
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 asx-.parents(x, depth)returns the parents ofxat the givendepth. For
instance,parents(x, 3)is equivalent tox---.children(x, [depth]):children(x)is the same asx+.children(x, depth)returns the children ofxat the givendepth. For
instance,children(x, 3)is equivalent tox+++.ancestors(x, [depth]):ancestors(x)is the same as::x.ancestors(x, depth)returns the ancestors ofxlimited to the givendepth.descendants(x, [depth]):descendants(x)is the same asx::.descendants(x, depth)returns the descendants ofxlimited to the givendepth.first_parent(x, [depth]):first_parent(x)is similar toparents(x), but
for merges, it only returns the first parent instead of returning all parents.
Thedepthargument also works similarly, sofirst_parent(x, 2)is
equivalent tofirst_parent(first_parent(x)).first_ancestors(x, [depth]): Similar toancestors(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, sofirst_ancestors()can be used to exclude changes made on other branches.reachable(srcs, domain): All commits reachable fromsrcs, traversing all
parent and child edges, such that the entire path is withindomain. 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 asx::x. Useful whenxincludes 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. Ifpatternis specified,
this selects the bookmarks whose name match the given string
pattern. For example,bookmarks(*push*)would match the
bookmarkspush-123andrepushedbut not the bookmarkmain. 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 thename_patternis specified,
the bookmarks whose names match the given string pattern
across all remotes are selected. If bothname_patternandremote_pattern
are specified, the selection is further restricted to just the remotes whose
names matchremote_pattern.For example,
remote_bookmarks(*push*, *ri*)would match the bookmarkspush-123@originandrepushed@privatebut notpush-123@upstreamormain@originormain@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"orremote="*"to select bookmarks including@gitones.tracked_remote_bookmarks([name_pattern], [[remote=]remote_pattern]): All
targets of tracked remote bookmarks. Supports the same optional arguments asremote_bookmarks().untracked_remote_bookmarks([name_pattern], [[remote=]remote_pattern]): All
targets of untracked remote bookmarks. Supports the same optional arguments asremote_bookmarks().tags([pattern]): All tag targets. Ifpatternis specified, this selects
the tags whose name match the given string pattern. For
example,tags(*v1*)would match the tagsv123andrev1but not the tagv2. 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. Seeremote_bookmarks()for arguments.tracked_remote_tags([name_pattern], [[remote=]remote_pattern]): All targets
of tracked remote tags. Supports the same optional arguments asremote_tags().untracked_remote_tags([name_pattern], [[remote=]remote_pattern]): All
targets of untracked remote tags. Supports the same optional arguments asremote_tags().visible_heads(): All visible heads (same asheads(all())if no hidden
revisions are mentioned).root(): The virtual commit that is the oldest ancestor of all other commits.heads(x): Commits inxthat are not ancestors of other commits inx.
Equivalent tox ~ ::x-. Note that this is different from
Mercurial'sheads(x)
function, which is equivalent tox ~ x-.roots(x): Commits inxthat are not descendants of other commits inx.
Equivalent tox ~ x+::. Note that this is different from
Mercurial'sroots(x)
function, which is equivalent tox ~ x+.latest(x, [count]): Latestcountcommits inx, based on committer
timestamp. The defaultcountis 1.fork_point(x): The fork point of all commits inx. The fork point is the
common ancestor(s) of all commits inxwhich do not have any descendants
that are also common ancestors of all commits inx. It is equivalent to
the revsetheads(::x_1 & ::x_2 & ... & ::x_N), wherex_{1..N}are commits
inx. Ifxresolves to a single commit,fork_point(x)resolves tox.fork_point(x | y)is whatgit merge-base x ycalculates (but unlikegit merge-base --fork-point, thefork_point()function does not take a
bookmark's history into account).merge_point(x): The merge point of all commits inx. Similar to the fork
point, the merge point is the common descendant(s) of all commits inxwhich
do not have any ancestors that are also common descendants of all commits inx. It is equivalent to the revsetroots(x_1:: & x_2:: & ... & x_N::),
wherex_{1..N}are commits inx. Ifxresolves to a single commit,merge_point(x)resolves tox.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): Evaluatesx, and errors if it is not of exactly sizecount. Otherwise, returnsx. This is useful in particular withcount=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, anddescription("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 toauthor_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 toauthor_email(exact-i:)committer(pattern): Commits with the committer's name or email matching the
given string pattern. Equivalent tocommitter_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 includesmerges()without
user modifications androot().files(expression): Commits modifying paths matching the given fileset
expression.Paths are relative to the directory
jjwas invoked from. A directory name
will match all files in that directory and its subdirectories.For example,
files(foo)will match filesfoo,foo/bar,foo/bar/baz.
It will not matchfoobarorbar/foo.Some file patterns might need quoting because the
expressionmust also be
parsable as a revset. For example,.has to be quoted infiles(".").diff_lines(text, [files]): Commits containing diffs matching the giventextpattern line by line.The search paths can be narrowed by the
filesexpression. 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]): likediff_lines()above, but matches only
the "added" side of the diff.diff_lines_removed(text, [files]): likediff_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 asx, but evaluated tonone()if any of the commits
inxdoesn't exist (e.g. is an unknown bookmark name.)coalesce(revsets...): Commits in the first revset in the list ofrevsets
which does not evaluate tonone(). If all revsets evaluate tonone(), then
the result ofcoalescewill also benone().working_copies(): The working copy commits across all the workspaces.at_operation(op, x): Evaluatesxat 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 toat_operation(op, x) | ::(at_operation(op, x | visible_heads()) | visible_heads()).
??? examples
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 tostring.glob:"pattern": Matches strings with Unix-style shell wildcardpattern.regex:"pattern": Matches substrings with regular
expressionpattern.substring:"string": Matches strings that containstring.
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 butx.x & y: Matches bothxandy.x ~ y: Matchesxbut noty.x | y: Matches eitherxory(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:
[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:
[revset-aliases]
HEAD = { definition = '@-', doc = 'The parent of the working-copy commit' }You can also use the dotted key syntax:
[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 namedupstreamororigin. 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 bookmarksmain,master, andtrunkon theupstreamandoriginremotes. If more than one potential trunk commit
exists, the newest one is chosen. If none of the bookmarks exist, the revset
evaluates toroot().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 topresent(@) | ancestors(immutable_heads().., 2) | trunk(). It is used as the default value
forrevsets.log, which is the set of revisions shown byjj logif no
revisions or paths are specified.builtin_immutable_heads(): Resolves totrunk() | tags() | untracked_remote_bookmarks() | untracked_remote_tags(). It is used as the
default definition forimmutable_heads()below. It is not recommended to
redefine this alias. Prefer to redefineimmutable_heads()instead.immutable_heads(): The heads of the set of immutable commits (not "heads
that are immutable"). Resolves totrunk() | tags() | untracked_remote_bookmarks() | untracked_remote_tags()by default. It is
actually defined asbuiltin_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 thatjjtreats 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, editimmutable_heads().mutable(): The set of commits thatjjtreats 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, editimmutable_heads().visible(): The set of visible commits. Resolves to::visible_heads().
This is equal toall()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):
jj log -r @-Show all ancestors of the working copy (like plain git log)
jj log -r ::@Show commits not on any remote bookmark:
jj log -r 'remote_bookmarks()..'Show commits not on origin (if you have other remotes like fork):
jj log -r 'remote_bookmarks(remote=origin)..'Show the initial commits in the repo (the ones Git calls "root commits"):
jj log -r 'root()+'Show some important commits (like git log --simplify-by-decoration):
jj log -r 'tags() | bookmarks()'Show local commits leading up to the working copy, as well as descendants of
those commits:
jj log -r '(remote_bookmarks()..@)::'Show commits authored by "martinvonz" and containing the word "reset" in the
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) andsplitcommands, 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 forsl 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 viajj op log, so you can tell how far back you want to go back.
Sapling hassl 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
youjj undoajj commit,jj diffwill show the same changes as
beforejj commit, but if yousl undoasl 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 usejjandgitinterchangeably 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 hasjj git push --changefor
automatically creating branches for specified commits. You have to specify
each commit you want to create a branch for by usingjj 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 likejj git push -r main..@
(to push all branches on the current stack of commits from where it forked
frommain).
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 usingjj-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:
// 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:
// 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:
// 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()andtry_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
*Argsstruct.
For example, documentation forjj newgoes onstruct NewArgs.As above, write an appropriate short first line for the command, but do not
include a trailing period / full stop. Seejj -hfor examples (--helpwill
show the full doc comment for each command, while the short form-hwill
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 configorjj 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.
textCORRECT: $ jj new -h Create a new, empty change and (by default) edit it in the working copy $ jj bookmark create -h Create a new bookmarktextINCORRECT: $ 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.
- For example,
Default aliases (i.e., those defined in
misc.toml) are not
automatically shown byclap, 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 ofjj describeis:textUpdate 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_dirtest 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. Seejj <command> -hfor examples
(--helpwill show the full doc comment for each arg, while the short form-hwill 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 toclap:This section is mostly discussing the user-facing argument / option names;rust#[arg(long = "flag")] my_values: MyType,
how you name variables is up to you.
- Note that the name of the struct field can be different from the name of
textCORRECT / 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)textINCORRECT / 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.logfor--revisionsinjj 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.
- In cases where this default comes from a configurable setting, such as
If you are unsure how your comment will look after being processed, run the
test_generate_markdown_docs_in_docs_dirtest 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:
textfunction_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: Typepart;
importantly, do not put the brackets around any surrounding commas.textCORRECT: parents(x, [depth]) if(condition: Boolean, then: Template, [else: Template]) -> TemplatetextINCORRECT: parents(x[, depth]) if(condition: Boolean, then: Template[, else]) -> TemplateFor named parameters (rare), put the parameter name in the format
[name=]
before thearg: Typepart, with no spaces. Note that this should also be
included in the square brackets indicating that the parameter is optional.nameandargmay 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.)textCORRECT: remote_bookmarks([name_pattern], [[remote=]remote_pattern])textINCORRECT: remote_bookmarks([name_pattern], [remote] = [remote_pattern]) remote_bookmarks([name_pattern], [remote=remote_pattern])For variadic parameters, add an ellipsis (
...) after thearg: Typepart.textCORRECT: coalesce(revsets...) concat(content: Template...) -> TemplatetextINCORRECT: 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.
textCORRECT: 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.textINCORRECT: 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
backticksto 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
(namedbuiltin_...) 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 withbuiltin_, and they should preferably have a
corresponding function overload that the non-function alias calls withself.
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--templateflag).For
[template-aliases], prefer to name functions with the prefixformat_.For
[colors], prefer to define colors for labels rather than for the color
or style. That is, do not add acolors.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 asprefix,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,selfis theCommittype, socommit_idis
equivalent toself.commit_id(). - In
jj op log,selfis theOperationtype, socurrent_operationis equivalent toself.current_operation(). - In
jj file list,selfis theTreeEntrytype, sopathis equivalent toself.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 namedp.
x * y,x / y,x % y: Multiplication / division / remainder. Operands must
beIntegers.
x + y,x - y: Addition / subtraction. Operands must beIntegers.
x >= y,x > y,x <= y,x < y: Greater than or equal / greater than /
lesser than or equal / lesser than. Operands must beIntegers.
x == y,x != y: Equal / not equal. Operands must be eitherBoolean,ByteString,Integer, orString.
x && y: Logical and, short-circuiting.
x || y: Logical or, short-circuiting.
x ++ y: Concatenatexandytemplates.
Global functions
The following functions are defined.
fill(width: Integer, content: Template, [break_words: bool]) -> Template:
Fill lines at the givenwidth. Ifbreak_wordsistrue, any words longer
thanwidthwill be split to fit inwidthcharacters.indent(prefix: Template, content: Template) -> Template: Indent
non-empty lines by the givenprefix.pad_start(width: Integer, content: Template, [fill_char: Template]): Pad (or
right-justify) content by adding leading fill characters. Thecontent
shouldn't have newline character.pad_end(width: Integer, content: Template, [fill_char: Template]): Pad (or
left-justify) content by adding trailing fill characters. Thecontent
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. Thecontentshouldn't have newline characters.truncate_start(width: Integer, content: Template, [ellipsis: Template]):
Truncatecontentby removing leading characters. Thecontentshouldn't
have newline character. Ifellipsisis provided andcontentwas truncated,
prepend theellipsisto the result.truncate_end(width: Integer, content: Template, [ellipsis: Template]):
Truncatecontentby removing trailing characters. Thecontentshouldn't
have newline character. Ifellipsisis provided andcontentwas truncated,
append theellipsisto 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. Thelabelis evaluated as a
space-separated string.hyperlink(url: Stringify, text: Template, [fallback: Template]) -> Template:
Rendertextas a hyperlink tourlusing OSC 8 escape sequences
when outputting with color enabled. Otherwise, rendersfallbackinstead,
which defaults totext. Use--color=alwaysto force hyperlinks when piping
output to a terminal emulator that supports OSC 8.raw_escape_sequence(content: Template) -> Template: Preserves any escape
sequences incontent(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 ofpatternincontentby formatting thereplacement
lambda for each match.This is similar to
String.replace(), but operates onTemplatecontent and preserves formatting of the parts ofcontentthat are
not replaced. The replacement itself is newly formatted template content.The lambda argument is a
RegexCapturesvalue 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: Formatcontentto string. This
effectively removes color labels.json(value: Serialize) -> String: Serializevaluein JSON format. To
serialize multiple values as a JSON object, usejson({key => value, ..}).
For example,json({"commit_id" => commit_id, "tags" => tags}).if(condition: Boolean, then: Any, [else: Any]) -> Any:
Conditionally evaluates tothen/elsecontent.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 ascontent_1 ++ ... ++ content_n.join(separator: Template, content: Template...) -> Template: Insertseparatorbetweencontents.separate(separator: Template, content: Template...) -> Template: Insertseparatorbetween non-emptycontents.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 byname.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 exampleself.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 givenseparator.
`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: Whetherneedleis a
prefix of the string..ends_with(needle: ByteStringify) -> Boolean: Whetherneedleis 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 andendis exclusive. Negative values count from the end
of the string, with-1being the last byte. Ifendis not given, returns
fromstartto 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 givenseparatorpattern. Iflimitis
specified, it determines the maximum number of elements in the result, with
the remainder of the string returned as the final element. Alimitof 0
returns an empty list..replace(pattern: StringPattern, replacement: ByteStringify, [limit: Integer]) -> ByteString: Replace occurrences of the givenpatternwith thereplacementstring.By default, all occurrences are replaced. If
limitis specified, at most
that many occurrences are replaced.Supports capture groups in patterns using
$0(entire match),$1,$2etc..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 multipleTrailers 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 thefilesexpression. 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 thefilesexpression. 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 tojj 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
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
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 givenseparator..filter(|item| expression) -> List: Filter list elements by predicateexpression. Example:description.lines().filter(|s| s.contains("#")).map(|item| expression) -> AnyList: Apply templateexpression
to each element. Example:parents.map(|c| c.commit_id().short()).any(|item| expression) -> Boolean: Returns true if any element satisfies
the predicateexpression. Example:parents.any(|c| c.description().contains("fix")).all(|item| expression) -> Boolean: Returns true if all elements satisfy
the predicateexpression. 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 atindex(0-based)..reverse() -> List: Returns the list in reverse order..skip(count: Integer) -> List: Skips the firstcountelements and
returns the rest..take(count: Integer) -> List: Returns only the firstcountelements.
`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 keykey.
`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 forOption 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 globalreplace() 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 atindex.
Capture group 0 is the full match. Errors if the index is out of bounds..name(name: Stringify) -> ByteString: Returns the named capture groupname. 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
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 is0. 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. Thestart/endindices should be specified in units of UTF-8 bytes. Indices are
0-based andendis exclusive. Negative values count from the end of the
string, with-1being the last byte. If thestartindex is in the middle
of a UTF-8 codepoint, the codepoint is fully part of the result. If theend
index is in the middle of a UTF-8 codepoint, the codepoint is not part of the
result. Ifendis not given, returns fromstartto 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 givenseparatorpattern. Iflimitis specified, it
determines the maximum number of elements in the result, with the remainder of
the string returned as the final element. Alimitof 0 returns an empty
list..replace(pattern: StringPattern, replacement: Stringify, [limit: Integer]) -> String: Replace occurrences of the givenpatternwith thereplacement
string.By default, all occurrences are replaced. If
limitis specified, at most
that many occurrences are replaced.Supports capture groups in patterns using
$0(entire match),$1,$2etc..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. UnlikeTemplate, 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 valueHH
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 betweenstartandself. It may be used in conjunction withTimestampRange::durationto obtain
a human-friendly duration between twoTimestamps.
`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.widthsets the entire width of the diff stat
including paths, andmax_bar_widthlimits 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:
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:
jj op log -T 'self.id().short()' --color=debugThis 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 thelabel(label, content) function. For example,
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:
jj config list --include-defaults --include-overridden colorsThe default colors can be found in colors.toml, and the default
explicitly added labels (such as mutable or divergent) can be found intemplates.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:
[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:
[template-aliases]
sh = { definition = 'commit_id.short()', doc = 'Short commit ID' }You can also use the dotted key syntax:
[template-aliases]
sh.definition = 'commit_id.short()'
sh.doc = 'Short commit ID'Examples
Print the object IDs on both sides of a diff:
jj diff -T 'path ++ " " ++ self.source().normal_value() ++ " -> " ++ self.target().normal_value() ++ "\n"'Get short commit IDs of the working-copy parents:
jj log -G -r @ -T 'parents.map(|c| c.commit_id().short()).join(",")'Show machine-readable list of full commit and change IDs:
jj log -G -T 'commit_id ++ " " ++ change_id ++ "\n"'Print the description of the current commit, defaulting to (no description set):
jj log -G -r @ -T 'coalesce(description, "(no description set)\n")'For each revision, print the name of its first bookmark or change ID:
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 undoto 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 nextandjj 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:
# 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-WorldRunning jj st (short for jj status) now yields something like this:
$ 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-1Let'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 ID7fd1a60b.
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 usinggit login 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 nextjj 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:
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:
# 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-1Now make the change in the README:
# 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-1Note that you didn't have to tell Jujutsu to add the change like you would withgit 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 runjj file untrack .
Also note that the commit hash for our current change (kntqzsqt) changed frome427edcf to 5d39e19d!
To see the diff, run jj diff:
$ 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 (likegit 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:
$ 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 goodbyeIf 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:
$ 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:
$ 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() 00000000The 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, likegit 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 logomits some commits you expect to see, you
can always runjj 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.
# 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.
$ 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:
/* 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:
$ 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.
$ 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 ownjj 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:
$ 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:
$ 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 ABCThat 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 theui.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:
$ 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
+CThe 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.
$ 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:
$ cd path/to/my-git-repo
$ jj git initThis 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
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.
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 abandonThis 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:
PS> jj config set --user ui.pager '["C:\\Program Files\\Git\\usr\\bin\\less.exe", "-FRX"]'
PS> jj config set --user ui.paginate autoTyping `@` in PowerShell
PowerShell uses @ as part the array sub-expression operator, so it
often needs to be escaped or quoted in commands:
PS> jj log -r `@
PS> jj log -r '@'One solution is to create a revset alias. For example, to make HEAD an alias
for @:
PS> jj config set --user revset-aliases.HEAD '@'
PS> jj log -r HEADWSL 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:
PS> jj workspace add --name wsl ~/my-repoThen only use the ~/my-repo workspace from Linux.
Symbolic link support
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 thegit 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 thesnapshot.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 ofsnapshot.auto-track), but files that were already tracked will remain tracked
even if they match ignore patterns. You can untrack such files with thejj file untrack command.
Workspaces
You can have multiple working copies backed by a single repo. Usejj 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:
- Snapshot the working copy (which gets recorded as an operation)
- Create new commits etc. "in memory" and record that as a new operation
- 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 renamedbar 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:
$ 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 aboveReverting 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:
$ jj log
B rename foo->bar
|
A add foo
$ jj revert -r B -o B
$ jj diff --from B- --to B+ # Should be emptyParallelize/serialize
This is a special case of the lossless rebase.
$ 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:
$ 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 CWe 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:
$ jj log
D rename foo2->foo3 and bar2->bar3
|\
C | rename bar->bar2
| |
| B rename foo->foo2
|/
A add foo and barCopies across merge commit
$ jj log
D delete baz
|\
C | rename foo->baz
| |
| B rename foo->bar
|/
A add foojj diff --from C --to D should now show a baz->bar rename (just likejj 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:
// 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:
M rename foo->baz, create bar
|
| L copy foo->bar, create baz
|/
K add fooAssuming the new files are different in each commit, we get the following trees.
Notation:
idis the hash of the contents (theFileId)- The
2:bar->1:foomeans that copy ID 2 (i.e. hash of theCopyHistory
struct) has filebar, which was copied from copy ID1, where it was
calledfoo.
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:fooThis graph also shows the relationship between the copy IDs and which commits
they appear in:
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 --> 1Let'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 runningjj 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 toM (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 betweenfoo 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 forfoo, we won't consider bar renamed to it. So we consider bar as copied
into baz.
So we get these diffs:
bazis deleted (deleting contentL)baris created (with contentM)foois renamed tobaz(showing diff fromKtoM)baris merged intobaz(showing diff fromLtoM)
Example: Divergent copy and rename (best rename target)
N copy baz->qux
|
M rename foo->baz
|
| L rename foo->bar
|/
K add fooWhen 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:
baris renamed tobazbaris copied toqux
Example: Copy onto deleted file
M copy foo->bar
|
L delete bar
|
K add foo, barWhen 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 thatbar 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:
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:
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 filefoo 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
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
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:
$ jj log
C rename bar->baz
|
| B rename foo->baz
|/
A add foo, add bar
$ jj new B CIt 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:
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
$ jj log
C rename bar->baz
|
B rename foo->bar
|
A add foo
$ jj rebase -r C -o A$ jj log
C rename foo->baz
|
| B rename foo->bar
|/
A add foo
$ jj rebase -r C -o BExample: Rename added file
A well-known and thorny problem in Mercurial occurs in the following scenario:
$ jj log
C rename foo->bar
|
| B modify foo
|/
A add foo
$ jj squash --from C --into AThe 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:
$ jj log
C rename foo->baz
|
| B rename foo->bar
|/
A add foo
$ jj new B CIn 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:
$ 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: fooInterestingly, 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:
$ 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
$ 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 FIf 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 doingjj 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:
- Implement support for copy-tracking in the test backend
- Implement diff algorithm and test it
- Implement merge algorithm and test it
- Implement blame algorithm and test it
- Implement file-following log algorithm and test it
- Extract some queries to the commit backend trait so cloud-based backends
(like the Google backend) can provide versions implemented using database
indexes - 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. - 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 afoo->baz
rename and abar->bazrename. - 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:
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
gitlinkentry in the commit's tree, where the value of thegitlinkentry
is the submodule commit id. This tells Git what to populate in the working
tree.A top level
.gitmodulesfile. This file is in Git's config syntax and
entries take the formsubmodule.<submodule-name>.*. These include many
settings about the submodules, but most importantly:submodule<submodule-name>.pathcontains the path from the root of the tree
to thegitlinkbeing described.submodule<submodule-name>.urlcontains 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 describeto 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 runjj log, causing a snapshot). When you save the
new descriptionjj describecompletes 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 runjj git fetchand
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. runjj 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.
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
|/
PWe will write A⁻ to denote the parent trees of commit A.
Example 1: two commits for change *B*, same parent
$ jj log
B/0
|
| B/1
|/
AIn this simple case it is clear the solution should be a child of A:
$ jj log
B (solution)
|
| B/0 (not visible)
|/
| B/1 (not visible)
|/
ALet'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:
$ jj log
B/0
|
| B/1
|/
| P (not visible)
|/
AHere 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:
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:
$ jj log
B/0
|
| B/1
|/
A
| P (not visible)
|/
XIn 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
$ jj log
B/0
|
| B/1
| /
| C
|/
AIn 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.
$ jj log
B/0
|
| B/1
| /
| C
|/
| P (not visible)
|/
AWe determine the parent(s) of the solution as follows:
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:
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:
$ jj log
B/0
|
| B/1
| |
| C
|/
A
| P (not visible)
|/
XIn this case parents will be
{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:
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'
|/
CAs 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 produceMergedState::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).
$ jj log
B/0
|
| B/1
| |
| | B/2
| |/
|/
AThis 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):
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:
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)
= v3try_resolve_deduplicating_same_diffs returns our desired value "v3". Note that
resolve_trivial (with either SameChange value) would return none. Here is
another example:
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 typeMerge<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:
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 useui.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
returnIGNORE), and that they can runjj config managed --ignore/notify/trustin 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()returningfalse- 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
- E.g., a script that runs jj to do a bisection probably doesn't want
- This is impossible in general due to