diff --git a/prerelease/cli-reference/index.html b/prerelease/cli-reference/index.html index 4d6727cfd2..b76e2f6022 100644 --- a/prerelease/cli-reference/index.html +++ b/prerelease/cli-reference/index.html @@ -6248,7 +6248,7 @@
jj describe
¶
-Update the change description or other metadata
+Update the change description or other metadata [aliases: desc]
Starts an editor to let you edit the description of changes. The editor will be $EDITOR, or pico
if that's not defined (Notepad
on Windows).
Usage: jj describe [OPTIONS] [REVISIONS]...
jj status
¶
-Show high-level repo status
+Show high-level repo status [aliases: st]
This includes:
jj
's documentation website!","text":"The complete list of the available documentation pages is located in the sidebar on the left of the page. The sidebar may be hidden; if so, you can open it either by widening your browser window or by clicking on the hamburger menu that appears in this situation.
Additional help is available using the jj help
command if you have jj
installed.
You may want to jump to:
jj
.jj
. This version of the docs corresponds to the main
branch of the jj
repo.jj
jj
in the repo's READMEjj new/commit
?","text":"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 set
.
jj git push --all
says \"Nothing changed\" instead of pushing it. What do I do?","text":"jj git push --all
pushes all bookmarks, not all revisions. You have two options:
jj git push --change
will automatically create a bookmark and push it.jj bookmark
commands to create or move a bookmark to either the commit you want to push or a descendant on it. Unlike Git, Jujutsu doesn't do this automatically (see previous question).jj log
?","text":"Is your commit visible with jj log -r 'all()'
?
If yes, you should be aware that jj log
only shows the revisions matching revsets.log
by default. You can change it as described in config to show more revisions.
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.
"},{"location":"FAQ/#how-can-i-get-jj-log-to-show-me-what-git-log-would-show-me","title":"How can I getjj log
to show me what git log
would show me?","text":"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).
jj log
evolves?","text":"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=always\n
This will continuously update the (colored) log output in the terminal. The --ignore-working-copy
option avoids conflicts with manual operations during the creation of snapshots. Martin used watch in a tmux pane during his presentation Jujutsu - A Git-compatible VCS.
Alternatively, you can use jj-fzf, where the central piece is the jj log
view and common operations can be carried out via key bindings while the log view updates.
The wiki lists additional TUIs and GUIs beyond the terminal: GUI-and-TUI
"},{"location":"FAQ/#should-i-co-locate-my-repository","title":"Should I co-locate my repository?","text":"Co-locating 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., bisection), or you simply prefer Git in some situations.
Tooling that expects a Git repository still works (IDEs, build tooling, etc.)
The co-location documentation describes the drawbacks but the most important ones are:
Interleaving git
and jj
commands may create confusing bookmark conflicts or divergent changes.
If the working copy commit or its parent contain any conflicted files, tools expecting a Git repo may interpret the commit contents or its diff in a wrong and confusing way. You should avoid doing mutating operations with Git tools and ignore the confusing information such tools present for conflicted commits (unless you are curious about the details of how jj
stores conflicts). See #3979 for plans to improve this situation.
Jujutsu commands may be a little slower in very large repositories due to importing and exporting changes to Git. Most repositories are not noticeably affected by this.
If you primarily use Jujutsu to modify the repository, the drawbacks are unlikely to affect you. Try co-locating while you learn Jujutsu, then switch if you find a specific reason not to co-locate.
"},{"location":"FAQ/#jj-is-said-to-record-the-working-copy-after-jj-log-and-every-other-command-where-can-i-see-these-automatic-saves","title":"jj
is said to record the working copy after jj log
and every other command. Where can I see these automatic \"saves\"?","text":"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.
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.
"},{"location":"FAQ/#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","title":"Can I interactively create a new commit from only some of the changes in the working copy, likegit add -p && git commit
or hg commit -i
?","text":"Since the changes are already in the working-copy commit, the equivalent to git add -p && git commit
/git commit -p
/hg commit -i
is to split the working-copy commit with jj split -i
(or the practically identical jj commit -i
).
For the equivalent of git commit --amend -p
/hg amend -i
, use jj squash -i
.
git rebase --interactive
or hg histedit
?","text":"Not yet, you can check this issue for updates.
To reorder commits, it is for now recommended to rebase commits individually, which may require multiple invocations of jj rebase -r
or jj rebase -s
.
To squash or split commits, use jj squash
and jj split
.
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.
You can keep your notes and other scratch files in the repository, if you add a wildcard pattern to either the repo's gitignore
or your global gitignore
. Something like *.scratch
or *.scratchpad
should do, after that rename the files you want to keep around to match the pattern.
If you keep your scratch files in their own directory with no tracked files, you can create a .gitignore
file in that directory containing only *
. This will ignore everything in the directory including the .gitignore
file itself.
If $EDITOR
integration is important, something like scratchpad.*
may be more helpful, as you can keep the filename extension intact (it matches scratchpad.md
, scratchpad.rs
and more). Another option is to add a directory to the global .gitignore
which then stores all your temporary files and notes. For example, you could add scratch/
to ~/.git/ignore
and then store arbitrary files in <your-git-repo>/scratch/
.
You can find more details on gitignore
files here.
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\n@ xxxxxxxx me@example.com 2024-08-21 11:13:21 ef612875\n\u2502 Add new feature\n\u25c9 yyyyyyyy me@example.com 2024-08-21 11:13:09 main b624cf12\n\u2502 Existing work\n~\n
First, create a new commit branched from main and add your private changes:
$ jj new main -m \"private: my credentials\"\nWorking copy now at: wwwwwwww 861de9eb (empty) private: my credentials\nParent commit : yyyyyyyy b624cf12 main | Existing work\nAdded 0 files, modified 1 files, removed 0 files\n\n$ echo '{ \"password\": \"p@ssw0rd1\" }' > secret_config.json\n
Now create a merge commit with the branch you're working on and the private commit:
$ jj new xxxxxxxx wwwwwwww\nWorking copy now at: vvvvvvvv ac4d9fbe (empty) (no description set)\nParent commit : xxxxxxxx ef612875 Add new feature\nParent commit : wwwwwwww 2106921e private: my credentials\nAdded 0 files, modified 1 files, removed 0 files\n\n$ jj log\n@ vvvvvvvv me@example.com 2024-08-22 08:57:40 ac4d9fbe\n\u251c\u2500\u256e (empty) (no description set)\n\u2502 \u25c9 wwwwwwww me@example.com 2024-08-22 08:57:40 2106921e\n\u2502 \u2502 private: my credentials\n\u25c9 \u2502 xxxxxxxx me@example.com 2024-08-21 11:13:21 ef612875\n\u251c\u2500\u256f Add new feature\n\u25c9 yyyyyyyy me@example.com 2024-08-21 11:13:09 main b624cf12\n\u2502 Existing work\n~\n
Now you're ready to work:
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\n$ jj new --no-edit -A xxxxxxxx -m \"Another feature\"\nWorking copy now at: uuuuuuuu 1c3cff09 (empty) Another feature\nParent commit : xxxxxxxx ef612875 Add new feature\n\n# Insert a new commit between yyyyyyyy and vvvvvvvv\n$ jj new --no-edit -A yyyyyyyy -B vvvvvvvv -m \"Yet another feature\"\nWorking copy now at: tttttttt 938ab831 (empty) Yet another feature\nParent commit : yyyyyyyy b624cf12 Existing work\n
To avoid pushing change wwwwwwww by mistake, use the configuration git.private-commits:
$ jj config set --user git.private-commits 'description(glob:\"private:*\")'\n
"},{"location":"FAQ/#i-accidentally-changed-files-in-the-wrong-commit-how-do-i-move-the-recent-changes-into-another-commit","title":"I accidentally changed files in the wrong commit, how do I move the recent changes into another commit?","text":"Use jj evolog -p
to see how your working-copy commit has evolved. Find the commit you want to restore the contents to. Let's say the current commit (with the changes intended for a new commit) are in commit X and the state you wanted is in commit Y. Note the commit id (normally in blue at the end of the line in the log output) of each of them. Now use jj new
to create a new working-copy commit, then run jj restore --from Y --to @-
to restore the parent commit to the old state, and jj restore --from X
to restore the new working-copy commit to the new state.
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 realising.
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 behaviour. Other workflows may prefer jj edit
as well.
A divergent change represents a change that has two or more visible commits associated with it. To refer to such commits, you must use their commit ID. Most commonly, the way to resolve this is to abandon the unneeded commits (using jj abandon <commit ID>
). If you would like to keep both commits with this change ID, you can jj duplicate
one of them before abandoning it.
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 set <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 list
to show all the commits associated with it.
At the moment you'll need a script, which adds the required fields for Gerrit like the Change-Id
footer. Then jj
can invoke it via an $EDITOR
override in an aliased command. Here's an example from an contributor (look for the jj signoff
alias).
After you have attached the Change-Id:
footer to the commit series, you'll have to manually invoke git push
of HEAD
on the underlying git repository into the remote Gerrit bookmark refs/for/$BRANCH
, where $BRANCH
is the base bookmark you want your changes to go to (e.g., git push origin HEAD:refs/for/main
). Using a co-located repo will make the underlying git repo directly accessible from the working directory.
We hope to integrate with Gerrit natively in the future.
"},{"location":"FAQ/#i-want-to-write-a-tool-which-integrates-with-jujutsu-should-i-use-the-library-or-parse-the-cli","title":"I want to write a tool which integrates with Jujutsu. Should I use the library or parse the CLI?","text":"There are some trade-offs and there is no definitive answer yet.
jj-lib
avoids parsing command output and makes error handling easier.jj-lib
is not a stable API, so you may have to make changes to your tool when the API changes.jj
binaries, like the one at Google (if you're using the library, you will not be able to detect custom backends and more).Bookmarks are named pointers to revisions (just like branches are in Git). You can move them without affecting the target revision's identity. Bookmarks automatically move when revisions are rewritten (e.g. by jj rebase
). You can pass a bookmark's name to commands that want a revision as argument. For example, jj new main
will create a new revision on top of the main
bookmark. Use jj bookmark list
to list bookmarks and jj bookmark <subcommand>
to create, move, or delete bookmarks. There is currently no concept of an active/current/checked-out bookmark.
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 repos).
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 <bookmark name>@<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.
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 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.
"},{"location":"bookmarks/#terminology-summary","title":"Terminology summary","text":"jj
can find out its actual state only when it's actively communicating with the remote. However, jj
does store the last-seen position of the remote bookmark; this is the commit jj show <bookmark name>@<remote name>
would show. This notion is completely analogous to Git's \"remote-tracking bookmarks\".jj bookmark track
command, for example.jj
tries to keep in sync with the tracked remote bookmark. For example, after jj bookmark track mybookmark@origin
, there will be a local bookmark mybookmark
that's tracking the remote mybookmark@origin
bookmark. A local bookmark can track a bookmark of the same name on 0 or more remotes.The notion of tracked bookmarks serves a similar function to the Git notion of an \"upstream branch\". Unlike Git, a single local bookmark can be tracking remote bookmarks on multiple remotes, and the names of the local and remote bookmarks must match.
"},{"location":"bookmarks/#manually-tracking-a-bookmark","title":"Manually tracking a bookmark","text":"To track a bookmark permanently use jj bookmark track <bookmark name>@<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.\n$ jj bookmark list --all\n$ # Find the bookmark.\n$ # [...]\n$ # Actually track the bookmark.\n$ jj bookmark track <bookmark name>@<remote name> # Example: jj bookmark track my-feature@origin\n$ # From this point on, <bookmark name> will be imported when fetching from <remote name>.\n$ jj git fetch --remote <remote name>\n$ # A local bookmark <bookmark name> should have been created or updated while fetching.\n$ jj new <bookmark name> # Do some local testing, etc.\n
"},{"location":"bookmarks/#untracking-a-bookmark","title":"Untracking a bookmark","text":"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.\n$ jj bookmark list --all\n$ # Find the bookmark we no longer want to track.\n$ # [...]\n# # Actually untrack it.\n$ jj bookmark untrack <bookmark name>@<remote name> # Example: jj bookmark untrack stuff@origin\n$ # From this point on, this remote bookmark won't be imported anymore.\n$ # The local bookmark (e.g. stuff) is unaffected. It may or may not still\n$ # be tracking bookmarks on other remotes (e.g. stuff@upstream).\n
"},{"location":"bookmarks/#listing-tracked-bookmarks","title":"Listing tracked bookmarks","text":"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 <bookmark name>
.
git.auto-local-bookmark
option","text":"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 git.auto-local-bookmark = true
. 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 bookmarks) by default.
Currently Jujutsu automatically moves local bookmarks when these conditions are met:
You could describe the movement as following along the change-id of the current bookmark commit, even if it isn't entirely accurate.
"},{"location":"bookmarks/#pushing-bookmarks-safety-checks","title":"Pushing bookmarks: Safety checks","text":"Before jj git push
actually moves, creates, or deletes a remote bookmark, it makes several safety checks.
jj
will contact the remote and check that the actual state of the remote bookmark matches jj
's record of its last known position. If there is a conflict, jj
will refuse to push the bookmark. In this case, you need to run jj git fetch --remote <remote name>
and resolve the resulting bookmark conflict. Then, you can try jj git push
again.
If you are familiar with Git, this makes jj git push
similar to git push --force-with-lease
.
There are a few cases where jj git push
will succeed even though the remote bookmark is in an unexpected location. These are the cases where jj git fetch
would not create a bookmark conflict and would not move the local bookmark, e.g. if the unexpected location is identical to the local position of the bookmark.
The local bookmark must not be conflicted. If it is, you would need to use jj bookmark set
, for example, to resolve the conflict.
This makes jj git push
safe even if jj git fetch
is performed on a timer in the background (this situation is a known issue1 with some forms of git push --force-with-lease
). If the bookmark moves on a remote in a problematic way, jj git fetch
will create a conflict. This should ensure that the user becomes aware of the conflict before they can jj git push
and override the bookmark on the remote.
If the remote bookmark already exists on the remote, it must be tracked. If the bookmark does not already exist on the remote, there is no problem; jj git push
will create the remote bookmark and mark it as tracked.
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 'all: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).
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
can be abbreviated as jj b c BOOKMARK-NAME
.
See \"A general note on safety\" in https://git-scm.com/docs/git-push#Documentation/git-push.txt---no-force-with-lease \u21a9
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.
"},{"location":"cli-reference/#command-line-help-for-jj","title":"Command-Line Help forjj
","text":"This document contains the help content for the jj
command-line program.
Command Overview:
jj
\u21b4jj abandon
\u21b4jj backout
\u21b4jj bookmark
\u21b4jj bookmark create
\u21b4jj bookmark delete
\u21b4jj bookmark forget
\u21b4jj bookmark list
\u21b4jj bookmark move
\u21b4jj bookmark rename
\u21b4jj bookmark set
\u21b4jj bookmark track
\u21b4jj bookmark untrack
\u21b4jj commit
\u21b4jj config
\u21b4jj config edit
\u21b4jj config get
\u21b4jj config list
\u21b4jj config path
\u21b4jj config set
\u21b4jj config unset
\u21b4jj describe
\u21b4jj diff
\u21b4jj diffedit
\u21b4jj duplicate
\u21b4jj edit
\u21b4jj evolog
\u21b4jj file
\u21b4jj file annotate
\u21b4jj file chmod
\u21b4jj file list
\u21b4jj file show
\u21b4jj file track
\u21b4jj file untrack
\u21b4jj fix
\u21b4jj git
\u21b4jj git clone
\u21b4jj git export
\u21b4jj git fetch
\u21b4jj git import
\u21b4jj git init
\u21b4jj git push
\u21b4jj git remote
\u21b4jj git remote add
\u21b4jj git remote list
\u21b4jj git remote remove
\u21b4jj git remote rename
\u21b4jj git remote set-url
\u21b4jj help
\u21b4jj init
\u21b4jj interdiff
\u21b4jj log
\u21b4jj new
\u21b4jj next
\u21b4jj operation
\u21b4jj operation abandon
\u21b4jj operation diff
\u21b4jj operation log
\u21b4jj operation restore
\u21b4jj operation show
\u21b4jj operation undo
\u21b4jj parallelize
\u21b4jj prev
\u21b4jj rebase
\u21b4jj resolve
\u21b4jj restore
\u21b4jj root
\u21b4jj show
\u21b4jj simplify-parents
\u21b4jj sparse
\u21b4jj sparse edit
\u21b4jj sparse list
\u21b4jj sparse reset
\u21b4jj sparse set
\u21b4jj split
\u21b4jj squash
\u21b4jj status
\u21b4jj tag
\u21b4jj tag list
\u21b4jj util
\u21b4jj util completion
\u21b4jj util config-schema
\u21b4jj util gc
\u21b4jj util mangen
\u21b4jj util markdown-help
\u21b4jj undo
\u21b4jj version
\u21b4jj workspace
\u21b4jj workspace add
\u21b4jj workspace forget
\u21b4jj workspace list
\u21b4jj workspace rename
\u21b4jj workspace root
\u21b4jj workspace update-stale
\u21b4jj
","text":"Jujutsu (An experimental VCS)
To get started, see the tutorial at https://martinvonz.github.io/jj/latest/tutorial/.
Usage: jj [OPTIONS] [COMMAND]
'jj help --help' list available keywords. Use 'jj help -k' to show help for one of these keywords.
"},{"location":"cli-reference/#subcommands","title":"Subcommands:","text":"abandon
\u2014 Abandon a revisionbackout
\u2014 Apply the reverse of a revision on top of another revisionbookmark
\u2014 Manage bookmarkscommit
\u2014 Update the description and create a new change on topconfig
\u2014 Manage config optionsdescribe
\u2014 Update the change description or other metadatadiff
\u2014 Compare file contents between two revisionsdiffedit
\u2014 Touch up the content changes in a revision with a diff editorduplicate
\u2014 Create a new change with the same content as an existing oneedit
\u2014 Sets the specified revision as the working-copy revisionevolog
\u2014 Show how a change has evolved over timefile
\u2014 File operationsfix
\u2014 Update files with formatting fixes or other changesgit
\u2014 Commands for working with Git remotes and the underlying Git repohelp
\u2014 Print this message or the help of the given subcommand(s)init
\u2014 Create a new repo in the given directoryinterdiff
\u2014 Compare the changes of two commitslog
\u2014 Show revision historynew
\u2014 Create a new, empty change and (by default) edit it in the working copynext
\u2014 Move the working-copy commit to the child revisionoperation
\u2014 Commands for working with the operation logparallelize
\u2014 Parallelize revisions by making them siblingsprev
\u2014 Change the working copy revision relative to the parent revisionrebase
\u2014 Move revisions to different parent(s)resolve
\u2014 Resolve a conflicted file with an external merge toolrestore
\u2014 Restore paths from another revisionroot
\u2014 Show the current workspace root directoryshow
\u2014 Show commit description and changes in a revisionsimplify-parents
\u2014 Simplify parent edges for the specified revision(s)sparse
\u2014 Manage which paths from the working-copy commit are present in the working copysplit
\u2014 Split a revision in twosquash
\u2014 Move changes from a revision into another revisionstatus
\u2014 Show high-level repo statustag
\u2014 Manage tagsutil
\u2014 Infrequently used commands such as for generating shell completionsundo
\u2014 Undo an operation (shortcut for jj op undo
)version
\u2014 Display version informationworkspace
\u2014 Commands for working with workspaces-R
, --repository <REPOSITORY>
\u2014 Path to repository to operate on
By default, Jujutsu searches for the closest .jj/ directory in an ancestor of the current working directory.
--ignore-working-copy
\u2014 Don't snapshot the working copy, and don't update it
By default, Jujutsu snapshots the working copy at the beginning of every command. The working copy is also updated at the end of the command, if the command modified the working-copy commit (@
). If you want to avoid snapshotting the working copy and instead see a possibly stale working copy commit, you can use --ignore-working-copy
. This may be useful e.g. in a command prompt, especially if you have another process that commits the working copy.
Loading the repository at a specific operation with --at-operation
implies --ignore-working-copy
.
--ignore-immutable
\u2014 Allow rewriting immutable commits
By default, Jujutsu prevents rewriting commits in the configured set of immutable commits. This option disables that check and lets you rewrite any commit but the root commit.
This option only affects the check. It does not affect the immutable_heads()
revset or the immutable
template keyword.
--at-operation <AT_OPERATION>
\u2014 Operation to load the repo at
Operation to load the repo at. By default, Jujutsu loads the repo at the most recent operation, or at the merge of the divergent operations if any.
You can use --at-op=<operation ID>
to see what the repo looked like at an earlier operation. For example jj --at-op=<operation ID> st
will show you what jj st
would have shown you when the given operation had just finished. --at-op=@
is pretty much the same as the default except that divergent operations will never be merged.
Use jj op log
to find the operation ID you want. Any unambiguous prefix of the operation ID is enough.
When loading the repo at an earlier operation, the working copy will be ignored, as if --ignore-working-copy
had been specified.
It is possible to run mutating commands when loading the repo at an earlier operation. Doing that is equivalent to having run concurrent commands starting at the earlier operation. There's rarely a reason to do that, but it is possible.
--debug
\u2014 Enable debug logging
--color <WHEN>
\u2014 When to colorize output (always, never, debug, auto)--quiet
\u2014 Silence non-primary command output
For example, jj file list
will still list files, but it won't tell you if the working copy was snapshotted or if descendants were rebased.
Warnings and errors will still be printed.
--no-pager
\u2014 Disable the pager
--config-toml <TOML>
\u2014 Additional configuration options (can be repeated)jj abandon
","text":"Abandon a revision
Abandon a revision, rebasing descendants onto its parent(s). The behavior is similar to jj restore --changes-in
; the difference is that jj abandon
gives you a new change, while jj restore
updates the existing change.
If a working-copy commit gets abandoned, it will be given a new, empty commit. This is true in general; it is not specific to this command.
Usage: jj abandon [OPTIONS] [REVISIONS]...
<REVISIONS>
\u2014 The revision(s) to abandon
Default value: @
-s
, --summary
\u2014 Do not print every abandoned commit on a separate line--restore-descendants
\u2014 Do not modify the content of the children of the abandoned commitsjj backout
","text":"Apply the reverse of a revision on top of another revision
Usage: jj backout [OPTIONS]
-r
, --revisions <REVISIONS>
\u2014 The revision(s) to apply the reverse of
Default value: @
-d
, --destination <DESTINATION>
\u2014 The revision to apply the reverse changes on top of
Default value: @
jj bookmark
","text":"Manage bookmarks
For information about bookmarks, see https://martinvonz.github.io/jj/latest/bookmarks.
Usage: jj bookmark <COMMAND>
create
\u2014 Create a new bookmarkdelete
\u2014 Delete an existing bookmark and propagate the deletion to remotes on the next pushforget
\u2014 Forget everything about a bookmark, including its local and remote targetslist
\u2014 List bookmarks and their targetsmove
\u2014 Move existing bookmarks to target revisionrename
\u2014 Rename old
bookmark name to new
bookmark nameset
\u2014 Create or update a bookmark to point to a certain committrack
\u2014 Start tracking given remote bookmarksuntrack
\u2014 Stop tracking given remote bookmarksjj bookmark create
","text":"Create a new bookmark
Usage: jj bookmark create [OPTIONS] <NAMES>...
<NAMES>
\u2014 The bookmarks to create-r
, --revision <REVISION>
\u2014 The bookmark's target revisionjj bookmark delete
","text":"Delete an existing bookmark and propagate the deletion to remotes on the next push
Usage: jj bookmark delete <NAMES>...
<NAMES>
\u2014 The bookmarks to delete
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
jj bookmark forget
","text":"Forget everything about a bookmark, including its local and remote targets
A forgotten bookmark will not impact remotes on future pushes. It will be recreated on future pulls if it still exists in the remote.
Usage: jj bookmark forget <NAMES>...
<NAMES>
\u2014 The bookmarks to forget
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
jj bookmark list
","text":"List bookmarks and their targets
By default, a tracking remote bookmark will be included only if its target is different from the local target. A non-tracking remote bookmark won't be listed. For a conflicted bookmark (both local and remote), old target revisions are preceded by a \"-\" and new target revisions are preceded by a \"+\".
For information about bookmarks, see https://martinvonz.github.io/jj/latest/bookmarks/.
Usage: jj bookmark list [OPTIONS] [NAMES]...
<NAMES>
\u2014 Show bookmarks whose local name matches
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
-a
, --all-remotes
\u2014 Show all tracking and non-tracking remote bookmarks including the ones whose targets are synchronized with the local bookmarks--remote <REMOTE>
\u2014 Show all tracking and non-tracking remote bookmarks belonging to this remote
Can be combined with --tracked
or --conflicted
to filter the bookmarks shown (can be repeated.)
By default, the specified remote name matches exactly. Use glob:
prefix to select remotes by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
-t
, --tracked
\u2014 Show remote tracked bookmarks only. Omits local Git-tracking bookmarks by default
-c
, --conflicted
\u2014 Show conflicted bookmarks only-r
, --revisions <REVISIONS>
\u2014 Show bookmarks whose local targets are in the given revisions
Note that -r deleted_bookmark
will not work since deleted_bookmark
wouldn't have a local target.
-T
, --template <TEMPLATE>
\u2014 Render each bookmark using the given template
All 0-argument methods of the RefName
type are available as keywords.
For the syntax, see https://martinvonz.github.io/jj/latest/templates/
jj bookmark move
","text":"Move existing bookmarks to target revision
If bookmark names are given, the specified bookmarks will be updated to point to the target revision.
If --from
options are given, bookmarks currently pointing to the specified revisions will be updated. The bookmarks can also be filtered by names.
Example: pull up the nearest bookmarks to the working-copy parent
$ jj bookmark move --from 'heads(::@- & bookmarks())' --to @-
Usage: jj bookmark move [OPTIONS] <--from <REVISIONS>|NAMES>
<NAMES>
\u2014 Move bookmarks matching the given name patterns
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
--from <REVISIONS>
\u2014 Move bookmarks from the given revisions--to <REVISION>
\u2014 Move bookmarks to this revision
Default value: @
-B
, --allow-backwards
\u2014 Allow moving bookmarks backwards or sideways
jj bookmark rename
","text":"Rename old
bookmark name to new
bookmark name
The new bookmark name points at the same commit as the old bookmark name.
Usage: jj bookmark rename <OLD> <NEW>
<OLD>
\u2014 The old name of the bookmark<NEW>
\u2014 The new name of the bookmarkjj bookmark set
","text":"Create or update a bookmark to point to a certain commit
Usage: jj bookmark set [OPTIONS] <NAMES>...
<NAMES>
\u2014 The bookmarks to update-r
, --revision <REVISION>
\u2014 The bookmark's target revision-B
, --allow-backwards
\u2014 Allow moving the bookmark backwards or sidewaysjj bookmark track
","text":"Start tracking given remote bookmarks
A tracking remote bookmark will be imported as a local bookmark of the same name. Changes to it will propagate to the existing local bookmark on future pulls.
Usage: jj bookmark track <BOOKMARK@REMOTE>...
<BOOKMARK@REMOTE>
\u2014 Remote bookmarks to track
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
Examples: bookmark@remote, glob:main@, glob:jjfan-@upstream
jj bookmark untrack
","text":"Stop tracking given remote bookmarks
A non-tracking remote bookmark is just a pointer to the last-fetched remote bookmark. It won't be imported as a local bookmark on future pulls.
Usage: jj bookmark untrack <BOOKMARK@REMOTE>...
<BOOKMARK@REMOTE>
\u2014 Remote bookmarks to untrack
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
Examples: bookmark@remote, glob:main@, glob:jjfan-@upstream
jj commit
","text":"Update the description and create a new change on top
Usage: jj commit [OPTIONS] [PATHS]...
<PATHS>
\u2014 Put these paths in the first commit-i
, --interactive
\u2014 Interactively choose which changes to include in the first commit--tool <NAME>
\u2014 Specify diff editor to be used (implies --interactive)-m
, --message <MESSAGE>
\u2014 The change description to use (don't open editor)--reset-author
\u2014 Reset the author to the configured user
This resets the author name, email, and timestamp.
You can use it in combination with the JJ_USER and JJ_EMAIL environment variables to set a different author:
$ JJ_USER='Foo Bar' JJ_EMAIL=foo@bar.com jj commit --reset-author
--author <AUTHOR>
\u2014 Set author to the provided string
This changes author name and email while retaining author timestamp for non-discardable commits.
jj config
","text":"Manage config options
Operates on jj configuration, which comes from the config file and environment variables.
For file locations, supported config options, and other details about jj config, see https://martinvonz.github.io/jj/latest/config/.
Usage: jj config <COMMAND>
edit
\u2014 Start an editor on a jj config fileget
\u2014 Get the value of a given config option.list
\u2014 List variables set in config file, along with their valuespath
\u2014 Print the path to the config fileset
\u2014 Update config file to set the given option to a given valueunset
\u2014 Update config file to unset the given optionjj config edit
","text":"Start an editor on a jj config file.
Creates the file if it doesn't already exist regardless of what the editor does.
Usage: jj config edit <--user|--repo>
--user
\u2014 Target the user-level config--repo
\u2014 Target the repo-level configjj config get
","text":"Get the value of a given config option.
Unlike jj config list
, the result of jj config get
is printed without extra formatting and therefore is usable in scripting. For example:
$ jj config list user.name user.name=\"Martin von Zweigbergk\" $ jj config get user.name Martin von Zweigbergk
Usage: jj config get <NAME>
<NAME>
jj config list
","text":"List variables set in config file, along with their values
Usage: jj config list [OPTIONS] [NAME]
<NAME>
\u2014 An optional name of a specific config option to look up--include-defaults
\u2014 Whether to explicitly include built-in default values in the list--include-overridden
\u2014 Allow printing overridden values--user
\u2014 Target the user-level config--repo
\u2014 Target the repo-level config-T
, --template <TEMPLATE>
\u2014 Render each variable using the given template
The following keywords are defined:
name: String
: Config name.value: String
: Serialized value in TOML syntax.overridden: Boolean
: True if the value is shadowed by other.For the syntax, see https://martinvonz.github.io/jj/latest/templates/
jj config path
","text":"Print the path to the config file
A config file at that path may or may not exist.
See jj config edit
if you'd like to immediately edit the file.
Usage: jj config path <--user|--repo>
--user
\u2014 Target the user-level config--repo
\u2014 Target the repo-level configjj config set
","text":"Update config file to set the given option to a given value
Usage: jj config set <--user|--repo> <NAME> <VALUE>
<NAME>
<VALUE>
--user
\u2014 Target the user-level config--repo
\u2014 Target the repo-level configjj config unset
","text":"Update config file to unset the given option
Usage: jj config unset <--user|--repo> <NAME>
<NAME>
--user
\u2014 Target the user-level config--repo
\u2014 Target the repo-level configjj describe
","text":"Update the change description or other metadata
Starts an editor to let you edit the description of changes. The editor will be $EDITOR, or pico
if that's not defined (Notepad
on Windows).
Usage: jj describe [OPTIONS] [REVISIONS]...
<REVISIONS>
\u2014 The revision(s) whose description to edit
Default value: @
-m
, --message <MESSAGE>
\u2014 The change description to use (don't open editor)
If multiple revisions are specified, the same description will be used for all of them.
--stdin
\u2014 Read the change description from stdin
If multiple revisions are specified, the same description will be used for all of them.
--no-edit
\u2014 Don't open an editor
This is mainly useful in combination with e.g. --reset-author
.
--reset-author
\u2014 Reset the author to the configured user
This resets the author name, email, and timestamp.
You can use it in combination with the JJ_USER and JJ_EMAIL environment variables to set a different author:
$ JJ_USER='Foo Bar' JJ_EMAIL=foo@bar.com jj describe --reset-author
--author <AUTHOR>
\u2014 Set author to the provided string
This changes author name and email while retaining author timestamp for non-discardable commits.
jj diff
","text":"Compare file contents between two revisions
With the -r
option, which is the default, shows the changes compared to the parent revision. If there are several parent revisions (i.e., the given revision is a merge), then they will be merged and the changes from the result to the given revision will be shown.
With the --from
and/or --to
options, shows the difference from/to the given revisions. If either is left out, it defaults to the working-copy commit. For example, jj diff --from main
shows the changes from \"main\" (perhaps a bookmark name) to the working-copy commit.
Usage: jj diff [OPTIONS] [PATHS]...
<PATHS>
\u2014 Restrict the diff to these paths-r
, --revision <REVISION>
\u2014 Show changes in this revision, compared to its parent(s)
If the revision is a merge commit, this shows changes from the automatic merge of the contents of all of its parents to the contents of the revision itself.
--from <FROM>
\u2014 Show changes from this revision
--to <TO>
\u2014 Show changes to this revision-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show-w
, --ignore-all-space
\u2014 Ignore whitespace when comparing lines-b
, --ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj diffedit
","text":"Touch up the content changes in a revision with a diff editor
With the -r
option, which is the default, starts a diff editor on the changes in the revision.
With the --from
and/or --to
options, starts a diff editor comparing the \"from\" revision to the \"to\" revision.
Edit the right side of the diff until it looks the way you want. Once you close the editor, the revision specified with -r
or --to
will be updated. Unless --restore-descendants
is used, descendants will be rebased on top as usual, which may result in conflicts.
See jj restore
if you want to move entire files from one revision to another. For moving changes between revisions, see jj squash -i
.
Usage: jj diffedit [OPTIONS]
-r
, --revision <REVISION>
\u2014 The revision to touch up
Defaults to @ if neither --to nor --from are specified.
--from <FROM>
\u2014 Show changes from this revision
Defaults to @ if --to is specified.
--to <TO>
\u2014 Edit changes in this revision
Defaults to @ if --from is specified.
--tool <NAME>
\u2014 Specify diff editor to be used
--restore-descendants
\u2014 Preserve the content (not the diff) when rebasing descendants
When rebasing a descendant on top of the rewritten revision, its diff compared to its parent(s) is normally preserved, i.e. the same way that descendants are always rebased. This flag makes it so the content/state is preserved instead of preserving the diff.
jj duplicate
","text":"Create a new change with the same content as an existing one
Usage: jj duplicate [REVISIONS]...
<REVISIONS>
\u2014 The revision(s) to duplicate
Default value: @
jj edit
","text":"Sets the specified revision as the working-copy revision
Note: it is generally recommended to instead use jj new
and jj squash
.
For more information, see https://martinvonz.github.io/jj/latest/FAQ#how-do-i-resume-working-on-an-existing-change
Usage: jj edit <REVISION>
<REVISION>
\u2014 The commit to editjj evolog
","text":"Show how a change has evolved over time
Lists the previous commits which a change has pointed to. The current commit of a change evolves when the change is updated, rebased, etc.
Usage: jj evolog [OPTIONS]
-r
, --revision <REVISION>
Default value: @
-n
, --limit <LIMIT>
\u2014 Limit number of revisions to show
--no-graph
\u2014 Don't show the graph, show a flat list of revisions-T
, --template <TEMPLATE>
\u2014 Render each revision using the given template
For the syntax, see https://martinvonz.github.io/jj/latest/templates/
-p
, --patch
\u2014 Show patch compared to the previous version of this change
If the previous version has different parents, it will be temporarily rebased to the parents of the new version, so the diff is not contaminated by unrelated changes.
-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted
--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show--ignore-all-space
\u2014 Ignore whitespace when comparing lines--ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj file
","text":"File operations
Usage: jj file <COMMAND>
annotate
\u2014 Show the source change for each line of the target filechmod
\u2014 Sets or removes the executable bit for paths in the repolist
\u2014 List files in a revisionshow
\u2014 Print contents of files in a revisiontrack
\u2014 Start tracking specified paths in the working copyuntrack
\u2014 Stop tracking specified paths in the working copyjj file annotate
","text":"Show the source change for each line of the target file.
Annotates a revision line by line. Each line includes the source change that introduced the associated line. A path to the desired file must be provided. The per-line prefix for each line can be customized via template with the templates.annotate_commit_summary
config variable.
Usage: jj file annotate [OPTIONS] <PATH>
<PATH>
\u2014 the file to annotate-r
, --revision <REVISION>
\u2014 an optional revision to start atjj file chmod
","text":"Sets or removes the executable bit for paths in the repo
Unlike the POSIX chmod
, jj file chmod
also works on Windows, on conflicted files, and on arbitrary revisions.
Usage: jj file chmod [OPTIONS] <MODE> <PATHS>...
<MODE>
Possible values:
n
: Make a path non-executable (alias: normal)x
: Make a path executable (alias: executable)<PATHS>
\u2014 Paths to change the executable bit for
-r
, --revision <REVISION>
\u2014 The revision to update
Default value: @
jj file list
","text":"List files in a revision
Usage: jj file list [OPTIONS] [PATHS]...
<PATHS>
\u2014 Only list files matching these prefixes (instead of all files)-r
, --revision <REVISION>
\u2014 The revision to list files in
Default value: @
jj file show
","text":"Print contents of files in a revision
If the given path is a directory, files in the directory will be visited recursively.
Usage: jj file show [OPTIONS] <PATHS>...
<PATHS>
\u2014 Paths to print-r
, --revision <REVISION>
\u2014 The revision to get the file contents from
Default value: @
jj file track
","text":"Start tracking specified paths in the working copy
Without arguments, all paths that are not ignored will be tracked.
New files in the working copy can be automatically tracked. You can configure which paths to automatically track by setting snapshot.auto-track
(e.g. to \"none()\"
or \"glob:**/*.rs\"
). Files that don't match the pattern can be manually tracked using this command. The default pattern is all()
and this command has no effect.
Usage: jj file track <PATHS>...
<PATHS>
\u2014 Paths to trackjj file untrack
","text":"Stop tracking specified paths in the working copy
Usage: jj file untrack <PATHS>...
<PATHS>
\u2014 Paths to untrack. They must already be ignored.
The paths could be ignored via a .gitignore or .git/info/exclude (in colocated repos).
jj fix
","text":"Update files with formatting fixes or other changes
The primary use case for this command is to apply the results of automatic code formatting tools to revisions that may not be properly formatted yet. It can also be used to modify files with other tools like sed
or sort
.
The changed files in the given revisions will be updated with any fixes determined by passing their file content through any external tools the user has configured for those files. Descendants will also be updated by passing their versions of the same files through the same tools, which will ensure that the fixes are not lost. This will never result in new conflicts. Files with existing conflicts will be updated on all sides of the conflict, which can potentially increase or decrease the number of conflict markers.
The external tools must accept the current file content on standard input, and return the updated file content on standard output. A tool's output will not be used unless it exits with a successful exit code. Output on standard error will be passed through to the terminal.
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 the substring $path
, which will be replaced with the repo-relative path of the file being fixed. It is useful to provide the path to tools that include the path in error messages, or behave differently based on the directory or file name.patterns
: Determines which files the tool will affect. 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.For example, the following configuration defines how two code formatters (clang-format
and black
) will apply to three different file extensions (.cc
, .h
, and .py
):
[fix.tools.clang-format]\ncommand = [\"/usr/bin/clang-format\", \"--assume-filename=$path\"]\npatterns = [\"glob:'**/*.cc'\",\n \"glob:'**/*.h'\"]\n\n[fix.tools.black]\ncommand = [\"/usr/bin/black\", \"-\", \"--stdin-filename=$path\"]\npatterns = [\"glob:'**/*.py'\"]\n
Execution order of tools that affect the same file is deterministic, but currently unspecified, and may change between releases. If two tools affect the same file, the second tool to run will receive its input from the output of the first tool.
There is also a deprecated configuration schema that defines a single command that will affect all changed files in the specified revisions. For example, the following configuration would apply the Rust formatter to all changed files (whether they are Rust files or not):
[fix]\ntool-command = [\"rustfmt\", \"--emit\", \"stdout\"]\n
The tool defined by tool-command
acts as if it was the first entry in fix.tools
, and uses pattern = \"all()\"``. Support for
tool-command` will be removed in a future version.
Usage: jj fix [OPTIONS] [PATHS]...
<PATHS>
\u2014 Fix only these paths-s
, --source <SOURCE>
\u2014 Fix files in the specified revision(s) and their descendants. If no revisions are specified, this defaults to the revsets.fix
setting, or reachable(@, mutable())
if it is not set--include-unchanged-files
\u2014 Fix unchanged files in addition to changed ones. If no paths are specified, all files in the repo will be fixedjj git
","text":"Commands for working with Git remotes and the underlying Git repo
For a comparison with Git, including a table of commands, see https://martinvonz.github.io/jj/latest/git-comparison/.
Usage: jj git <COMMAND>
clone
\u2014 Create a new repo backed by a clone of a Git repoexport
\u2014 Update the underlying Git repo with changes made in the repofetch
\u2014 Fetch from a Git remoteimport
\u2014 Update repo with changes made in the underlying Git repoinit
\u2014 Create a new Git backed repopush
\u2014 Push to a Git remoteremote
\u2014 Manage Git remotesjj git clone
","text":"Create a new repo backed by a clone of a Git repo
The Git repo will be a bare git repo stored inside the .jj/
directory.
Usage: jj git clone [OPTIONS] <SOURCE> [DESTINATION]
<SOURCE>
\u2014 URL or path of the Git repo to clone<DESTINATION>
\u2014 Specifies the target directory for the Jujutsu repository clone. If not provided, defaults to a directory named after the last component of the source URL. The full directory path will be created if it doesn't exist--remote <REMOTE_NAME>
\u2014 Name of the newly created remote
Default value: origin
--colocate
\u2014 Whether or not to colocate the Jujutsu repo with the git repo
--depth <DEPTH>
\u2014 Create a shallow clone of the given depthjj git export
","text":"Update the underlying Git repo with changes made in the repo
Usage: jj git export
jj git fetch
","text":"Fetch from a Git remote
If a working-copy commit gets abandoned, it will be given a new, empty commit. This is true in general; it is not specific to this command.
Usage: jj git fetch [OPTIONS]
-b
, --branch <BRANCH>
\u2014 Fetch only some of the branches
By default, the specified name matches exactly. Use glob:
prefix to expand *
as a glob. The other wildcard characters aren't supported.
Default value: glob:*
--remote <REMOTE>
\u2014 The remote to fetch from (only named remotes are supported, can be repeated)
--all-remotes
\u2014 Fetch from all remotesjj git import
","text":"Update repo with changes made in the underlying Git repo
If a working-copy commit gets abandoned, it will be given a new, empty commit. This is true in general; it is not specific to this command.
Usage: jj git import
jj git init
","text":"Create a new Git backed repo
Usage: jj git init [OPTIONS] [DESTINATION]
<DESTINATION>
\u2014 The destination directory where the jj
repo will be created. If the directory does not exist, it will be created. If no directory is given, the current directory is used.
By default the git
repo is under $destination/.jj
Default value: .
--colocate
\u2014 Specifies that the jj
repo should also be a valid git
repo, allowing the use of both jj
and git
commands in the same directory.
This is done by placing the backing git repo into a .git
directory in the root of the jj
repo along with the .jj
directory. If the .git
directory already exists, all the existing commits will be imported.
This option is mutually exclusive with --git-repo
.
--git-repo <GIT_REPO>
\u2014 Specifies a path to an existing git repository to be used as the backing git repo for the newly created jj
repo.
If the specified --git-repo
path happens to be the same as the jj
repo path (both .jj and .git directories are in the same working directory), then both jj
and git
commands will work on the same repo. This is called a co-located repo.
This option is mutually exclusive with --colocate
.
jj git push
","text":"Push to a Git remote
By default, pushes any bookmarks pointing to remote_bookmarks(remote=<remote>)..@
. Use --bookmark
to push specific bookmarks. Use --all
to push all bookmarks. Use --change
to generate bookmark names based on the change IDs of specific commits.
Before the command actually moves, creates, or deletes a remote bookmark, it makes several safety checks. If there is a problem, you may need to run jj git fetch --remote <remote name>
and/or resolve some bookmark conflicts.
Usage: jj git push [OPTIONS]
--remote <REMOTE>
\u2014 The remote to push to (only named remotes are supported)-b
, --bookmark <BOOKMARK>
\u2014 Push only this bookmark, or bookmarks matching a pattern (can be repeated)
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets#string-patterns.
--all
\u2014 Push all bookmarks (including deleted bookmarks)
--tracked
\u2014 Push all tracked bookmarks (including deleted bookmarks)
This usually means that the bookmark was already pushed to or fetched from the relevant remote. For details, see https://martinvonz.github.io/jj/latest/bookmarks#remotes-and-tracked-bookmarks
--deleted
\u2014 Push all deleted bookmarks
Only tracked bookmarks can be successfully deleted on the remote. A warning will be printed if any untracked bookmarks on the remote correspond to missing local bookmarks.
--allow-empty-description
\u2014 Allow pushing commits with empty descriptions
--allow-private
\u2014 Allow pushing commits that are private-r
, --revisions <REVISIONS>
\u2014 Push bookmarks pointing to these commits (can be repeated)-c
, --change <CHANGE>
\u2014 Push this commit by creating a bookmark based on its change ID (can be repeated)--dry-run
\u2014 Only display what will change on the remotejj git remote
","text":"Manage Git remotes
The Git repo will be a bare git repo stored inside the .jj/
directory.
Usage: jj git remote <COMMAND>
add
\u2014 Add a Git remotelist
\u2014 List Git remotesremove
\u2014 Remove a Git remote and forget its bookmarksrename
\u2014 Rename a Git remoteset-url
\u2014 Set the URL of a Git remotejj git remote add
","text":"Add a Git remote
Usage: jj git remote add <REMOTE> <URL>
<REMOTE>
\u2014 The remote's name<URL>
\u2014 The remote's URLjj git remote list
","text":"List Git remotes
Usage: jj git remote list
jj git remote remove
","text":"Remove a Git remote and forget its bookmarks
Usage: jj git remote remove <REMOTE>
<REMOTE>
\u2014 The remote's namejj git remote rename
","text":"Rename a Git remote
Usage: jj git remote rename <OLD> <NEW>
<OLD>
\u2014 The name of an existing remote<NEW>
\u2014 The desired name for old
jj git remote set-url
","text":"Set the URL of a Git remote
Usage: jj git remote set-url <REMOTE> <URL>
<REMOTE>
\u2014 The remote's name<URL>
\u2014 The desired url for remote
jj help
","text":"Print this message or the help of the given subcommand(s)
Usage: jj help [OPTIONS] [COMMAND]...
<COMMAND>
\u2014 Print help for the subcommand(s)-k
, --keyword <KEYWORD>
\u2014 Show help for keywords instead of commands
Possible values:
revsets
: A functional language for selecting a set of revisiontutorial
: Show a tutorial to get started with jjjj init
","text":"Create a new repo in the given directory
If the given directory does not exist, it will be created. If no directory is given, the current directory is used.
Usage: jj init [DESTINATION]
<DESTINATION>
\u2014 The destination directory
Default value: .
jj interdiff
","text":"Compare the changes of two commits
This excludes changes from other commits by temporarily rebasing --from
onto --to
's parents. If you wish to compare the same change across versions, consider jj evolog -p
instead.
Usage: jj interdiff [OPTIONS] <--from <FROM>|--to <TO>> [PATHS]...
<PATHS>
\u2014 Restrict the diff to these paths--from <FROM>
\u2014 Show changes from this revision--to <TO>
\u2014 Show changes to this revision-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show-w
, --ignore-all-space
\u2014 Ignore whitespace when comparing lines-b
, --ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj log
","text":"Show revision history
Renders a graphical view of the project's history, ordered with children before parents. By default, the output only includes mutable revisions, along with some additional revisions for context.
Spans of revisions that are not included in the graph per --revisions
are rendered as a synthetic node labeled \"(elided revisions)\".
Usage: jj log [OPTIONS] [PATHS]...
<PATHS>
\u2014 Show revisions modifying the given paths-r
, --revisions <REVISIONS>
\u2014 Which revisions to show
If no paths nor revisions are specified, this defaults to the revsets.log
setting.
--reversed
\u2014 Show revisions in the opposite order (older revisions first)
-n
, --limit <LIMIT>
\u2014 Limit number of revisions to show
Applied after revisions are filtered and reordered.
--no-graph
\u2014 Don't show the graph, show a flat list of revisions
-T
, --template <TEMPLATE>
\u2014 Render each revision using the given template
For the syntax, see https://martinvonz.github.io/jj/latest/templates/
-p
, --patch
\u2014 Show patch
-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show--ignore-all-space
\u2014 Ignore whitespace when comparing lines--ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj new
","text":"Create a new, empty change and (by default) edit it in the working copy
By default, jj
will edit the new change, making the working copy represent the new commit. This can be avoided with --no-edit
.
Note that you can create a merge commit by specifying multiple revisions as argument. For example, jj new main @
will create a new commit with the main
bookmark and the working copy as parents.
For more information, see https://martinvonz.github.io/jj/latest/working-copy/.
Usage: jj new [OPTIONS] [REVISIONS]...
<REVISIONS>
\u2014 Parent(s) of the new change
Default value: @
-m
, --message <MESSAGE>
\u2014 The change description to use--no-edit
\u2014 Do not edit the newly created change-A
, --insert-after <INSERT_AFTER>
\u2014 Insert the new change after the given commit(s)-B
, --insert-before <INSERT_BEFORE>
\u2014 Insert the new change before the given commit(s)jj next
","text":"Move the working-copy commit to the child revision
The command creates a new empty working copy revision that is the child of a descendant offset
revisions ahead of the parent of the current working copy.
For example, when the offset is 1:
D D @\n| |/\nC @ => C\n|/ |\nB B\n
If --edit
is passed, the working copy revision is changed to the child of the current working copy revision.
D D\n| |\nC C\n| |\nB => @\n| |\n@ A\n
Usage: jj next [OPTIONS] [OFFSET]
<OFFSET>
\u2014 How many revisions to move forward. Advances to the next child by default
Default value: 1
-e
, --edit
\u2014 Instead of creating a new working-copy commit on top of the target commit (like jj new
), edit the target commit directly (like jj edit
)
Takes precedence over config in ui.movement.edit
; i.e. will negate ui.movement.edit = false
-n
, --no-edit
\u2014 The inverse of --edit
Takes precedence over config in ui.movement.edit
; i.e. will negate ui.movement.edit = true
--conflict
\u2014 Jump to the next conflicted descendant
jj operation
","text":"Commands for working with the operation log
For information about the operation log, see https://martinvonz.github.io/jj/latest/operation-log/.
Usage: jj operation <COMMAND>
abandon
\u2014 Abandon operation historydiff
\u2014 Compare changes to the repository between two operationslog
\u2014 Show the operation logrestore
\u2014 Create a new operation that restores the repo to an earlier stateshow
\u2014 Show changes to the repository in an operationundo
\u2014 Create a new operation that undoes an earlier operationjj operation abandon
","text":"Abandon operation history
To discard old operation history, use jj op abandon ..<operation ID>
. It will abandon the specified operation and all its ancestors. The descendants will be reparented onto the root operation.
To discard recent operations, use jj op restore <operation ID>
followed by jj op abandon <operation ID>..@-
.
The abandoned operations, commits, and other unreachable objects can later be garbage collected by using jj util gc
command.
Usage: jj operation abandon <OPERATION>
<OPERATION>
\u2014 The operation or operation range to abandonjj operation diff
","text":"Compare changes to the repository between two operations
Usage: jj operation diff [OPTIONS]
--operation <OPERATION>
\u2014 Show repository changes in this operation, compared to its parent--from <FROM>
\u2014 Show repository changes from this operation--to <TO>
\u2014 Show repository changes to this operation--no-graph
\u2014 Don't show the graph, show a flat list of modified changes-p
, --patch
\u2014 Show patch of modifications to changes
If the previous version has different parents, it will be temporarily rebased to the parents of the new version, so the diff is not contaminated by unrelated changes.
-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted
--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show--ignore-all-space
\u2014 Ignore whitespace when comparing lines--ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj operation log
","text":"Show the operation log
Like other commands, jj op log
snapshots the current working-copy changes and reconciles divergent operations. Use --at-op=@ --ignore-working-copy
to inspect the current state without mutation.
Usage: jj operation log [OPTIONS]
-n
, --limit <LIMIT>
\u2014 Limit number of operations to show--no-graph
\u2014 Don't show the graph, show a flat list of operations-T
, --template <TEMPLATE>
\u2014 Render each operation using the given template
For the syntax, see https://martinvonz.github.io/jj/latest/templates/
--op-diff
\u2014 Show changes to the repository at each operation
-p
, --patch
\u2014 Show patch of modifications to changes (implies --op-diff)
If the previous version has different parents, it will be temporarily rebased to the parents of the new version, so the diff is not contaminated by unrelated changes.
-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted
--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show--ignore-all-space
\u2014 Ignore whitespace when comparing lines--ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj operation restore
","text":"Create a new operation that restores the repo to an earlier state
This restores the repo to the state at the specified operation, effectively undoing all later operations. It does so by creating a new operation.
Usage: jj operation restore [OPTIONS] <OPERATION>
<OPERATION>
\u2014 The operation to restore to
Use jj op log
to find an operation to restore to. Use e.g. jj --at-op=<operation ID> log
before restoring to an operation to see the state of the repo at that operation.
--what <WHAT>
\u2014 What portions of the local state to restore (can be repeated)
This option is EXPERIMENTAL.
Default values: repo
, remote-tracking
Possible values:
repo
: The jj repo state and local bookmarksremote-tracking
: The remote-tracking bookmarks. Do not restore these if you'd like to push after the undojj operation show
","text":"Show changes to the repository in an operation
Usage: jj operation show [OPTIONS] [OPERATION]
<OPERATION>
\u2014 Show repository changes in this operation, compared to its parent(s)
Default value: @
--no-graph
\u2014 Don't show the graph, show a flat list of modified changes-p
, --patch
\u2014 Show patch of modifications to changes
If the previous version has different parents, it will be temporarily rebased to the parents of the new version, so the diff is not contaminated by unrelated changes.
-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted
--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show--ignore-all-space
\u2014 Ignore whitespace when comparing lines--ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj operation undo
","text":"Create a new operation that undoes an earlier operation
This undoes an individual operation by applying the inverse of the operation.
Usage: jj operation undo [OPTIONS] [OPERATION]
<OPERATION>
\u2014 The operation to undo
Use jj op log
to find an operation to undo.
Default value: @
--what <WHAT>
\u2014 What portions of the local state to restore (can be repeated)
This option is EXPERIMENTAL.
Default values: repo
, remote-tracking
Possible values:
repo
: The jj repo state and local bookmarksremote-tracking
: The remote-tracking bookmarks. Do not restore these if you'd like to push after the undojj parallelize
","text":"Parallelize revisions by making them siblings
Running jj parallelize 1::2
will transform the history like this:
3\n| 3\n2 / \\\n| -> 1 2\n1 \\ /\n| 0\n0\n
The command effectively says \"these revisions are actually independent\", meaning that they should no longer be ancestors/descendants of each other. However, revisions outside the set that were previously ancestors of a revision in the set will remain ancestors of it. For example, revision 0 above remains an ancestor of both 1 and 2. Similarly, revisions outside the set that were previously descendants of a revision in the set will remain descendants of it. For example, revision 3 above remains a descendant of both 1 and 2.
Therefore, jj parallelize '1 | 3'
is a no-op. That's because 2, which is not in the target set, was a descendant of 1 before, so it remains a descendant, and it was an ancestor of 3 before, so it remains an ancestor.
Usage: jj parallelize [REVISIONS]...
<REVISIONS>
\u2014 Revisions to parallelizejj prev
","text":"Change the working copy revision relative to the parent revision
The command creates a new empty working copy revision that is the child of an ancestor offset
revisions behind the parent of the current working copy.
For example, when the offset is 1:
D @ D\n|/ |\nA => A @\n| |/\nB B\n
If --edit
is passed, the working copy revision is changed to the parent of the current working copy revision.
D @ D\n|/ |\nC => @\n| |\nB B\n| |\nA A\n
Usage: jj prev [OPTIONS] [OFFSET]
<OFFSET>
\u2014 How many revisions to move backward. Moves to the parent by default
Default value: 1
-e
, --edit
\u2014 Edit the parent directly, instead of moving the working-copy commit
Takes precedence over config in ui.movement.edit
; i.e. will negate ui.movement.edit = false
-n
, --no-edit
\u2014 The inverse of --edit
Takes precedence over config in ui.movement.edit
; i.e. will negate ui.movement.edit = true
--conflict
\u2014 Jump to the previous conflicted ancestor
jj rebase
","text":"Move revisions to different parent(s)
There are three different ways of specifying which revisions to rebase: -b
to rebase a whole branch, -s
to rebase a revision and its descendants, and -r
to rebase a single commit. If none of them is specified, it defaults to -b @
.
With -s
, the command rebases the specified revision and its descendants onto the destination. For example, jj rebase -s M -d O
would transform your history like this (letters followed by an apostrophe are post-rebase versions):
O N'\n| |\n| N M'\n| | |\n| M O\n| | => |\n| | L | L\n| |/ | |\n| K | K\n|/ |/\nJ J\n
With -b
, the command rebases the whole \"branch\" containing the specified revision. A \"branch\" is the set of commits that includes:
In other words, jj rebase -b X -d Y
rebases commits in the revset (Y..X)::
(which is equivalent to jj rebase -s 'roots(Y..X)' -d Y
for a single root). For example, either jj rebase -b L -d O
or jj rebase -b M -d O
would transform your history like this (because L
and M
are on the same \"branch\", relative to the destination):
O N'\n| |\n| N M'\n| | |\n| M | L'\n| | => |/\n| | L K'\n| |/ |\n| K O\n|/ |\nJ J\n
With -r
, the command rebases only the specified revisions onto the destination. Any \"hole\" left behind will be filled by rebasing descendants onto the specified revision's parent(s). For example, jj rebase -r K -d M
would transform your history like this:
M K'\n| |\n| L M\n| | => |\n| K | L'\n|/ |/\nJ J\n
Note that you can create a merge commit by repeating the -d
argument. For example, if you realize that commit L actually depends on commit M in order to work (in addition to its current parent K), you can run jj rebase -s L -d K -d M
:
M L'\n| |\\\n| L M |\n| | => | |\n| K | K\n|/ |/\nJ J\n
If a working-copy commit gets abandoned, it will be given a new, empty commit. This is true in general; it is not specific to this command.
Usage: jj rebase [OPTIONS] <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
-b
, --branch <BRANCH>
\u2014 Rebase the whole branch relative to destination's ancestors (can be repeated)
jj rebase -b=br -d=dst
is equivalent to jj rebase '-s=roots(dst..br)' -d=dst
.
If none of -b
, -s
, or -r
is provided, then the default is -b @
.
-s
, --source <SOURCE>
\u2014 Rebase specified revision(s) together with their trees of descendants (can be repeated)
Each specified revision will become a direct child of the destination revision(s), even if some of the source revisions are descendants of others.
If none of -b
, -s
, or -r
is provided, then the default is -b @
.
-r
, --revisions <REVISIONS>
\u2014 Rebase the given revisions, rebasing descendants onto this revision's parent(s)
Unlike -s
or -b
, you may jj rebase -r
a revision A
onto a descendant of A
.
If none of -b
, -s
, or -r
is provided, then the default is -b @
.
-d
, --destination <DESTINATION>
\u2014 The revision(s) to rebase onto (can be repeated to create a merge commit)
-A
, --insert-after <INSERT_AFTER>
\u2014 The revision(s) to insert after (can be repeated to create a merge commit)
Only works with -r
and -s
.
-B
, --insert-before <INSERT_BEFORE>
\u2014 The revision(s) to insert before (can be repeated to create a merge commit)
Only works with -r
and -s
.
--skip-emptied
\u2014 If true, when rebasing would produce an empty commit, the commit is abandoned. It will not be abandoned if it was already empty before the rebase. Will never skip merge commits with multiple non-empty parents
jj resolve
","text":"Resolve a conflicted file with an external merge tool
Only conflicts that can be resolved with a 3-way merge are supported. See docs for merge tool configuration instructions.
Note that conflicts can also be resolved without using this command. You may edit the conflict markers in the conflicted file directly with a text editor.
Usage: jj resolve [OPTIONS] [PATHS]...
<PATHS>
\u2014 Restrict to these paths when searching for a conflict to resolve. We will attempt to resolve the first conflict we can find. You can use the --list
argument to find paths to use here-r
, --revision <REVISION>
Default value: @
-l
, --list
\u2014 Instead of resolving one conflict, list all the conflicts
--tool <NAME>
\u2014 Specify 3-way merge tool to be usedjj restore
","text":"Restore paths from another revision
That means that the paths get the same content in the destination (--to
) as they had in the source (--from
). This is typically used for undoing changes to some paths in the working copy (jj restore <paths>
).
If only one of --from
or --to
is specified, the other one defaults to the working copy.
When neither --from
nor --to
is specified, the command restores into the working copy from its parent(s). jj restore
without arguments is similar to jj abandon
, except that it leaves an empty revision with its description and other metadata preserved.
See jj diffedit
if you'd like to restore portions of files rather than entire files.
Usage: jj restore [OPTIONS] [PATHS]...
<PATHS>
\u2014 Restore only these paths (instead of all paths)--from <FROM>
\u2014 Revision to restore from (source)--to <TO>
\u2014 Revision to restore into (destination)-c
, --changes-in <REVISION>
\u2014 Undo the changes in a revision as compared to the merge of its parents.
This undoes the changes that can be seen with jj diff -r REVISION
. If REVISION
only has a single parent, this option is equivalent to jj restore --to REVISION --from REVISION-
.
The default behavior of jj restore
is equivalent to jj restore --changes-in @
.
--restore-descendants
\u2014 Preserve the content (not the diff) when rebasing descendants
jj root
","text":"Show the current workspace root directory
Usage: jj root
jj show
","text":"Show commit description and changes in a revision
Usage: jj show [OPTIONS] [REVISION]
<REVISION>
\u2014 Show changes in this revision, compared to its parent(s)
Default value: @
-T
, --template <TEMPLATE>
\u2014 Render a revision using the given template
For the syntax, see https://martinvonz.github.io/jj/latest/templates/
-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted
--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show--ignore-all-space
\u2014 Ignore whitespace when comparing lines--ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj simplify-parents
","text":"Simplify parent edges for the specified revision(s).
Removes all parents of each of the specified revisions that are also indirect ancestors of the same revisions through other parents. This has no effect on any revision's contents, including the working copy.
In other words, for all (A, B, C) where A has (B, C) as parents and C is an ancestor of B, A will be rewritten to have only B as a parent instead of B+C.
Usage: jj simplify-parents <--source <SOURCE>|--revisions <REVISIONS>>
-s
, --source <SOURCE>
\u2014 Simplify specified revision(s) together with their trees of descendants (can be repeated)-r
, --revisions <REVISIONS>
\u2014 Simplify specified revision(s) (can be repeated)jj sparse
","text":"Manage which paths from the working-copy commit are present in the working copy
Usage: jj sparse <COMMAND>
edit
\u2014 Start an editor to update the patterns that are present in the working copylist
\u2014 List the patterns that are currently present in the working copyreset
\u2014 Reset the patterns to include all files in the working copyset
\u2014 Update the patterns that are present in the working copyjj sparse edit
","text":"Start an editor to update the patterns that are present in the working copy
Usage: jj sparse edit
jj sparse list
","text":"List the patterns that are currently present in the working copy
By default, a newly cloned or initialized repo will have have a pattern matching all files from the repo root. That pattern is rendered as .
(a single period).
Usage: jj sparse list
jj sparse reset
","text":"Reset the patterns to include all files in the working copy
Usage: jj sparse reset
jj sparse set
","text":"Update the patterns that are present in the working copy
For example, if all you need is the README.md
and the lib/
directory, use jj sparse set --clear --add README.md --add lib
. If you no longer need the lib
directory, use jj sparse set --remove lib
.
Usage: jj sparse set [OPTIONS]
--add <ADD>
\u2014 Patterns to add to the working copy--remove <REMOVE>
\u2014 Patterns to remove from the working copy--clear
\u2014 Include no files in the working copy (combine with --add)jj split
","text":"Split a revision in two
Starts a diff editor on the changes in the revision. Edit the right side of the diff until it has the content you want in the first revision. Once you close the editor, your edited content will replace the previous revision. The remaining changes will be put in a new revision on top.
If the change you split had a description, you will be asked to enter a change description for each commit. If the change did not have a description, the second part will not get a description, and you will be asked for a description only for the first part.
Splitting an empty commit is not supported because the same effect can be achieved with jj new
.
Usage: jj split [OPTIONS] [PATHS]...
<PATHS>
\u2014 Put these paths in the first commit-i
, --interactive
\u2014 Interactively choose which parts to split. This is the default if no paths are provided--tool <NAME>
\u2014 Specify diff editor to be used (implies --interactive)-r
, --revision <REVISION>
\u2014 The revision to split
Default value: @
-p
, --parallel
\u2014 Split the revision into two parallel revisions instead of a parent and child
jj squash
","text":"Move changes from a revision into another revision
With the -r
option, moves the changes from the specified revision to the parent revision. Fails if there are several parent revisions (i.e., the given revision is a merge).
With the --from
and/or --into
options, moves changes from/to the given revisions. If either is left out, it defaults to the working-copy commit. For example, jj squash --into @--
moves changes from the working-copy commit to the grandparent.
If, after moving changes out, the source revision is empty compared to its parent(s), and --keep-emptied
is not set, it will be abandoned. Without --interactive
or paths, the source revision will always be empty.
If the source was abandoned and both the source and destination had a non-empty description, you will be asked for the combined description. If either was empty, then the other one will be used.
If a working-copy commit gets abandoned, it will be given a new, empty commit. This is true in general; it is not specific to this command.
Usage: jj squash [OPTIONS] [PATHS]...
<PATHS>
\u2014 Move only changes to these paths (instead of all paths)-r
, --revision <REVISION>
\u2014 Revision to squash into its parent (default: @)-f
, --from <FROM>
\u2014 Revision(s) to squash from (default: @)-t
, --into <INTO>
\u2014 Revision to squash into (default: @)-m
, --message <MESSAGE>
\u2014 The description to use for squashed revision (don't open editor)-u
, --use-destination-message
\u2014 Use the description of the destination revision and discard the description(s) of the source revision(s)-i
, --interactive
\u2014 Interactively choose which parts to squash--tool <NAME>
\u2014 Specify diff editor to be used (implies --interactive)-k
, --keep-emptied
\u2014 The source revision will not be abandonedjj status
","text":"Show high-level repo status
This includes:
Usage: jj status [PATHS]...
<PATHS>
\u2014 Restrict the status display to these pathsjj tag
","text":"Manage tags
Usage: jj tag <COMMAND>
list
\u2014 List tagsjj tag list
","text":"List tags
Usage: jj tag list [OPTIONS] [NAMES]...
<NAMES>
\u2014 Show tags whose local name matches
By default, the specified name matches exactly. Use glob:
prefix to select tags by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
-T
, --template <TEMPLATE>
\u2014 Render each tag using the given template
All 0-argument methods of the RefName
type are available as keywords.
For the syntax, see https://martinvonz.github.io/jj/latest/templates/
jj util
","text":"Infrequently used commands such as for generating shell completions
Usage: jj util <COMMAND>
completion
\u2014 Print a command-line-completion scriptconfig-schema
\u2014 Print the JSON schema for the jj TOML config formatgc
\u2014 Run backend-dependent garbage collectionmangen
\u2014 Print a ROFF (manpage)markdown-help
\u2014 Print the CLI help for all subcommands in Markdownjj util completion
","text":"Print a command-line-completion script
Apply it by running one of these:
source <(jj util completion bash)
jj util completion fish | source
jj util completion nushell | save \"completions-jj.nu\"\nuse \"completions-jj.nu\" * # Or `source \"completions-jj.nu\"`\n
autoload -U compinit\ncompinit\nsource <(jj util completion zsh)\n
Usage: jj util completion [SHELL]
<SHELL>
Possible values: bash
, elvish
, fish
, nushell
, power-shell
, zsh
jj util config-schema
","text":"Print the JSON schema for the jj TOML config format
Usage: jj util config-schema
jj util gc
","text":"Run backend-dependent garbage collection
Usage: jj util gc [OPTIONS]
--expire <EXPIRE>
\u2014 Time threshold
By default, only obsolete objects and operations older than 2 weeks are pruned.
Only the string \"now\" can be passed to this parameter. Support for arbitrary absolute and relative timestamps will come in a subsequent release.
jj util mangen
","text":"Print a ROFF (manpage)
Usage: jj util mangen
jj util markdown-help
","text":"Print the CLI help for all subcommands in Markdown
Usage: jj util markdown-help
jj undo
","text":"Undo an operation (shortcut for jj op undo
)
Usage: jj undo [OPTIONS] [OPERATION]
<OPERATION>
\u2014 The operation to undo
Use jj op log
to find an operation to undo.
Default value: @
--what <WHAT>
\u2014 What portions of the local state to restore (can be repeated)
This option is EXPERIMENTAL.
Default values: repo
, remote-tracking
Possible values:
repo
: The jj repo state and local bookmarksremote-tracking
: The remote-tracking bookmarks. Do not restore these if you'd like to push after the undojj version
","text":"Display version information
Usage: jj version
jj workspace
","text":"Commands for working with workspaces
Workspaces let you add additional working copies attached to the same repo. A common use case is so you can run a slow build or test in one workspace while you're continuing to write code in another workspace.
Each workspace has its own working-copy commit. When you have more than one workspace attached to a repo, they are indicated by <workspace name>@
in jj log
.
Each workspace also has own sparse patterns.
Usage: jj workspace <COMMAND>
add
\u2014 Add a workspaceforget
\u2014 Stop tracking a workspace's working-copy commit in the repolist
\u2014 List workspacesrename
\u2014 Renames the current workspaceroot
\u2014 Show the current workspace root directoryupdate-stale
\u2014 Update a workspace that has become stalejj workspace add
","text":"Add a workspace
By default, the new workspace inherits the sparse patterns of the current workspace. You can override this with the --sparse-patterns
option.
Usage: jj workspace add [OPTIONS] <DESTINATION>
<DESTINATION>
\u2014 Where to create the new workspace--name <NAME>
\u2014 A name for the workspace
To override the default, which is the basename of the destination directory.
-r
, --revision <REVISION>
\u2014 A list of parent revisions for the working-copy commit of the newly created workspace. You may specify nothing, or any number of parents.
If no revisions are specified, the new workspace will be created, and its working-copy commit will exist on top of the parent(s) of the working-copy commit in the current workspace, i.e. they will share the same parent(s).
If any revisions are specified, the new workspace will be created, and the new working-copy commit will be created with all these revisions as parents, i.e. the working-copy commit will exist as if you had run jj new r1 r2 r3 ...
.
--sparse-patterns <SPARSE_PATTERNS>
\u2014 How to handle sparse patterns when creating a new workspace
Default value: copy
Possible values:
copy
: Copy all sparse patterns from the current workspacefull
: Include all files in the new workspaceempty
: Clear all files from the workspace (it will be empty)jj workspace forget
","text":"Stop tracking a workspace's working-copy commit in the repo
The workspace will not be touched on disk. It can be deleted from disk before or after running this command.
Usage: jj workspace forget [WORKSPACES]...
<WORKSPACES>
\u2014 Names of the workspaces to forget. By default, forgets only the current workspacejj workspace list
","text":"List workspaces
Usage: jj workspace list
jj workspace rename
","text":"Renames the current workspace
Usage: jj workspace rename <NEW_WORKSPACE_NAME>
<NEW_WORKSPACE_NAME>
\u2014 The name of the workspace to update tojj workspace root
","text":"Show the current workspace root directory
Usage: jj workspace root
jj workspace update-stale
","text":"Update a workspace that has become stale
For information about stale working copies, see https://martinvonz.github.io/jj/latest/working-copy/.
Usage: jj workspace update-stale
This document was generated automatically by clap-markdown
.
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.
"},{"location":"code-of-conduct/#our-standards","title":"Our Standards","text":"Examples of behavior that contributes to a positive environment for our community include:
Examples of unacceptable behavior include:
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.
"},{"location":"code-of-conduct/#scope","title":"Scope","text":"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.
"},{"location":"code-of-conduct/#enforcement","title":"Enforcement","text":"Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at two or more of jaraco@jaraco.com, avamsi07@gmail.com, me@waleedkhan.name, and opensource@google.com. 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.
"},{"location":"code-of-conduct/#enforcement-guidelines","title":"Enforcement Guidelines","text":"Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
"},{"location":"code-of-conduct/#1-correction","title":"1. Correction","text":"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.
"},{"location":"code-of-conduct/#2-warning","title":"2. Warning","text":"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.
"},{"location":"code-of-conduct/#3-temporary-ban","title":"3. Temporary Ban","text":"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.
"},{"location":"code-of-conduct/#4-permanent-ban","title":"4. Permanent Ban","text":"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.
"},{"location":"code-of-conduct/#attribution","title":"Attribution","text":"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.
"},{"location":"community_tools/","title":"Community-built tools around Jujutsu","text":"Important: Many of these are not complete yet, just like Jujutsu itself. But they already simplify many workflows and can improve your experience.
"},{"location":"community_tools/#diffedit3","title":"Diffedit3","text":"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
"},{"location":"community_tools/#gg-gui-for-jj","title":"GG - GUI for JJ","text":"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.
"},{"location":"community_tools/#hunknvim","title":"Hunk.nvim","text":"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.
"},{"location":"community_tools/#lazyjj","title":"LazyJJ","text":"lazyjj is a lazygit inspired TUI for Jujutsu.
Find it here.
"},{"location":"community_tools/#jj-tui","title":"JJ TUI","text":"This is TUI for Jujutsu built in Ocaml, it is unopiniated and its creator is open to feedback.
Find it here.
"},{"location":"community_tools/#visual-jujutsu","title":"Visual Jujutsu","text":"VJJ is a fzf (fuzzy finder) wrapper for Jujutsu, which is meant to be used interactively in the terminal.
Find it here.
"},{"location":"community_tools/#visualjj","title":"VisualJJ","text":"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.
"},{"location":"community_tools/#finding-other-integrations","title":"Finding other integrations","text":"You can find other community contributed tools and integrations in our Wiki.
"},{"location":"config/","title":"Configuration","text":"These are the config settings available to jj/Jujutsu.
"},{"location":"config/#config-files-and-toml","title":"Config files and TOML","text":"jj
loads several types of config settings:
The built-in settings. These cannot be edited. They can be viewed in the cli/src/config/
directory in jj
's source repo.
The user settings. These can be edited with jj config edit --user
. User settings are located in the user config file, which can be found with jj config path --user
.
The repo settings. These can be edited with jj config edit --repo
and are located in .jj/repo/config.toml
.
Settings specified in 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.
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.
In TOML, anything under a heading can be dotted instead. For example, user.name = \"YOUR NAME\"
is equivalent to:
[user]\nname = \"YOUR NAME\"\n
For future reference, here are a couple of more complicated examples,
# Dotted style\ntemplate-aliases.\"format_short_id(id)\" = \"id.shortest(12)\"\ncolors.\"commit_id prefix\".bold = true\n\n# is equivalent to:\n[template-aliases]\n\"format_short_id(id)\" = \"id.shortest(12)\"\n\n[colors]\n\"commit_id prefix\" = { bold = true }\n
Jujutsu favors the dotted style in these instructions, if only because it's easier to write down in an unconfusing way. If you are confident with TOML then use whichever suits you in your config. If you mix dotted keys and headings, put the dotted keys before the first heading.
That's probably enough TOML to keep you out of trouble but the syntax guide is very short if you ever need to check.
"},{"location":"config/#user-settings","title":"User settings","text":"user.name = \"YOUR NAME\"\nuser.email = \"YOUR_EMAIL@example.com\"\n
Don't forget to change these to your own details!
"},{"location":"config/#ui-settings","title":"UI settings","text":""},{"location":"config/#colorizing-output","title":"Colorizing output","text":"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 color\n
"},{"location":"config/#custom-colors-and-styles","title":"Custom colors and styles","text":"You can customize the colors used for various elements of the UI. For example:
colors.commit_id = \"green\"\n
The following colors are available:
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\"\n
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, or make the text bold or underlined. For that, you need to use a table:
colors.commit_id = { fg = \"green\", bg = \"#ff1525\", bold = true, underline = true }\n
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\"\ncolors.\"working_copy commit_id\" = { underline = true }\n
Parts of the style that are not overridden - such as the foreground color in the example above - are inherited from the parent style.
Which elements can be colored is not yet documented, but see the default color configuration for some examples of what's possible.
"},{"location":"config/#default-command","title":"Default command","text":"When jj
is run with no explicit subcommand, the value of the ui.default-command
setting will be used instead. Possible values are any valid subcommand name, subcommand alias, or user-defined alias (defaults to \"log\"
).
ui.default-command = [\"log\", \"--reversed\"]\n
"},{"location":"config/#default-description","title":"Default description","text":"The editor content of a commit description can be populated by the draft_commit_description
template.
[templates]\ndraft_commit_description = '''\nconcat(\n description,\n surround(\n \"\\nJJ: This commit contains the following changes:\\n\", \"\",\n indent(\"JJ: \", diff.stat(72)),\n ),\n)\n'''\n
The value of the ui.default-description
setting can also be used in order to fill in things like BUG=, TESTED= etc.
ui.default-description = \"\\n\\nTESTED=TODO\"\n
"},{"location":"config/#diff-colors-and-styles","title":"Diff colors and styles","text":"In color-words and git diffs, word-level hunks are rendered with underline. You can override the default style with the following keys:
[colors]\n# Highlight hunks with background\n\"diff removed token\" = { bg = \"#221111\", underline = false }\n\"diff added token\" = { bg = \"#002200\", underline = false }\n
"},{"location":"config/#diff-format","title":"Diff format","text":"# Possible values: \"color-words\" (default), \"git\", \"summary\"\nui.diff.format = \"git\"\n
"},{"location":"config/#color-words-diff-options","title":"Color-words diff options","text":"In color-words diffs, changed words are displayed inline by default. 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. You can also change the default number of lines of context shown.
max-inline-alternation
: Maximum number of removed/added word alternation to inline. For example, <added> ... <added>
sequence has 1 alternation, so the line will be inline if max-inline-alternation >= 1
. <added> ... <removed> ... <added>
sequence has 3 alternation.
0
: disable inlining, making --color-words
more similar to --git
1
: inline removes-only or adds-only lines2
, 3
, ..: inline up to 2
, 3
, .. alternation-1
: inline all linesThe default is 3
.
This parameter is experimental. The definition is subject to change.
context
: Number of lines of context to show in the diff. The default is 3
.
[diff.color-words]\nmax-inline-alternation = 3\ncontext = 3\n
"},{"location":"config/#git-diff-options","title":"Git diff options","text":"In git diffs you can change the default number of lines of context shown.
context
: Number of lines of context to show in the diff. The default is 3
.[diff.git]\ncontext = 3\n
"},{"location":"config/#generating-diffs-by-external-command","title":"Generating diffs by external command","text":"If ui.diff.tool
is set, the specified diff command will be called instead of the internal diff function.
[ui]\n# Use Difftastic by default\ndiff.tool = [\"difft\", \"--color=always\", \"$left\", \"$right\"]\n# Use tool named \"<name>\" (see below)\ndiff.tool = \"<name>\"\n
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>]\n# program = \"<name>\" # Defaults to the name of the tool if not specified\ndiff-args = [\"--color=always\", \"$left\", \"$right\"]\n
$left
and $right
are replaced with the paths to the left and right directories to diff respectively.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]\ndiff.tool = \"vimdiff\"\n\n[merge-tools.vimdiff]\ndiff-invocation-mode = \"file-by-file\"\n
"},{"location":"config/#set-of-immutable-commits","title":"Set of immutable commits","text":"You can configure the set of immutable commits via revset-aliases.\"immutable_heads()\"
. The default set of immutable heads is builtin_immutable_heads()
, which in turn is defined as present(trunk()) | tags() | untracked_remote_bookmarks()
. For example, to also consider the release@origin
bookmark immutable:
revset-aliases.\"immutable_heads()\" = \"builtin_immutable_heads() | release@origin\"\n
To prevent rewriting commits authored by other users:
# The `trunk().. &` bit is an optimization to scan for non-`mine()` commits\n# only among commits that are not in `trunk()`.\nrevset-aliases.\"immutable_heads()\" = \"builtin_immutable_heads() | (trunk().. & ~mine())\"\n
Ancestors of the configured set are also immutable. The root commit is always immutable even if the set is empty.
"},{"location":"config/#log","title":"Log","text":""},{"location":"config/#default-revisions","title":"Default revisions","text":"You can configure the revisions jj log
would show when neither -r
nor any paths are specified.
# Show commits that are not in `main@origin`\nrevsets.log = \"main@origin..\"\n
The default value for revsets.log
is 'present(@) | ancestors(immutable_heads().., 2) | present(trunk())'
.
You can configure the template used when no -T
is specified.
templates.log
for jj log
templates.op_log
for jj op log
templates.show
for jj show
[templates]\n# Use builtin log template\nlog = \"builtin_log_compact\"\n# Use builtin op log template\nop_log = \"builtin_log_compact\"\n# Use builtin show template\nshow = \"builtin_log_detailed\"\n
If you want to see the full description when you do jj log
you can add this to your config:
[templates]\nlog = \"builtin_log_compact_full_description\"\n
"},{"location":"config/#graph-style","title":"Graph style","text":"# Possible values: \"curved\" (default), \"square\", \"ascii\", \"ascii-large\"\nui.graph.style = \"square\"\n
"},{"location":"config/#node-style","title":"Node style","text":"The symbols used to represent commits or operations can be customized via templates.
templates.log_node
for commits (with Option<Commit>
keywords)templates.op_log_node
for operations (with Operation
keywords)For example:
[templates]\nlog_node = '''\ncoalesce(\n if(!self, \"\ud83e\udf80\"),\n if(current_working_copy, \"@\"),\n if(root, \"\u2534\"),\n if(immutable, \"\u25cf\", \"\u25cb\"),\n)\n'''\nop_log_node = 'if(current_operation, \"@\", \"\u25cb\")'\n
"},{"location":"config/#wrap-log-content","title":"Wrap log content","text":"If enabled, log
/evolog
/op log
content will be wrapped based on the terminal width.
ui.log-word-wrap = true\n
"},{"location":"config/#display-of-commit-and-change-ids","title":"Display of commit and change ids","text":"Can be customized by the format_short_id()
template alias.
[template-aliases]\n# Highlight unique prefix and show at least 12 characters (default)\n'format_short_id(id)' = 'id.shortest(12)'\n# Just the shortest possible unique prefix\n'format_short_id(id)' = 'id.shortest()'\n# Show unique prefix and the rest surrounded by brackets\n'format_short_id(id)' = 'id.shortest(12).prefix() ++ \"[\" ++ id.shortest(12).rest() ++ \"]\"'\n# Always show 12 characters\n'format_short_id(id)' = 'id.short(12)'\n
To customize these separately, use the format_short_commit_id()
and format_short_change_id()
aliases:
[template-aliases]\n# Uppercase change ids. `jj` treats change and commit ids as case-insensitive.\n'format_short_change_id(id)' = 'format_short_id(id).upper()'\n
To get shorter prefixes for certain revisions, set revsets.short-prefixes
:
# Prioritize the current bookmark\nrevsets.short-prefixes = \"(main..@)::\"\n
"},{"location":"config/#relative-timestamps","title":"Relative timestamps","text":"Can be customized by the format_timestamp()
template alias.
[template-aliases]\n# Full timestamp in ISO 8601 format\n'format_timestamp(timestamp)' = 'timestamp'\n# Relative timestamp rendered as \"x days/hours/seconds ago\"\n'format_timestamp(timestamp)' = 'timestamp.ago()'\n
jj op log
defaults to relative timestamps. To use absolute timestamps, you will need to modify the format_time_range()
template alias.
[template-aliases]\n'format_time_range(time_range)' = 'time_range.start() ++ \" - \" ++ time_range.end()'\n
"},{"location":"config/#author-format","title":"Author format","text":"Can be customized by the format_short_signature()
template alias.
[template-aliases]\n# Full email address (default)\n'format_short_signature(signature)' = 'signature.email()'\n# Both name and email address\n'format_short_signature(signature)' = 'signature'\n# Username part of the email address\n'format_short_signature(signature)' = 'signature.username()'\n
"},{"location":"config/#allow-large-revsets-by-default","title":"Allow \"large\" revsets by default","text":"Certain commands (such as jj rebase
) can take multiple revset arguments, but default to requiring each of those revsets to expand to a single revision. This restriction can be overridden by prefixing a revset that the user wants to be able to expand to more than one revision with the all:
modifier.
Another way you can override this check is by setting ui.always-allow-large-revsets
to true
. Then, jj
will allow every one of the revset arguments of such commands to expand to any number of revisions.
# Assume `all:` prefix before revsets whenever it would make a difference\nui.always-allow-large-revsets = true\n
"},{"location":"config/#pager","title":"Pager","text":"The default pager is can be set via ui.pager
or the PAGER
environment variable. The priority is as follows (environment variables are marked with a $
):
ui.pager
> $PAGER
less -FRX
is the default pager in the absence of any other setting, except on Windows where it is :builtin
.
The special value :builtin
enables usage of the integrated pager called minus
. See the docs for the minus
pager for the key bindings and some more details.
If you are using a standard Linux distro, your system likely already has $PAGER
set and that will be preferred over the built-in. To use the built-in:
jj config set --user ui.pager :builtin\n
It is possible the default will change to :builtin
for all platforms in the future.
Additionally, paging behavior can be toggled via ui.paginate
like so:
# Enable pagination for commands that support it (default)\nui.paginate = \"auto\"\n# Disable all pagination, equivalent to using --no-pager\nui.paginate = \"never\"\n
"},{"location":"config/#processing-contents-to-be-paged","title":"Processing contents to be paged","text":"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\"]\n
Some formatters (like delta
) require git style diffs for formatting. You can configure this style of diff as the default with the ui.diff
setting. For example:
[ui]\npager = \"delta\"\n\n[ui.diff]\nformat = \"git\"\n
"},{"location":"config/#aliases","title":"Aliases","text":"You can define aliases for commands, including their arguments. For example:
# `jj l` shows commits on the working-copy commit's (anonymous) bookmark\n# compared to the `main` bookmark\naliases.l = [\"log\", \"-r\", \"(main..@):: | (main..@)-\"]\n
"},{"location":"config/#editor","title":"Editor","text":"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
Pico is the default editor (Notepad on Windows) in the absence of any other setting, but you could set it explicitly too.
ui.editor = \"pico\"\n
To use NeoVim instead:
ui.editor = \"nvim\"\n
For GUI editors you possibly need to use a -w
or --wait
. Some examples:
ui.editor = \"code -w\" # VS Code\nui.editor = \"code.cmd -w\" # VS Code on Windows\nui.editor = \"bbedit -w\" # BBEdit\nui.editor = \"subl -n -w\" # Sublime Text\nui.editor = \"mate -w\" # TextMate\nui.editor = [\"C:/Program Files/Notepad++/notepad++.exe\",\n \"-multiInst\", \"-notabbar\", \"-nosession\", \"-noPlugin\"] # Notepad++\nui.editor = \"idea --temp-project --wait\" #IntelliJ\n
Obviously, you would only set one line, don't copy them all in!
"},{"location":"config/#editing-diffs","title":"Editing diffs","text":"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, e.g. Meld to use the meld
diff editor.
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\nmerge-tools.meld.program = \"/path/to/meld\" # May be necessary if `meld` is not in the PATH\nmerge-tools.meld.edit-args = [\"--newtab\", \"$left\", \"$right\"]\n
jj
makes the following substitutions:
$left
and $right
are replaced with the paths to the left and right directories to diff respectively.
If no edit-args
are specified, [\"$left\", \"$right\"]
are set by default.
Finally, ui.diff-editor
can be a list that specifies a command and its arguments.
Some examples:
# Use merge-tools.meld.edit-args\nui.diff-editor = \"meld\" # Or `kdiff3`, or `diffedit3`, ...\n# Specify edit-args inline\nui.diff-editor = [\"/path/to/binary\", \"--be-helpful\", \"$left\", \"$right\"]\n# Equivalent to [\"binary\", \"$left\", \"$right\"] arguments by default\nui.diff-editor = \"binary\"\n
"},{"location":"config/#experimental-3-pane-diff-editing","title":"Experimental 3-pane diff editing","text":"We offer two special \"3-pane\" diff editor configs:
meld-3
, which requires installing Meld, anddiffedit3
, which requires installing diffedit3
.Meld
is a graphical application that is recommended, but can be difficult to install in some situations. diffedit3
is designed to be easy to install and to be usable in environments where Meld is difficult to use (e.g. over SSH via port forwarding). diffedit3
starts a local server that can be accessed via a web browser, similarly to Jupyter.
There is also the diffedit3-ssh
which is similar to diffedit3
but does not try to open the web browser pointing to the local server (the URL printed to the terminal) automatically. diffedit3-ssh
also always uses ports in between 17376-17380 and fails if they are all busy. This can be useful when working over SSH. Open the fold below for more details of how to set that up.
ssh -L 17376:localhost:17376 \\\n -L 17377:localhost:17377 \\\n -L 17378:localhost:17378 \\\n -L 17379:localhost:17379 \\\n -L 17380:localhost:17380 \\\n myhost.example.com\n
`diffedit3-ssh` is set up to use these 5 ports by default. Usually, only the first of them will be used. The rest are used if another program happens to use one of them, or if you run multiple instances of `diffedit3` at the same time. Another way is to add a snippet to `~/.ssh/config`: Host myhost\n User myself\n Hostname myhost.example.com\n LocalForward 17376 localhost:17376\n LocalForward 17377 localhost:17377\n LocalForward 17378 localhost:17378\n LocalForward 17379 localhost:17379\n LocalForward 17380 localhost:17380\n
With that configuration, you should be able to simply `ssh myhost`. Setting either ui.diff-editor = \"meld-3\"
or ui.diff-editor = \"diffedit3\"
will result in the diff editor showing 3 panes: the diff on the left and right, and an editing pane in the middle. This allow you to see both sides of the original diff while editing.
If you use ui.diff-editor = \"meld-3\"
, note that you can still get the 2-pane Meld view using jj diff --tool meld
. diffedit3
has a button you can use to switch to a 2-pane view.
To configure other diff editors in this way, you can include $output
together with $left
and $right
in merge-tools.TOOL.edit-args
. jj
will replace $output
with the directory where the diff editor will be expected to put the result of the user's edits. Initially, the contents of $output
will be the same as the contents of $right
.
JJ-INSTRUCTIONS
","text":"When editing a diff, jj will include a synthetic file called JJ-INSTRUCTIONS
in the diff with instructions on how to edit the diff. Any changes you make to this file will be ignored. To suppress the creation of this file, set ui.diff-instructions = false
.
Using 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.
The ui.merge-editor
key specifies the tool used for three-way merge tools by jj resolve
. For example:
# Use merge-tools.meld.merge-args\nui.merge-editor = \"meld\" # Or \"vscode\" or \"vscodium\" or \"kdiff3\" or \"vimdiff\"\n# Specify merge-args inline\nui.merge-editor = [\"meld\", \"$left\", \"$base\", \"$right\", \"-o\", \"$output\"]\n
The \"vscode\", \"vscodium\", \"meld\", \"kdiff3\", and \"vimdiff\" tools can be used out of the box, as long as they are installed.
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.
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\nmerge-tools.kdiff3.merge-args = [\"$base\", \"$left\", \"$right\", \"-o\", \"$output\", \"--auto\"]\nmerge-tools.meld.merge-args = [\"$left\", \"$base\", \"$right\", \"-o\", \"$output\", \"--auto-merge\"]\n\nmerge-tools.vimdiff.merge-args = [\"-f\", \"-d\", \"$output\", \"-M\",\n \"$left\", \"$base\", \"$right\",\n \"-c\", \"wincmd J\", \"-c\", \"set modifiable\",\n \"-c\", \"set write\"]\nmerge-tools.vimdiff.program = \"vim\"\nmerge-tools.vimdiff.merge-tool-edits-conflict-markers = true # See below for an explanation\n
jj
makes the following substitutions:
$output
(REQUIRED) is replaced with the name of the file that the merge tool should output. jj
will read this file after the merge tool exits.
$left
and $right
are replaced with the paths to two files containing the content of each side of the conflict.
$base
is replaced with the path to a file containing the contents of the conflicted file in the last common ancestor of the two sides of the conflict.
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. This is appropriate for most graphical merge tools.
Some tools (e.g. vimdiff
) can present a multi-way diff but don't resolve conflict themselves. When using such tools, jj
can help you by populating the output file with conflict markers before starting the merge tool (instead of leaving the output file empty and letting the merge tool fill it in). To do that, set the merge-tools.vimdiff.merge-tool-edits-conflict-markers = true
option.
With this option set, if the output file still contains conflict markers after the conflict is done, jj
assumes that the conflict was only partially resolved and parses the conflict markers to get the new state of the conflict. The conflict is considered fully resolved when there are no conflict markers left.
The jj fix
command allows you to efficiently rewrite files in complex commit graphs with no risk of introducing conflicts, using tools like clang-format
or prettier
. The tools run as subprocesses that take file content on standard input and repeat it, with any desired changes, on standard output. The file is only rewritten if the subprocess produces a successful exit code.
Suppose you want to use clang-format
to format your *.c
and *.h
files, as well as sorting their #include
directives.
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 $path
substitution to provide the name of the file in a command argument.
[fix.tools.clang-format]\ncommand = [\"/usr/bin/clang-format\", \"--sort-includes\", \"--assume-filename=$path\"]\npatterns = [\"glob:'**/*.c'\",\n \"glob:'**/*.h'\"]\n
"},{"location":"config/#sort-and-remove-duplicate-lines-from-a-file","title":"Sort and remove duplicate lines from a file","text":"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]\ncommand = [\"sort\", \"-u\"]\npatterns = [\"word_list.txt\"]\n
"},{"location":"config/#execution-order-of-tools","title":"Execution order of tools","text":"If two or more tools affect the same file, they are executed in the ascending lexicographical order of their configured names. This will remain as a tie breaker if other ordering mechanisms are introduced in the future. If you use numbers in tool names to control execution order, remember to include enough leading zeros so that, for example, 09
sorts before 10
.
Suppose you want to keep only the 10 smallest numbers in a text file that contains one number on each line. This can be accomplished with sort
and head
, but execution order is important.
[fix.tools.1-sort-numbers-file]\ncommand = [\"sort\", \"-n\"]\npatterns = [\"numbers.txt\"]\n\n[fix.tools.2-truncate-numbers-file]\ncommand = [\"head\", \"-n\", \"10\"]\npatterns = [\"numbers.txt\"]\n
"},{"location":"config/#commit-signing","title":"Commit Signing","text":"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.
[signing]\nsign-all = true\nbackend = \"gpg\"\nkey = \"4ED556E9729E000F\"\n## You can set `key` to anything accepted by `gpg -u`\n# key = \"signing@example.com\"\n
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\"\n
Also by default the gpg backend will ignore key expiry when verifying commit signatures. To consider expired keys as invalid you can set:
signing.backends.gpg.allow-expired-keys = false\n
"},{"location":"config/#ssh-signing","title":"SSH Signing","text":"[signing]\nsign-all = true\nbackend = \"ssh\"\nkey = \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGj+J6N6SO+4P8dOZqfR1oiay2yxhhHnagH52avUqw5h\"\n## You can also use a path instead of embedding the key\n# key = \"~/.ssh/id_for_signing.pub\"\n
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\"\n
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\"\n
"},{"location":"config/#git-settings","title":"Git settings","text":""},{"location":"config/#default-remotes-for-jj-git-fetch-and-jj-git-push","title":"Default remotes for jj git fetch
and jj git push
","text":"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\"\njj config set --repo git.fetch '[\"origin\", \"upstream\"]'\n
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\"\n
Note that unlike git.fetch
, git.push
can currently only be a single remote. This is not a hard limitation, and could be changed in the future if there is demand.
When jj
imports a new remote-tracking bookmark from Git, it can also create a local bookmark with the same name. This feature is disabled by default because it may be undesirable in some repositories, e.g.:
You can enable this behavior by setting git.auto-local-bookmark
like so,
git.auto-local-bookmark = true\n
This setting is applied only to new remote bookmarks. Existing remote bookmarks can be tracked individually by using jj bookmark track
/untrack
commands.
# import feature1 bookmark and start tracking it\njj bookmark track feature1@origin\n# delete local gh-pages bookmark and stop tracking it\njj bookmark delete gh-pages\njj bookmark untrack gh-pages@upstream\n
"},{"location":"config/#abandon-commits-that-became-unreachable-in-git","title":"Abandon commits that became unreachable in Git","text":"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 = false\n
"},{"location":"config/#prefix-for-generated-bookmarks-on-push","title":"Prefix for generated bookmarks on push","text":"jj git push --change
generates bookmark names with a prefix of \"push-\" by default. You can pick a different prefix by setting git.push-bookmark-prefix
. For example:
git.push-bookmark-prefix = \"martinvonz/push-\"\n
"},{"location":"config/#set-of-private-commits","title":"Set of private commits","text":"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.
# Prevent pushing work in progress or anything explicitly labeled \"private\"\ngit.private-commits = \"description(glob:'wip:*') | description(glob:'private:*')\"\n
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.
"},{"location":"config/#filesystem-monitor","title":"Filesystem monitor","text":"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 core.fsmonitor
option. Currently, the valid values are \"none\"
or \"watchman\"
.
To configure the Watchman filesystem monitor, set core.fsmonitor = \"watchman\"
. Ensure that you have installed the Watchman executable on your system.
You can configure jj
to use watchman triggers to automatically create snapshots on filesystem changes by setting core.watchman.register_snapshot_trigger = true
.
You can check whether Watchman is enabled and whether it is installed correctly using jj debug watchman status
.
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.
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.
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\"\n# the following is equivalent\nsnapshot.max-new-file-size = 10485760\n
The value can be specified using a human readable string with typical suffixes; B
, MiB
, GB
, etc. By default, if no suffix is provided, or the value is a raw integer literal, the value is interpreted as if it were specified in bytes.
Files that already exist in the working copy are not subject to this limit.
Setting this value to zero will disable the limit entirely.
"},{"location":"config/#ways-to-specify-jj-config-details","title":"Ways to specifyjj
config: details","text":""},{"location":"config/#user-config-file","title":"User config file","text":"An easy way to find the user config file is:
jj config path --user\n
The rest of this section covers the details of where this file can be located.
On all platforms, the user's global jj
configuration file is located at either ~/.jjconfig.toml
(where ~
represents $HOME
on Unix-likes, or %USERPROFILE%
on Windows) or in a platform-specific directory. The platform-specific location is recommended for better integration with platform services. It is an error for both of these files to exist.
$XDG_CONFIG_HOME/jj/config.toml
/home/alice/.config/jj/config.toml
macOS $HOME/Library/Application Support/jj/config.toml
/Users/Alice/Library/Application Support/jj/config.toml
Windows {FOLDERID_RoamingAppData}\\jj\\config.toml
C:\\Users\\Alice\\AppData\\Roaming\\jj\\config.toml
The location of the jj
config file can also be overridden with the JJ_CONFIG
environment variable. If it is not empty, it should contain the path to a TOML file that will be used instead of any configuration file in the default locations. For example,
env JJ_CONFIG=/dev/null jj log # Ignores any settings specified in the config file.\n
"},{"location":"config/#specifying-config-on-the-command-line","title":"Specifying config on the command-line","text":"You can use one or more --config-toml
options on the command line to specify additional configuration settings. This overrides settings defined in config files or environment variables. For example,
jj --config-toml='ui.color=\"always\"' --config-toml='ui.diff-editor=\"kdiff3\"' split\n
Config specified this way must be valid TOML. In particular, string values must be surrounded by quotes. To pass these quotes to jj
, most shells require surrounding those quotes with single quotes as shown above.
In sh
-compatible shells, --config-toml
can be used to merge entire TOML files with the config specified in .jjconfig.toml
:
jj --config-toml=\"$(cat extra-config.toml)\" log\n
"},{"location":"conflicts/","title":"First-class conflicts","text":""},{"location":"conflicts/#introduction","title":"Introduction","text":"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).
"},{"location":"conflicts/#advantages","title":"Advantages","text":"The deeper understanding of conflicts has many advantages:
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.For information about how conflicts are handled in the working copy, see here.
"},{"location":"conflicts/#conflict-markers","title":"Conflict markers","text":"Conflicts are \"materialized\" using conflict markers in various contexts. For example, when you run 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). Here's an example of how Git can render a conflict using its \"diff3\" style:
<<<<<<< left\n apple\n grapefruit\n orange\n ||||||| base\n apple\n grape\n orange\n =======\n APPLE\n GRAPE\n ORANGE\n >>>>>>> right\n
In this example, the left side changed \"grape\" to \"grapefruit\", and the right side made all lines uppercase. To resolve the conflict, we would presumably keep the right side (the third section) and replace \"GRAPE\" by \"GRAPEFRUIT\". This way of visually finding the changes between the base and one side and then applying them to the other side is a common way of resolving conflicts when using Git's \"diff3\" style.
Jujutsu helps you by combining the base and one side into a unified diff for you, making it easier to spot the differences to apply to the other side. Here's how that would look for the same example as above:
<<<<<<<\n %%%%%%%\n apple\n -grape\n +grapefruit\n orange\n +++++++\n APPLE\n GRAPE\n ORANGE\n >>>>>>>\n
As in Git, the <<<<<<<
and >>>>>>>
lines mark the start and end of the conflict. The %%%%%%%
line indicates the start of a diff. The +++++++
line indicates the start of a snapshot (not a diff).
There is another reason for this format (in addition to helping you spot the differences): The format supports more complex conflicts involving more than 3 inputs. Such conflicts can arise when you merge more than 2 commits. They would typically be rendered as a single snapshot (as above) but with more than one unified diffs. The process for resolving them is similar: Manually apply each diff onto the snapshot.
"},{"location":"contributing/","title":"How to Contribute","text":""},{"location":"contributing/#policies","title":"Policies","text":"We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow.
"},{"location":"contributing/#contributor-license-agreement","title":"Contributor License Agreement","text":"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 your current agreements on file or to sign a new one.
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.
"},{"location":"contributing/#code-reviews","title":"Code reviews","text":"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.
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 start the commit message with <topic>:
and don't use conventional commits. 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.
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.
When your first PR has been approved, we typically give you contributor access, so you can address any remaining minor comments and then merge the PR yourself when you're ready. If you realize that some comments require non-trivial changes, please ask your reviewer to take another look.
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.
"},{"location":"contributing/#community-guidelines","title":"Community Guidelines","text":"This project follows Google's Open Source Community Guidelines.
"},{"location":"contributing/#contributing-large-patches","title":"Contributing large patches","text":"Before sending a PR for a large change which designs/redesigns or reworks an existing component, we require an architecture review from multiple stakeholders, which we do with Design Docs, see the process here.
"},{"location":"contributing/#contributing-to-the-documentation","title":"Contributing to the documentation","text":"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.
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++.
"},{"location":"contributing/#setting-up-a-development-environment","title":"Setting up a development environment","text":"To develop jj
, the mandatory steps are simply to install Rust (the default installer options are fine), clone the repository, and use cargo build
, cargo fmt
, cargo clippy --workspace --all-targets
, and cargo test --workspace
. If you are preparing a PR, there are some additional recommended steps.
One-time setup:
rustup toolchain add nightly # wanted for 'rustfmt'\nrustup toolchain add 1.76 # also specified in Cargo.toml\ncargo install cargo-insta\ncargo install cargo-watch\ncargo install cargo-nextest\n
During development (adapt according to your preference):
cargo watch --ignore '.jj/**' -s \\\n 'cargo clippy --workspace --all-targets \\\n && cargo +1.76 check --workspace --all-targets'\ncargo +nightly fmt # Occasionally\ncargo nextest run --workspace # Occasionally\ncargo insta test --workspace --test-runner nextest # Occasionally\n
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
.
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 the insta
crate. To make this convenient, install the cargo-insta
binary. Use cargo insta test --workspace
to run tests, and cargo insta review --workspace
to update the snapshot tests. The --workspace
flag is needed to run the tests on all crates; by default, only the crate in the current directory is tested.
GitHub CI checks require that the code is formatted with the nightly version of rustfmt
. To do this on your computer, install the nightly toolchain and use cargo +nightly fmt
.
Your code will be rejected if it cannot be compiled with the minimal supported version of Rust (\"MSRV\"). Currently, jj
follows a rather casual MSRV policy: \"The current rustc
stable version, minus one.\" As of this writing, that version is 1.76.0.
Your code needs to pass cargo clippy
. You can also use cargo +nightly clippy
if you wish to see more warnings.
You may also want to install and use cargo-watch
. In this case, you should exclude .jj
. directory from the filesystem watcher, as it gets updated on every jj log
.
To run tests more quickly, use cargo nextest run --workspace
. To use nextest
with insta
, use cargo insta test --workspace --test-runner nextest
.
On Linux, you may be able to speed up nextest
even further by using the mold
linker, as explained below.
mold
for faster tests on Linux","text":"On a machine with a multi-core CPU, one way to speed up cargo nextest
on Linux is to use the multi-threaded mold
linker. This linker may help if, currently, your CPU is underused while Rust is linking test binaries. Before proceeding with mold
, you can check whether this is an issue worth solving using a system monitoring tool such as htop
.
mold
is packaged for many distributions. On Debian, for example, sudo apt install mold
should just work.
A simple way to use mold
is via the -run
option, e.g.:
mold -run cargo insta test --workspace --test-runner nextest\n
There will be no indication that a different linker is used, except for higher CPU usage while linking and, hopefully, faster completion. You can verify that mold
was indeed used by running readelf -p .comment target/debug/jj
.
There are also ways of having Rust use mold
by default, see the \"How to use\" instructions.
On recent versions of MacOS, the default linker Rust uses is already multi-threaded. It should use all the CPU cores without any configuration.
"},{"location":"contributing/#previewing-the-html-documentation","title":"Previewing the HTML documentation","text":"The documentation for jj
is automatically published to the website at https://martinvonz.github.io/jj/.
When editing documentation, we'd appreciate it if you checked that the result will look as expected when published to the website.
"},{"location":"contributing/#setting-up-the-prerequisites","title":"Setting up the prerequisites","text":"To build the website, you must have Python and poetry 1.8+
installed (the latest version is recommended). It is easiest to install poetry
via pipx
, as explained in the Poetry installation instructions. A few helpful points from the instructions: pipx
can often be installed from your distribution, e.g. sudo apt install pipx
; this will usually also install Python for you if necessary. Any version of pipx
will do. If you are installing pipx
manually, you may first need to follow the Python installation instructions.
Once you have poetry
installed, you should ask it to install the rest of the required tools into a virtual environment as follows:
poetry install\n
You may get requests to \"unlock a keyring\", an error messages about failing to do so, or Poetry may simply hang indefinitely. The workaround is to either to unlock the keyring or to run the following, and then to try poetry install
again:
# For sh-compatible shells or recent versions of `fish`\nexport PYTHON_KEYRING_BACKEND=keyring.backends.fail.Keyring\n
"},{"location":"contributing/#building-the-html-docs-locally-with-live-reload","title":"Building the HTML docs locally (with live reload)","text":"The HTML docs are built with MkDocs. After following the above steps, you should be able to view the docs by running
# Note: this and all the commands below should be run from the root of\n# the `jj` source tree.\npoetry run -- mkdocs serve\n
and opening http://127.0.0.1:8000 in your browser.
As you edit the md
files, the website should be rebuilt and reloaded in your browser automatically, unless build errors occur.
You should occasionally check the terminal from which you ran mkdocs serve
for any build errors or warnings. Warnings about \"GET /versions.json HTTP/1.1\" code 404
are expected and harmless.
The full jj
website includes the documentation for several jj
versions (prerelease
, latest release, and the older releases). The top-level URL https://martinvonz.github.io/jj redirects to https://martinvonz.github.io/jj/latest, which in turn redirects to the docs for the last stable version.
The different versions of documentation are managed and deployed with mike
, which can be run with poetry 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
as a co-located jj + git
repository (jj clone --colocate
), cloned from your fork of jj
(e.g. jjfan.github.com/jj
). You can also use a pure Git repo if you prefer.
Make sure jjfan.github.com/jj
includes the gh-pages
bookmark of the jj repo and run git fetch origin gh-pages
.
Go to the GitHub repository settings, enable GitHub Pages, and configure them to use the gh-pages
bookmark (this is usually the default).
Run the same sh
script that is used in GitHub CI (details below):
.github/scripts/docs-build-deploy 'https://jjfan.github.io/jj/'\\\n prerelease main --push\n
This should build the version of the docs from the current commit, deploy it as a new commit to the gh-pages
bookmark, and push the gh-pages
bookmark to the origin.
Now, you should be able to see the full website, including your latest changes to the prerelease
version, at https://jjfan.github.io/jj/prerelease/
.
(Optional) The previous steps actually only rebuild https://jjfan.github.io/jj/prerelease/
and its alias https://jjfan.github.io/jj/main/
. If you'd like to test out version switching back and forth, you can also rebuild the docs for the latest release as follows.
jj new v1.33.1 # Let's say `jj 1.33.1` is the currently the latest release\n.github/scripts/docs-build-deploy 'https://jjfan.github.io/jj/'\\\n v1.33.1 latest --push\n
(Optional) When you are done, you may want to reset the gh-bookmarks
to the same spot as it is in the upstream. If you configured the upstream
remote, this can be done with:
# This will LOSE any changes you made to `gh-pages`\njj git fetch --remote upstream\njj bookmark set gh-pages -r gh-pages@upstream\njj git push --remote origin --bookmark gh-pages\n
If you want to preserve some of the changes you made, you can do jj bookmark set my-changes -r gh-pages
BEFORE running the above commands.
docs-build-deploy
script","text":"The script sets up the site_url
mkdocs config to 'https://jjfan.github.io/jj/'
. If this config does not match the URL where you loaded the website, some minor website features (like the version switching widget) will have reduced functionality.
Then, the script passes the rest of its arguments to potery run -- mike deploy
, which does the rest of the job. Run poetry run -- mike help deploy
to find out what the arguments do.
If you need to do something more complicated, you can use poetry 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://martinvonz.github.com/jj (instead of the usual main
bookmark).
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.
protoc
compiler. This usually means either apt-get install protobuf-compiler
or downloading an official release. The prost
library docs have additional advice.cargo run -p gen-protos
regularly (or after every edit to a .proto
file). This is the same as running cargo run
from lib/gen-protos
. The gen-protos
binary will use the prost-build
library to compile the .proto
files into .rs
files..proto
file, you will need to edit the list of these files in lib/gen-protos/src/main.rs
.The .rs
files generated from .proto
files are included in the repository, and there is a GitHub CI check that will complain if they do not match.
One easy-to-use sampling profiler is samply. For example:
cargo install samply\nsamply record jj diff\n
Then just open the link it prints. Another option is to use the instrumentation we've added manually (using tracing::instrument
) in various places. For example:
JJ_TRACE=/tmp/trace.json jj diff\n
Then go to https://ui.perfetto.dev/
in Chrome and load /tmp/trace.json
from there."},{"location":"design_doc_blueprint/","title":"Title","text":"A cool name for your Project
Author: Your-Name
If there are multiple authors, just list them all
"},{"location":"design_doc_blueprint/#summary","title":"Summary","text":"A short summary of your project/re-design/component and what problems it addresses in about 3-10 sentences.
"},{"location":"design_doc_blueprint/#state-of-the-feature-as-of-version-optional","title":"State of the Feature as of$VERSION
(optional)","text":"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.
"},{"location":"design_doc_blueprint/#prior-work-optional","title":"Prior work (optional)","text":"Does this feature exist somewhere else and which tradeoffs it made.
If there's no prior work, then use the related work section below.
"},{"location":"design_doc_blueprint/#goals-and-non-goals","title":"Goals and non-goals","text":"Direct goals of the project and features deemed not worth pursuing.
"},{"location":"design_doc_blueprint/#overview","title":"Overview","text":"A detailed overview of the project and the improvements it brings.
"},{"location":"design_doc_blueprint/#detailed-design","title":"Detailed Design","text":"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.
"},{"location":"design_doc_blueprint/#alternatives-considered-optional","title":"Alternatives considered (optional)","text":"Other alternatives to your suggested approach, and why they fall short.
"},{"location":"design_doc_blueprint/#issues-addressed-optional","title":"Issues addressed (optional)","text":"A list of issues which are addressed by this design.
"},{"location":"design_doc_blueprint/#related-work-optional","title":"Related Work (optional)","text":"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.
"},{"location":"design_doc_blueprint/#future-possibilities","title":"Future Possibilities","text":"The section for things which could be added to it or deemed out of scope during the discussion.
"},{"location":"design_docs/","title":"Jujutsu Design Docs","text":"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 the native backend or the server component for Jujutsu, you'll need to go through this process.
"},{"location":"design_docs/#process","title":"Process","text":"docs/design
, named after your improvement or project.You can find the base template of a new Design Doc here.
"},{"location":"filesets/","title":"Filesets","text":"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.
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'
The following patterns are supported:
\"path\"
, path
(the quotes are optional), or cwd:\"path\"
: Matches cwd-relative path prefix (file or files under directory recursively.)cwd-file:\"path\"
or file:\"path\"
: Matches cwd-relative file (or exact) path.cwd-glob:\"pattern\"
or glob:\"pattern\"
: Matches file paths with cwd-relative Unix-style shell wildcard pattern
. For example, glob:\"*.c\"
will match all .c
files in the current working directory non-recursively.root:\"path\"
: Matches workspace-relative path prefix (file or files under directory recursively.)root-file:\"path\"
: Matches workspace-relative file (or exact) path.root-glob:\"pattern\"
: Matches file paths with workspace-relative Unix-style shell wildcard pattern
.The following operators are supported. x
and y
below can be any fileset expressions.
~x
: Matches everything but x
.x & y
: Matches both x
and y
.x ~ y
: Matches x
but not y
.x | y
: Matches either x
or y
(or both).(listed in order of binding strengths)
You can use parentheses to control evaluation order, such as (x & y) | z
or x & (y | z)
.
You can also specify patterns by using functions.
all()
: Matches everything.none()
: Matches nothing.Show diff excluding Cargo.lock
.
jj diff '~Cargo.lock'\n
List files in src
excluding Rust sources.
jj file list 'src ~ glob:\"**/*.rs\"'\n
Split a revision in two, putting foo
into the second commit.
jj split '~foo'\n
"},{"location":"git-comparison/","title":"Comparison with Git","text":""},{"location":"git-comparison/#introduction","title":"Introduction","text":"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.
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 further down explaining how to achieve various use cases.
HEAD
and the working copy, so workflows that depend on it can be modeled using proper commits instead. Jujutsu has excellent support for moving changes between commits. Details.No 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.
jj rebase
), all its descendants commits will automatically be rebased on top. Branches pointing to it will also get updated, and so will the working copy if it points to any of the rebased commits.main
branch, you'll get a bookmark by that name in your local repo. If you then move it and push back to the remote, the main
branch on the remote will be updated. Details.git rebase --root
, git checkout --orphan
).Git's \"index\" has multiple roles. One role is as a cache of file system information. Jujutsu has something similar. Unfortunately, Git exposes the index to the user, which makes the CLI unnecessarily complicated (learning what the different flavors of git reset
do, especially when combined with commits and/or paths, usually takes a while). Jujutsu, like Mercurial, doesn't make that mistake.
As a Git power-user, you may think that you need the power of the index to commit only part of the working copy. However, Jujutsu provides commands for more directly achieving most use cases you're used to using Git's index for. For example, to create a commit from part of the changes in the working copy, you might be used to using git add -p; git commit
. With Jujutsu, you'd instead use jj split
to split the working-copy commit into two commits. To add more changes into the parent commit, which you might normally use git add -p; git commit --amend
for, you can instead use jj squash -i
to choose which changes to move into the parent commit, or jj squash <file>
to move a specific file.
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/amend -r <revision>
will move the diff from that revision into its parent.
jj git init [--colocate]
git init
Clone an existing repo jj git clone <source> <destination> [--remote <remote name>]
(there is no support for cloning non-Git repos yet) git clone <source> <destination> [--origin <remote name>]
Update the local repo with all bookmarks/branches from a remote jj git fetch [--remote <remote>]
(there is no support for fetching into non-Git repos yet) git fetch [<remote>]
Update a remote repo with all bookmarks/branches from the local repo jj git push --all [--remote <remote>]
(there is no support for pushing from non-Git repos yet) git push --all [<remote>]
Update a remote repo with a single bookmark from the local repo jj git push --bookmark <bookmark name> [--remote <remote>]
(there is no support for pushing from non-Git repos yet) git push <remote> <bookmark name>
Add a remote target to the repo jj git remote add <remote> <url>
git remote add <remote> <url>
Show summary of current work and repo status jj st
git status
Show diff of the current change jj diff
git diff HEAD
Show diff of another change jj diff -r <revision>
git diff <revision>^ <revision>
Show diff from another change to the current change jj diff --from <revision>
git diff <revision>
Show diff from change A to change B jj diff --from A --to B
git diff A B
Show description and diff of a change jj show <revision>
git show <revision>
Add a file to the current change touch filename
touch filename; git add filename
Remove a file from the current change rm filename
git rm filename
Modify a file in the current change echo stuff >> filename
echo stuff >> filename
Finish work on the current change and start a new change jj commit
git commit -a
See log of ancestors of the current commit jj log -r ::@
git log --oneline --graph --decorate
See log of all reachable commits jj log -r 'all()'
or jj log -r ::
git log --oneline --graph --decorate --branches
Show log of commits not on the main bookmark jj log
(TODO) List versioned files in the working copy jj file list
git ls-files --cached
Search among files versioned in the repository grep foo $(jj file list)
, or rg --no-require-git foo
git grep foo
Abandon the current change and start a new change jj abandon
git reset --hard
(cannot be undone) Make the current change empty jj restore
git reset --hard
(same as abandoning a change since Git has no concept of a \"change\") Abandon the parent of the working copy, but keep its diff in the working copy jj squash --from @-
git reset --soft HEAD~
Discard working copy changes in some files jj restore <paths>...
git restore <paths>...
or git checkout HEAD -- <paths>...
Edit description (commit message) of the current change jj describe
Not supported Edit description (commit message) of the previous change jj describe @-
git commit --amend --only
Temporarily put away the current change jj new @-
(the old working-copy commit remains as a sibling commit) (the old working-copy commit X can be restored with jj edit X
) git stash
Start working on a new change based on the <main> bookmark/branch jj new main
git switch -c topic main
or git checkout -b topic main
(may need to stash or commit first) Move bookmark/branch A onto bookmark/branch B jj rebase -b A -d B
git rebase B A
(may need to rebase other descendant branches separately) Move change A and its descendants onto change B jj rebase -s A -d B
git rebase --onto B A^ <some descendant bookmark>
(may need to rebase other descendant bookmarks separately) Reorder changes from A-B-C-D to A-C-B-D jj rebase -r C --before B
git rebase -i A
Move the diff in the current change into the parent change jj squash/amend
git commit --amend -a
Interactively move part of the diff in the current change into the parent change jj squash/amend -i
git add -p; git commit --amend
Move the diff in the working copy into an ancestor jj squash --into X
git commit --fixup=X; git rebase -i --autosquash X^
Interactively move part of the diff in an arbitrary change to another arbitrary change jj squash -i --from X --into Y
Not supported Interactively split the changes in the working copy in two jj split
git commit -p
Interactively split an arbitrary change in two jj split -r <revision>
Not supported (can be emulated with the \"edit\" action in git rebase -i
) Interactively edit the diff in a given change jj diffedit -r <revision>
Not supported (can be emulated with the \"edit\" action in git rebase -i
) Resolve conflicts and continue interrupted operation echo resolved > filename; jj squash/amend
(operations don't get interrupted, so no need to continue) echo resolved > filename; git add filename; git rebase/merge/cherry-pick --continue
Create a copy of a commit on top of another commit jj duplicate <source>; jj rebase -r <duplicate commit> -d <destination>
(there's no single command for it yet) git co <destination>; git cherry-pick <source>
Find the root of the working copy (or check if in a repo) jj workspace root
git rev-parse --show-toplevel
List bookmarks/branches jj bookmark list
or jj b l
for short git branch
Create a bookmark/branch jj bookmark create <name> -r <revision>
git branch <name> <revision>
Move a bookmark/branch forward jj bookmark move <name> --to <revision>
or jj b m <name> --to <revision>
for short git branch -f <name> <revision>
Move a bookmark/branch backward or sideways jj bookmark move <name> --to <revision> --allow-backwards
git branch -f <name> <revision>
Delete a bookmark/branch jj bookmark delete <name>
git branch --delete <name>
See log of operations performed on the repo jj op log
Not supported Undo an earlier operation jj [op] undo <operation ID>
(jj undo
is an alias for jj op undo
) Not supported Create a commit that cancels out a previous commit jj backout -r <revision>
git revert <revision>
"},{"location":"git-compatibility/","title":"Git compatibility","text":"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).
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.
~/.gitconfig
) that's respected is the following. Feel free to file a bug if you miss any particular configuration options.[remote \"<name>\"]
).core.excludesFile
ssh-agent
, a password-less key ( only ~/.ssh/id_rsa
, ~/.ssh/id_ed25519
or ~/.ssh/id_ed25519_sk
), or a credential.helper
..gitignore
files are supported. So are ignores in .git/info/exclude
or configured via Git's core.excludesfile
config. The .gitignore
support uses a native implementation, so please report a bug if you notice any difference compared to git
.eol
attribute.jj diff
will show a diff from the Git HEAD to the working copy. There are ways of fulfilling your use cases without a staging area.git gc
in the Git repo, but it's not tested, so it's probably a good idea to make a backup of the whole workspace first. There's no garbage collection and repacking of Jujutsu's own data structures yet, however.jj git init --git-repo=<path>
to create a repo backed by a bare Git repo.jj workspace
family of commands.jj sparse
command.To create an empty repo using the Git backend, use jj init --git <name>
. Since the command creates a Jujutsu repo, it will have a .jj/
directory. The underlying Git repo will be inside of that directory (currently in .jj/repo/store/git/
).
To create a Jujutsu repo backed by a Git repo you already have on disk, use jj git init --git-repo=<path to Git repo> <name>
. The repo will work similar to a Git worktree, meaning that the working copies files and the record of the working-copy commit will be separate, but the commits will be accessible in both repos. Use jj git import
to update the Jujutsu repo with changes made in the Git repo. Use jj git export
to update the Git repo with changes made in the Jujutsu repo.
To create a Jujutsu repo from a remote Git URL, use jj git clone <URL> [<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.
A \"co-located\" Jujutsu repo is a hybrid Jujutsu/Git repo. These can be created if you initialize the Jujutsu repo in an existing Git repo by running jj git init --colocate
or with jj git clone --colocate
. The Git repo and the Jujutsu repo 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 repo 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.
There are a few downsides to this mode of operation. Generally, using co-located repos may require you to deal with more involved Jujutsu and Git concepts.
Interleaving jj
and git
commands increases the chance of confusing branch conflicts or conflicted (AKA divergent) change ids. These never lose data, but can be annoying.
Such interleaving can happen unknowingly. For example, some IDEs can cause it because they automatically run git fetch
in the background from time to time.
In co-located repos with a very large number of branches or other refs, jj
commands can get noticeably slower because of the automatic jj git import
executed on each command. This can be mitigated by occasionally running jj util gc
to speed up the import (that command includes packing the Git refs).
Git tools will have trouble with revisions that contain conflicted files. While jj
renders these files with conflict markers in the working copy, they are stored in a non-human-readable fashion inside the repo. Git tools will often see this non-human-readable representation.
When a jj
branch is conflicted, the position of the branch in the Git repo will disagree with one or more of the conflicted positions. The state of that branch in git will be labeled as though it belongs to a remote named \"git\", e.g. branch@git
.
Jujutsu will ignore Git's staging area. It will not understand merge conflicts as Git represents them, unfinished git rebase
states, as well as other less common states a Git repository can be in.
Colocated repositories are less resilient to concurrency issues if you share the repo using an NFS filesystem or Dropbox. In general, such use of Jujutsu is not currently thoroughly tested.
There may still be bugs when interleaving mutating jj
and git
commands, usually having to do with a branch pointer ending up in the wrong place. We are working on the known ones, and are not aware of any major ones. Please report any new ones you find, or if any of the known bugs are less minor than they appear.
A Jujutsu repo backed by a Git repo has a full Git repo inside, so it is technically possible (though not officially supported) to convert it into a co-located repo like so:
# Move the Git repo\nmv .jj/repo/store/git .git\n# Tell jj where to find it\necho -n '../../../.git' > .jj/repo/store/git_target\n# Ignore the .jj directory in Git\necho '/*' > .jj/.gitignore\n# Make the Git repository non-bare and set HEAD\ngit config --unset core.bare\njj new @-\n
We may officially support this in the future. If you try this, we would appreciate feedback and bug reports.
"},{"location":"git-compatibility/#branches","title":"Branches","text":"TODO: Describe how branches are mapped
"},{"location":"git-compatibility/#format-mapping-details","title":"Format mapping details","text":"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.
Commit metadata that cannot be represented in Git commits (such as the Change ID and information about conflicts) is stored outside of the Git repo (currently in .jj/store/extra/
).
Commits with conflicts cannot be represented in Git. They appear in the Git commit as 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 the Git-external storage mentioned in the paragraph above. 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 those directories, making it look like they replaced all the other paths in your repo. You will probably want to run jj abandon
to get back to the state with the unresolved conflicts.
This guide assumes a basic understanding of either Git or Mercurial.
"},{"location":"github/#set-up-an-ssh-key","title":"Set up an SSH key","text":"As of October 2023 it's recommended to set up an SSH key to work with GitHub projects. See GitHub's Tutorial. This restriction may be lifted in the future, see issue #469 for more information and progress on authenticated HTTP.
"},{"location":"github/#basic-workflow","title":"Basic workflow","text":"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.
"},{"location":"github/#using-a-generated-bookmark-name","title":"Using a generated bookmark name","text":"In this example we're letting Jujutsu auto-create a bookmark.
# Start a new commit off of the default bookmark.\n$ jj new main\n# Refactor some files, then add a description and start a new commit\n$ jj commit -m 'refactor(foo): restructure foo()'\n# Add a feature, then add a description and start a new commit\n$ jj commit -m 'feat(bar): add support for bar'\n# Let Jujutsu generate a bookmark name and push that to GitHub. Note that we\n# push the working-copy commit's *parent* because the working-copy commit\n# itself is empty.\n$ jj git push -c @-\n
"},{"location":"github/#using-a-named-bookmark","title":"Using a named bookmark","text":"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.\n$ jj new main\n# Refactor some files, then add a description and start a new commit\n$ jj commit -m 'refactor(foo): restructure foo()'\n# Add a feature, then add a description and start a new commit\n$ jj commit -m 'feat(bar): add support for bar'\n# Create a bookmark so we can push it to GitHub. Note that we created the bookmark\n# on the working-copy commit's *parent* because the working copy itself is empty.\n$ jj bookmark create bar -r @- # `bar` now contains the previous two commits.\n# Push the bookmark to GitHub (pushes only `bar`)\n$ jj git push\n
While it's possible to create a bookmark in advance and commit on top of it in a Git-like manner, you will then need to move the bookmark manually when you create a new commits. Unlike Git, Jujutsu will not do it automatically.
"},{"location":"github/#updating-the-repository","title":"Updating the repository","text":"As of October 2023, Jujutsu has no equivalent to a git pull
command (see issue #1039). Until such a command is added, you need to use jj git fetch
followed by a jj rebase -d $main_bookmark
to update your changes.
After doing jj git init --colocate
, Git will be in a detached HEAD state, which is unusual, as Git mainly works with bookmarks. In a co-located repository, 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\n$ # Do some more work.\n$ jj commit -m \"Update tutorial\"\n# Create a bookmark on the working-copy commit's parent\n$ jj bookmark create doc-update -r @-\n$ jj git push\n
"},{"location":"github/#working-in-a-jujutsu-repository","title":"Working in a Jujutsu repository","text":"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\n$ jj commit\n$ # Push change \"mw\", letting Jujutsu automatically create a bookmark called\n$ # \"push-mwmpwkwknuz\"\n$ jj git push --change mw\n
"},{"location":"github/#addressing-review-comments","title":"Addressing review comments","text":"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 bookmark1. Some projects (such as Jujutsu and LLVM) instead prefer that you keep your commits clean by rewriting them and then force-pushing2.
"},{"location":"github/#adding-new-commits","title":"Adding new commits","text":"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.\n$ jj new your-feature\n$ # Address the comments by updating the code. Then review the changes.\n$ jj diff\n$ # Give the fix a description and create a new working-copy on top.\n$ jj commit -m 'address pr comments'\n$ # Update the bookmark to point to the new commit.\n$ jj bookmark set your-feature -r @-\n$ # Push it to your remote\n$ jj git push\n
Notably, the above workflow creates a new commit for you. The same can be achieved without creating a new commit.
Warning We strongly suggest to jj new
after the example below, as all further edits still get amended to the previous commit.
$ # Create a new commit on top of the `your-feature` bookmark from above.\n$ jj new your-feature\n$ # Address the comments by updating the code. Then review the changes.\n$ jj diff\n$ # Give the fix a description.\n$ jj describe -m 'address pr comments'\n$ # Update the bookmark to point to the current commit.\n$ jj bookmark set your-feature -r @\n$ # Push it to your remote\n$ jj git push\n
"},{"location":"github/#rewriting-commits","title":"Rewriting commits","text":"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`,\n$ # as reviewers requested a fix there.\n$ jj new your-feature- # NOTE: the trailing hyphen is not a typo!\n$ # Address the comments by updating the code. Then review the changes.\n$ jj diff\n$ # Squash the changes into the parent commit\n$ jj squash\n$ # Push the updated bookmark to the remote. Jujutsu automatically makes it a\n$ # force push\n$ jj git push --bookmark your-feature\n
The hyphen after your-feature
comes from the revset syntax.
By default, jj git clone
imports the default remote bookmark (which is usually main
or master
), but jj git fetch
doesn't import new remote bookmarks to local bookmarks. This means that if you want to iterate or test another contributor's bookmark, you'll need to do jj new <bookmark>@<remote>
onto it.
If you want to import all remote bookmarks including inactive ones, set git.auto-local-bookmark = true
in the config file. Then you can specify a contributor's bookmark as jj new <bookmark>
instead of jj new <bookmark>@<remote>
.
You can find more information on that setting here.
"},{"location":"github/#using-github-cli","title":"Using GitHub CLI","text":"GitHub CLI will have trouble finding the proper Git repository path in jj repos that aren't co-located (see issue #1008). You can configure the $GIT_DIR
environment variable to point it to the right path:
$ GIT_DIR=.jj/repo/store/git gh issue list\n
You can make that automatic by installing direnv and defining hooks in a .envrc
file in the repository root to configure $GIT_DIR
. Just add this line into .envrc
:
export GIT_DIR=$PWD/.jj/repo/store/git\n
and run direnv allow
to approve it for direnv to run. Then GitHub CLI will work automatically even in repos that aren't co-located so you can execute commands like gh issue list
normally.
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())'\n
Log all revisions that you authored, across all bookmarks that aren't on any remote:
$ jj log -r 'mine() & bookmarks() & ~remote_bookmarks()'\n
Log all remote bookmarks that you authored or committed to:
$ jj log -r 'remote_bookmarks() & (mine() | committer(your@email.com))'\n
Log all descendants of the current working copy that aren't on any remote:
$ jj log -r '::@ & ~remote_bookmarks()'\n
"},{"location":"github/#merge-conflicts","title":"Merge conflicts","text":"For a detailed overview, how Jujutsu handles conflicts, revisit the tutorial.
"},{"location":"github/#using-several-remotes","title":"Using several remotes","text":"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\n$ cd repo\n$ jj git remote add origin git@github.com:your-org/your-repo-fork\n
This will automatically setup your repository to track the main bookmark from the upstream repository, typically main@upstream
or master@upstream
.
You might want to jj git fetch
from \"upstream\" and to jj git push
to \"origin\". You can configure the default remotes to fetch from and push to in your configuration file (for example, .jj/repo/config.toml
):
[git]\nfetch = \"upstream\"\npush = \"origin\"\n
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]\nfetch = [\"upstream\", \"origin\"]\npush = \"origin\"\n
This is a GitHub-style review, as GitHub currently only is able to compare bookmarks.\u00a0\u21a9
If you're wondering why we prefer clean commits in this project, see e.g. this blog post \u21a9
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.
"},{"location":"glossary/#backend","title":"Backend","text":"A backend is an implementation of the storage layer. There are currently two builtin commit backends: the Git backend and the native backend. The Git backend stores commits in a Git repository. The native backend is used for testing purposes only. Alternative backends could be used, for example, if somebody wanted to use jj with a humongous monorepo (as Google does).
There are also pluggable backends for storing other information than commits, such as the \"operation store backend\" for storing the operation log.
"},{"location":"glossary/#bookmark","title":"Bookmark","text":"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.
"},{"location":"glossary/#branch","title":"Branch","text":"In the context of jj
, the work \"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 co-located repository, each local Git branch corresponds to a jj
bookmark.
A change is a commit as it evolves over time.
"},{"location":"glossary/#change-id","title":"Change ID","text":"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.
For the git backend, Change IDs are currently maintained only locally and not exchanged via push/fetch operations.
"},{"location":"glossary/#commit","title":"Commit","text":"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 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\".
"},{"location":"glossary/#commit-id","title":"Commit ID","text":"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.
When using the Git backend and the backing Git repository's .git/
directory is a sibling of .jj/
, we call the repository \"co-located\". Most tools designed for Git can be easily used on such repositories. jj
and git
commands can be used interchangeably.
See here for details.
"},{"location":"glossary/#conflict","title":"Conflict","text":"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.
"},{"location":"glossary/#divergent-change","title":"Divergent change","text":"A divergent change is a change that has more than one visible commit.
"},{"location":"glossary/#head","title":"Head","text":"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.
See visible commits.
"},{"location":"glossary/#operation","title":"Operation","text":"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.
"},{"location":"glossary/#operation-log","title":"Operation log","text":"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.
"},{"location":"glossary/#repository","title":"Repository","text":"Basically everything under .jj/
, i.e. the full set of operations and commits.
TODO
"},{"location":"glossary/#revision","title":"Revision","text":"A synonym for Commit.
"},{"location":"glossary/#revset","title":"Revset","text":"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.
"},{"location":"glossary/#rewrite","title":"Rewrite","text":"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.
"},{"location":"glossary/#root-commit","title":"Root commit","text":"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.
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.
"},{"location":"glossary/#tracked-bookmarks-and-tracking-bookmarks","title":"Tracked bookmarks and tracking bookmarks","text":"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.
"},{"location":"glossary/#visible-commits","title":"Visible commits","text":"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, visible commits are the \"latest versions\" of a revision with a given change id. A commit that's abandoned or rewritten stops being visible and is labeled as \"hidden\". Such commits are no longer accessible using a change id, but they are still accessible by their commit id.
"},{"location":"glossary/#view","title":"View","text":"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.
"},{"location":"glossary/#workspace","title":"Workspace","text":"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\".
"},{"location":"glossary/#working-copy","title":"Working copy","text":"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\".
"},{"location":"glossary/#working-copy-commit","title":"Working-copy commit","text":"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.
"},{"location":"install-and-setup/","title":"Installation and setup","text":""},{"location":"install-and-setup/#installation","title":"Installation","text":""},{"location":"install-and-setup/#download-pre-built-binaries-for-a-release","title":"Download pre-built binaries for a release","text":"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).
If you'd like to install a prerelease version, you'll need to use one of the options below.
"},{"location":"install-and-setup/#cargo-binstall","title":"Cargo Binstall","text":"If you use cargo-binstall
, you can install the same binaries of the last jj
release from GitHub as follows:
# Will put the jj binary for the latest release in ~/.cargo/bin by default\ncargo binstall --strategies crate-meta-data jj-cli\n
Without the --strategies
option, you may get equivalent binaries that should be compiled from the same source code.
First make sure that you have a Rust version >= 1.76 and that the libssl-dev
, openssl
, pkg-config
, and build-essential
packages are installed by running something like this:
sudo apt-get install libssl-dev openssl pkg-config build-essential\n
Now run either:
# To install the *prerelease* version from the main branch\ncargo install --git https://github.com/martinvonz/jj.git --locked --bin jj jj-cli\n
or:
# To install the latest release\ncargo install --locked --bin jj jj-cli\n
"},{"location":"install-and-setup/#arch-linux","title":"Arch Linux","text":"You can install the jujutsu
package from the official extra repository:
pacman -S jujutsu\n
Or install from the AUR repository with an AUR Helper:
yay -S jujutsu-git\n
"},{"location":"install-and-setup/#nix-os","title":"Nix OS","text":"If you're on Nix OS 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:martinvonz/jj'\n
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\nnix profile install 'github:martinvonz/jj'\n
"},{"location":"install-and-setup/#homebrew","title":"Homebrew","text":"If you use linuxbrew, you can run:
# Installs the latest release\nbrew install jj\n
"},{"location":"install-and-setup/#gentoo-linux","title":"Gentoo Linux","text":"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/jj\n
"},{"location":"install-and-setup/#mac","title":"Mac","text":""},{"location":"install-and-setup/#from-source-vendored-openssl","title":"From Source, Vendored OpenSSL","text":"First make sure that you have a Rust version >= 1.76. You may also need to run:
xcode-select --install\n
Now run either:
# To install the *prerelease* version from the main branch\ncargo install --git https://github.com/martinvonz/jj.git \\\n --features vendored-openssl --locked --bin jj jj-cli\n
or:
# To install the latest release\ncargo install --features vendored-openssl --locked --bin jj jj-cli\n
"},{"location":"install-and-setup/#from-source-homebrew-openssl","title":"From Source, Homebrew OpenSSL","text":"First make sure that you have a Rust version >= 1.76. You will also need Homebrew installed. You may then need to run some or all of these:
xcode-select --install\nbrew install openssl\nbrew install pkg-config\nexport PKG_CONFIG_PATH=\"$(brew --prefix)/opt/openssl@3/lib/pkgconfig\"\n
Now run either:
# To install the *prerelease* version from the main branch\ncargo install --git https://github.com/martinvonz/jj.git --locked --bin jj jj-cli\n
or:
# To install the latest release\ncargo install --locked --bin jj jj-cli\n
"},{"location":"install-and-setup/#homebrew_1","title":"Homebrew","text":"If you use Homebrew, you can run:
# Installs the latest release\nbrew install jj\n
"},{"location":"install-and-setup/#macports","title":"MacPorts","text":"You can also install jj
via the MacPorts jujutsu
port:
# Installs the latest release\nsudo port install jujutsu\n
"},{"location":"install-and-setup/#windows","title":"Windows","text":"First make sure that you have a Rust version >= 1.76. Now run either:
# To install the *prerelease* version from the main branch\ncargo install --git https://github.com/martinvonz/jj.git --locked --bin jj jj-cli --features vendored-openssl\n
or:
# To install the latest release\ncargo install --locked --bin jj jj-cli --features vendored-openssl\n
"},{"location":"install-and-setup/#initial-configuration","title":"Initial configuration","text":"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\"\n$ jj config set --user user.email \"martinvonz@google.com\"\n
"},{"location":"install-and-setup/#command-line-completion","title":"Command-line completion","text":"To set up command-line completion, source the output of jj util completion bash/zsh/fish
. Exactly how to source it depends on your shell.
source <(jj util completion bash)\n
"},{"location":"install-and-setup/#zsh","title":"Zsh","text":"autoload -U compinit\ncompinit\nsource <(jj util completion zsh)\n
"},{"location":"install-and-setup/#fish","title":"Fish","text":"jj util completion fish | source\n
"},{"location":"install-and-setup/#nushell","title":"Nushell","text":"jj util completion nushell | save completions-jj.nu\nuse completions-jj.nu * # Or `source completions-jj.nu`\n
"},{"location":"install-and-setup/#xonsh","title":"Xonsh","text":"source-bash $(jj util completion)\n
"},{"location":"operation-log/","title":"Operation log","text":""},{"location":"operation-log/#introduction","title":"Introduction","text":"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 an operation (jj [op] undo
), which doesn't need to be the most recent one. It also lets you restore the entire repo to the way it looked at an earlier point (jj op restore
).
When referring to operations, you can use @
to represent the current operation.
The following operators are supported:
x-
: Parents of x
(e.g. @-
)x+
: Children of x
One benefit of the operation log (and the reason for its creation) is that it allows lock-free concurrency -- you can run concurrent jj
commands without corrupting the repo, even if you run the commands on different machines that access the repo via a distributed file system (as long as the file system guarantees that a write is only visible once previous writes are visible). When you run a jj
command, it will start by loading the repo at the latest operation. It will not see any changes written by concurrent commands. If there are conflicts, you will be informed of them by subsequent jj st
and/or jj log
commands.
As an example, let's say you had started editing the description of a change and then also update the contents of the change (maybe because you had forgotten the editor). When you eventually close your editor, the command will succeed and e.g. jj log
will indicate that the change has diverged.
The top-level --at-operation/--at-op
option allows you to load the repo at a specific operation. This can be useful for understanding how your repo got into the current state. It can be even more useful for understanding why someone else's repo got into its current state.
When you use --at-op
, the automatic snapshotting of the working copy will not take place. When referring to a revision with the @
symbol (as many commands do by default), that will resolve to the working-copy commit recorded in the operation's view (which is actually how it always works -- it's just the snapshotting that's skipped with --at-op
).
As a top-level option, --at-op
can be passed to any command. However, you will typically only want to run read-only commands. For example, jj log
, jj st
, and jj diff
all make sense. It's still possible to run e.g. jj --at-op=<some operation ID> describe
. That's equivalent to having started jj describe
back when the specified operation was the most recent operation and then let it run until now (which can be done for that particular command by not closing the editor). There's practically no good reason to do that other than to simulate concurrent commands.
Similar tools:
git move
). Under heavy development and quickly gaining new features.Jujutsu supports a functional language for selecting a set of revisions. Expressions in this language are called \"revsets\" (the idea comes from Mercurial). The language consists of symbols, operators, and functions.
Most jj
commands accept a revset (or multiple). Many commands, such as jj diff -r <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.
Most revsets search only the visible commits. Other commits are only included if you explicitly mention them (e.g. by commit ID or a Git ref pointing to them).
"},{"location":"revsets/#symbols","title":"Symbols","text":"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 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 all visible commits with that change ID (there is typically only one visible commit with a given change ID). A unique prefix of the full change ID can also be used. It is an error to use a non-unique prefix.
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-\"'
.
Jujutsu attempts to resolve a symbol in the following order:
The following operators are supported. x
and y
below can be any revset, not only symbols.
x-
: Parents of x
, can be empty.x+
: Children of x
, can be empty.x::
: Descendants of x
, including the commits in x
itself. Shorthand for x::visible_heads()
.x..
: Revisions that are not ancestors of x
. Shorthand for x..visible_heads()
.::x
: Ancestors of x
, including the commits in x
itself. Shorthand for root()::x
...x
: Ancestors of x
, including the commits in x
itself, but excluding the root commit. Shorthand for root()..x
. Equivalent to ::x ~ root()
.x::y
: Descendants of x
that are also ancestors of y
. Equivalent to x:: & ::y
. This is what git log
calls --ancestry-path x..y
.x..y
: Ancestors of y
that are not also ancestors of x
. Equivalent to ::y ~ ::x
. This is what git log
calls x..y
(i.e. the same as we call it).::
: All visible commits in the repo. Shorthand for root()::visible_heads()
. Equivalent to all()
...
: All visible commits in the repo, but excluding the root commit. Shorthand for root()..visible_heads()
. Equivalent to ~root()
.~x
: Revisions that are not in x
.x & y
: Revisions that are in both x
and y
.x ~ y
: Revisions that are in x
but not in y
.x | y
: Revisions that are in either x
or y
(or both).(listed in order of binding strengths)
You can use parentheses to control evaluation order, such as (x & y) | z
or x & (y | z)
.
Given this history:
D\n|\\\n| o C\n| |\no | B\n|/\no A\n|\no root()\n
Operator x-
D-
\u21d2 {C,B}
B-
\u21d2 {A}
A-
\u21d2 {root()}
root()-
\u21d2 {}
(empty set)none()-
\u21d2 {}
(empty set)(D|A)-
\u21d2 {C,B,root()}
(C|B)-
\u21d2 {A}
Operator x+
D+
\u21d2 {}
(empty set)B+
\u21d2 {D}
A+
\u21d2 {B,C}
root()+
\u21d2 {A}
none()+
\u21d2 {}
(empty set)(C|B)+
\u21d2 {D}
(B|root())+
\u21d2 {D,A}
Operator x::
D::
\u21d2 {D}
B::
\u21d2 {D,B}
A::
\u21d2 {D,C,B,A}
root()::
\u21d2 {D,C,B,A,root()}
none()::
\u21d2 {}
(empty set)(C|B)::
\u21d2 {D,C,B}
Operator x..
D..
\u21d2 {}
(empty set)B..
\u21d2 {D,C}
(note that, unlike B::
, this includes C
)A..
\u21d2 {D,C,B}
root()..
\u21d2 {D,C,B,A}
none()..
\u21d2 {D,C,B,A,root()}
(C|B)..
\u21d2 {D}
Operator ::x
::D
\u21d2 {D,C,B,A,root()}
::B
\u21d2 {B,A,root()}
::A
\u21d2 {A,root()}
::root()
\u21d2 {root()}
::none()
\u21d2 {}
(empty set)::(C|B)
\u21d2 {C,B,A,root()}
Operator ..x
..D
\u21d2 {D,C,B,A}
..B
\u21d2 {B,A}
..A
\u21d2 {A}
..root()
\u21d2 {}
(empty set)..none()
\u21d2 {}
(empty set)..(C|B)
\u21d2 {C,B,A}
Operator x::y
D::D
\u21d2 {D}
B::D
\u21d2 {D,B}
(note that, unlike B..D
, this includes B
and excludes C
)A::D
\u21d2 {D,C,B,A}
root()::D
\u21d2 {D,C,B,A,root()}
none()::D
\u21d2 {}
(empty set)D::B
\u21d2 {}
(empty set)(C|B)::(C|B)
\u21d2 {C,B}
Operator x..y
D..D
\u21d2 {}
(empty set)B..D
\u21d2 {D,C}
(note that, unlike B::D
, this includes C
and excludes B
)A..D
\u21d2 {D,C,B}
root()..D
\u21d2 {D,C,B,A}
none()..D
\u21d2 {D,C,B,A,root()}
D..B
\u21d2 {}
(empty set)(C|B)..(C|B)
\u21d2 {}
(empty set)You can also specify revisions by using functions. Some functions take other revsets (expressions) as arguments.
parents(x)
: Same as x-
.
children(x)
: Same as x+
.
ancestors(x[, depth])
: ancestors(x)
is the same as ::x
. ancestors(x, depth)
returns the ancestors of x
limited to the given depth
.
descendants(x[, depth])
: descendants(x)
is the same as x::
. descendants(x, depth)
returns the descendants of x
limited to the given depth
.
reachable(srcs, domain)
: All commits reachable from srcs
within domain
, traversing all parent and child edges.
connected(x)
: Same as x::x
. Useful when x
includes several commits.
all()
: All visible commits in the repo.
none()
: No commits. This function is rarely useful; it is provided for completeness.
bookmarks([pattern])
: All local bookmark targets. If pattern
is specified, this selects the bookmarks whose name match the given string pattern. For example, bookmarks(push)
would match the bookmarks push-123
and repushed
but not the bookmark main
. If a bookmark is in a conflicted state, all its possible targets are included.
remote_bookmarks([bookmark_pattern[, [remote=]remote_pattern]])
: All remote bookmarks targets across all remotes. If just the bookmark_pattern
is specified, the bookmarks whose names match the given string pattern across all remotes are selected. If both bookmark_pattern
and remote_pattern
are specified, the selection is further restricted to just the remotes whose names match remote_pattern
.
For example, remote_bookmarks(push, ri)
would match the bookmarks push-123@origin
and repushed@private
but not push-123@upstream
or main@origin
or main@upstream
. If a bookmark is in a conflicted state, all its possible targets are included.
While Git-tracking bookmarks can be selected by <name>@git
, these bookmarks aren't included in remote_bookmarks()
.
tracked_remote_bookmarks([bookmark_pattern[, [remote=]remote_pattern]])
: All targets of tracked remote bookmarks. Supports the same optional arguments as remote_bookmarks()
.
untracked_remote_bookmarks([bookmark_pattern[, [remote=]remote_pattern]])
: All targets of untracked remote bookmarks. Supports the same optional arguments as remote_bookmarks()
.
tags()
: All tag targets. If a tag is in a conflicted state, all its possible targets are included.
git_refs()
: All Git ref targets as of the last import. If a Git ref is in a conflicted state, all its possible targets are included.
git_head()
: The Git HEAD
target as of the last import.
visible_heads()
: All visible heads (same as heads(all())
).
root()
: The virtual commit that is the oldest ancestor of all other commits.
heads(x)
: Commits in x
that are not ancestors of other commits in x
. Note that this is different from Mercurial's heads(x)
function, which is equivalent to x ~ x-
.
roots(x)
: Commits in x
that are not descendants of other commits in x
. Note that this is different from Mercurial's roots(x)
function, which is equivalent to x ~ x+
.
latest(x[, count])
: Latest count
commits in x
, based on committer timestamp. The default count
is 1.
merges()
: Merge commits.
description(pattern)
: Commits that have a description matching the given string pattern.
author(pattern)
: Commits with the author's name or email matching the given string pattern.
mine()
: Commits where the author's email matches the email of the current user.
committer(pattern)
: Commits with the committer's name or email matching the given string pattern.
author_date(pattern)
: Commits with author dates matching the specified date pattern.
committer_date(pattern)
: Commits with committer dates matching the specified date pattern.
empty()
: Commits modifying no files. This also includes merges()
without user modifications and root()
.
files(expression)
: Commits modifying paths matching the given fileset expression.
Paths are relative to the directory jj
was invoked from. A directory name will match all files in that directory and its subdirectories.
For example, files(foo)
will match files foo
, foo/bar
, foo/bar/baz
. It will not match foobar
or bar/foo
.
Some file patterns might need quoting because the expression
must also be parsable as a revset. For example, .
has to be quoted in files(\".\")
.
diff_contains(text[, files])
: Commits containing diffs matching the given text
pattern line by line.
The search paths can be narrowed by the files
expression. All modified files are scanned by default, but it is likely to change in future version to respect the command line path arguments.
For example, diff_contains(\"TODO\", \"src\")
will search revisions where \"TODO\" is added to or removed from files under \"src\".
conflicts()
: Commits with conflicts.
present(x)
: Same as x
, but evaluated to none()
if any of the commits in x
doesn't exist (e.g. is an unknown bookmark name.)
coalesce(revsets...)
: Commits in the first revset in the list of revsets
which does not evaluate to none()
. If all revsets evaluate to none()
, then the result of coalesce
will also be none()
.
working_copies()
: The working copy commits across all the workspaces.
at_operation(op, x)
: Evaluates x
at the specified operation. For example, at_operation(@-, visible_heads())
will return all heads which were visible at the previous operation.
Given this history:
E\n|\n| D\n|/|\n| o C\n| |\no | B\n|/\no A\n|\no root()\n
function reachable()
reachable(E, A..)
\u21d2 {E,D,C,B}
reachable(D, A..)
\u21d2 {E,D,C,B}
reachable(C, A..)
\u21d2 {E,D,C,B}
reachable(B, A..)
\u21d2 {E,D,C,B}
reachable(A, A..)
\u21d2 {}
(empty set)function connected()
connected(E|A)
\u21d2 {E,B,A}
connected(D|A)
\u21d2 {D,C,B,A}
connected(A)
\u21d2 {A}
function heads()
heads(E|D)
\u21d2 {E,D}
heads(E|C)
\u21d2 {E,C}
heads(E|B)
\u21d2 {E}
heads(E|A)
\u21d2 {E}
heads(A)
\u21d2 {A}
function roots()
roots(E|D)
\u21d2 {E,D}
roots(E|C)
\u21d2 {E,C}
roots(E|B)
\u21d2 {B}
roots(E|A)
\u21d2 {A}
roots(A)
\u21d2 {A}
Functions that perform string matching support the following pattern syntax:
\"string\"
, or string
(the quotes are optional), or substring:\"string\"
: Matches strings that contain string
.exact:\"string\"
: Matches strings exactly equal to string
.glob:\"pattern\"
: Matches strings with Unix-style shell wildcard pattern
.regex:\"pattern\"
: Matches substrings with regular expression pattern
.You can append -i
after the kind to match case\u2010insensitively (e.g. glob-i:\"fix*jpeg*\"
).
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:
New symbols and functions 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]\n'HEAD' = '@-'\n'user()' = 'user(\"me@example.org\")'\n'user(x)' = 'author(x) | committer(x)'\n
"},{"location":"revsets/#built-in-aliases","title":"Built-in Aliases","text":"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 trunk bookmark of the remote named origin
or upstream
. The bookmarks main
, master
, and trunk
are tried. If more than one potential trunk commit exists, the newest one is chosen. If none of the bookmarks exist, the revset evaluates to root()
.
When working with an existing Git repository (via jj git clone
or jj git init
), trunk()
will be overridden at the repository level to the default bookmark of the remote origin
.
You can override this as appropriate. If you do, make sure it always resolves to exactly one commit. For example:
[revset-aliases]\n'trunk()' = 'your-bookmark@your-remote'\n
builtin_immutable_heads()
: Resolves to present(trunk()) | tags() | untracked_remote_bookmarks()
. It is used as the default definition for immutable_heads()
below. it is not recommended to redefined this alias. Prefer to redefine immutable_heads()
instead.
immutable_heads()
: Resolves to present(trunk()) | tags() | untracked_remote_bookmarks()
by default. It is actually defined as builtin_immutable_heads()
, and can be overridden as required. See here for details.
immutable()
: The set of commits that jj
treats as immutable. This is equivalent to ::(immutable_heads() | root())
. It is not recommended to redefine this alias. Note that modifying this will not change whether a commit is immutable. To do that, edit immutable_heads()
.
mutable()
: The set of commits that jj
treats as mutable. This is equivalent to ~immutable()
. It is not recommended to redefined this alias. Note that modifying this will not change whether a commit is immutable. To do that, edit immutable_heads()
.
all:
modifier","text":"Certain commands (such as jj rebase
) can take multiple revset arguments, and each of these may resolve to one-or-many revisions. By default, jj
will not allow revsets that resolve to more than one revision \u2014 a so-called \"large revset\" \u2014 and will ask you to confirm that you want to proceed by prefixing it with the all:
modifier.
If you set the ui.always-allow-large-revsets
option to true
, jj
will behave as though the all:
modifier was used every time it would matter.
An all:
modifier before a revset expression does not otherwise change its meaning. Strictly speaking, it is not part of the revset language. The notation is similar to the modifiers like glob:
allowed before string patterms.
For example, jj rebase -r w -d xyz+
will rebase w
on top of the child of xyz
as long as xyz
has exactly one child.
If xyz
has more than one child, the all:
modifier is not specified, and ui.always-allow-large-revsets
is false
(the default), jj rebase -r w -d xyz+
will return an error.
If ui.always-allow-large-revsets
was true
, the above command would act as if all:
was set (see the next paragraph).
With the all:
modifier, jj rebase -r w -d all:xyz+
will make w
into a merge commit if xyz
has more than one child. The all:
modifier confirms that the user expected xyz
to have more than one child.
A more useful example: if w
is a merge commit, jj rebase -s w -d all:w- -d xyz
will add xyz
to the list of w
's parents.
Show the parent(s) of the working-copy commit (like git log -1 HEAD
):
jj log -r @-\n
Show all ancestors of the working copy (like plain git log
)
jj log -r ::@\n
Show commits not on any remote bookmark:
jj log -r 'remote_bookmarks()..'\n
Show commits not on origin
(if you have other remotes like fork
):
jj log -r 'remote_bookmarks(remote=origin)..'\n
Show the initial commits in the repo (the ones Git calls \"root commits\"):
jj log -r 'root()+'\n
Show some important commits (like git --simplify-by-decoration
):
jj log -r 'tags() | bookmarks()'\n
Show local commits leading up to the working copy, as well as descendants of those commits:
jj log -r '(remote_bookmarks()..@)::'\n
Show commits authored by \"martinvonz\" and containing the word \"reset\" in the description:
jj log -r 'author(martinvonz) & description(reset)'\n
"},{"location":"roadmap/","title":"Roadmap","text":"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.
"},{"location":"roadmap/#support-for-copies-and-renames","title":"Support for copies and renames","text":"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.
"},{"location":"roadmap/#forge-integrations","title":"Forge integrations","text":"We would like to make it easier to work with various popular forges by providing something like jj github submit
, jj gitlab submit
, and jj gerrit send
. For popular forges, we might include that support by default in the standard jj
binary.
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.
"},{"location":"roadmap/#better-rust-api-for-uis","title":"Better Rust API for UIs","text":"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
.
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.
"},{"location":"roadmap/#open-source-cloud-based-repos-server-and-daemon-process","title":"Open-source cloud-based repos (server and daemon process)","text":"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 of objects it thinks the client might ask for next. In 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.
"},{"location":"roadmap/#virtual-file-system-vfs","title":"Virtual file system (VFS)","text":"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.
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.
"},{"location":"sapling-comparison/","title":"Comparison with Sapling","text":""},{"location":"sapling-comparison/#introduction","title":"Introduction","text":"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:
split
commands, and automatically rebasing descendant commits when you amend a commit.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:
sl shelve
.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:
Undo: jj's undo is powered by the operation log, which records how the repo has changed over time. Sapling has a similar feature with its MetaLog. They seem to provide similar functionality, but jj also exposes the log to the user via jj op log
, so you can tell how far back you want to go back. Sapling has sl debugmetalog
, but that seems to show the history of a single commit, not the whole repo's history. Thanks to jj snapshotting the working copy, it's possible to undo changes to the working copy. For example, if you jj undo
a jj commit
, jj diff
will show the same changes as before jj commit
, but if you sl undo
a sl commit
, the working copy will be clean.
jj
and git
interchangeably in the same repo.blame/annotate
or bisect
commands, and also no copy/rename support. Sapling also has very nice web UI called Interactive Smartlog, which lets you drag and drop commits to rebase them, among other things.sl pr submit --stack
, which lets you push a stack of commits as separate GitHub PRs, including setting the base branch. It only supports GitHub. jj doesn't have any direct integration with GitHub or any other forge. However, it has jj git push --change
for automatically creating branches for specified commits. You have to specify each commit you want to create a branch for by using jj git push --change X --change Y ...
, and you have to manually set up any base branches in GitHub's UI (or GitLab's or ...). On subsequent pushes, you can update all at once by specifying something like jj git push -r main..@
(to push all branches on the current stack of commits from where it forked from main
).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 represent objects of different types; the types are described in a follow-up section. In addition to context-specific keywords, the top-level object can be referenced as self
.
In jj log
/jj evolog
templates, all 0-argument methods of the Commit
type are available as keywords. For example, commit_id
is equivalent to self.commit_id()
.
In jj op log
templates, all 0-argument methods of the Operation
type are available as keywords. For example, current_operation
is equivalent to self.current_operation()
.
The following operators are supported.
x.f()
: Method call.-x
: Negate integer value.!x
: Logical not.x == y
: Logical equal. Operands must be either Boolean
, Integer
, or String
.x && y
: Logical and, short-circuiting.x || y
: Logical or, short-circuiting.x ++ y
: Concatenate x
and y
templates.(listed in order of binding strengths)
"},{"location":"templates/#global-functions","title":"Global functions","text":"The following functions are defined.
fill(width: Integer, content: Template) -> Template
: Fill lines at the given width
.indent(prefix: Template, content: Template) -> Template
: Indent non-empty lines by the given prefix
.pad_start(width: Integer, content: Template[, fill_char: Template])
: Pad (or right-justify) content by adding leading fill characters. The content
shouldn't have newline character.pad_end(width: Integer, content: Template[, fill_char: Template])
: Pad (or left-justify) content by adding trailing fill characters. The content
shouldn't have newline character.truncate_start(width: Integer, content: Template)
: Truncate content
by removing leading characters. The content
shouldn't have newline character.truncate_end(width: Integer, content: Template)
: Truncate content
by removing trailing characters. The content
shouldn't have newline character.label(label: Template, content: Template) -> Template
: Apply label to the content. The label
is evaluated as a space-separated string.raw_escape_sequence(content: Template) -> Template
: Preserves any escape sequences in content
(i.e., bypasses sanitization) and strips labels. Note: This function is intended for escape sequences and as such, its output is expected to be invisible / of no display width. Outputting content with nonzero display width may break wrapping, indentation etc.if(condition: Boolean, then: Template[, else: Template]) -> Template
: Conditionally evaluate then
/else
template content.coalesce(content: Template...) -> Template
: Returns the first non-empty content.concat(content: Template...) -> Template
: Same as content_1 ++ ... ++ content_n
.separate(separator: Template, content: Template...) -> Template
: Insert separator between non-empty contents.surround(prefix: Template, suffix: Template, content: Template) -> Template
: Surround non-empty content with texts such as parentheses.No methods are defined. Can be constructed with false
or true
literal.
This type cannot be printed. The following methods are defined.
description() -> String
change_id() -> ChangeId
commit_id() -> CommitId
parents() -> List<Commit>
author() -> Signature
committer() -> Signature
mine() -> Boolean
: Commits where the author's email matches the email of the current user.working_copies() -> String
: For multi-workspace repository, indicate working-copy commit as <workspace name>@
.current_working_copy() -> Boolean
: True for the working-copy commit of the current workspace.bookmarks() -> List<RefName>
: Local and remote bookmarks pointing to the commit. A tracking remote bookmark will be included only if its target is different from the local one.local_bookmarks() -> List<RefName>
: All local bookmarks pointing to the commit.remote_bookmarks() -> List<RefName>
: All remote bookmarks pointing to the commit.tags() -> List<RefName>
git_refs() -> List<RefName>
git_head() -> Boolean
: True for the Git HEAD
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).immutable() -> Boolean
: True if the commit is included in the set of immutable commits.contained_in(revset: String) -> 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: String]) -> TreeDiff
: Changes from the parents within the files
expression. All files are compared by default, but it is likely to change in future version to respect the command line path arguments.root() -> Boolean
: True if the commit is the root commit.The following methods are defined.
.normal_hex() -> String
: Normal hex representation (0-9a-f), useful for ChangeId, whose canonical hex representation is \"reversed\" (z-k)..short([len: Integer]) -> String
.shortest([min_len: Integer]) -> ShortestIdPrefix
: Shortest unique prefix.No methods are defined.
"},{"location":"templates/#list-type","title":"List type","text":"A list can be implicitly converted to Boolean
. The following methods are defined.
.len() -> Integer
: Number of elements in the list..join(separator: Template) -> Template
: Concatenate elements with the given separator
..map(|item| expression) -> ListTemplate
: Apply template expression
to each element. Example: parents.map(|c| c.commit_id().short())
The following methods are defined. See also the List
type.
.join(separator: Template) -> Template
This type cannot be printed. The following methods are defined.
current_operation() -> Boolean
description() -> String
id() -> OperationId
tags() -> String
time() -> TimestampRange
user() -> String
snapshot() -> Boolean
: True if the operation is a snapshot operation.root() -> Boolean
: True if the operation is the root operation.The following methods are defined.
.short([len: Integer]) -> String
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.
The following methods are defined.
.name() -> String
: Local bookmark or tag name..remote() -> String
: Remote name or empty if this is a local 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.The following methods are defined.
.prefix() -> String
.rest() -> String
.upper() -> ShortestIdPrefix
.lower() -> ShortestIdPrefix
The following methods are defined.
.name() -> String
.email() -> String
.username() -> String
.timestamp() -> Timestamp
This type cannot be printed. The following methods are defined.
.lower() -> Integer
: Lower bound..upper() -> Option<Integer>
: Upper bound if known..exact() -> Option<Integer>
: Exact value if upper bound is known and it equals to the lower bound..zero() -> Boolean
: True if upper bound is known and is 0
.A string can be implicitly converted to Boolean
. The following methods are defined.
.len() -> Integer
: Length in UTF-8 bytes..contains(needle: Template) -> Boolean
.first_line() -> String
.lines() -> List<String>
: Split into lines excluding newline characters..upper() -> String
.lower() -> String
.starts_with(needle: Template) -> Boolean
.ends_with(needle: Template) -> Boolean
.remove_prefix(needle: Template) -> String
: Removes the passed prefix, if present.remove_suffix(needle: Template) -> String
: Removes the passed suffix, if present.substr(start: Integer, end: Integer) -> String
: Extract substring. The start
/end
indices should be specified in UTF-8 bytes. Negative values count from the end of the string.String literals must be surrounded by single or double quotes ('
or \"
). A double-quoted string literal supports the following escape sequences:
\\\"
: double quote\\\\
: backslash\\t
: horizontal tab\\r
: carriage return\\n
: new line\\0
: null\\e
: escape (i.e., \\x1b
)\\xHH
: byte with hex value HH
Other escape sequences are not supported. Any UTF-8 characters are allowed inside a string literal, with two exceptions: unescaped \"
-s and uses of \\
that don't form a valid escape sequence.
A single-quoted string literal has no escape syntax. '
can't be expressed inside a single-quoted string literal.
Most types can be implicitly converted to Template
. No methods are defined.
The following methods are defined.
.ago() -> String
: Format as relative timestamp..format(format: String) -> 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: String) -> Boolean
: True if the timestamp is exactly at or after the given date..before(date: String) -> Boolean
: True if the timestamp is before, but not including, the given date.The following methods are defined.
.start() -> Timestamp
.end() -> Timestamp
.duration() -> String
This type cannot be printed. The following methods are defined.
.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) -> Template
: Format as a histogram of the changes..summary() -> Template
: Format as a list of status code and path pairs.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 cli/src/config/templates.toml
file in jj's source tree.
New keywords and functions 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 function will be shadowed by name, and can't co-exist with aliases.
For example:
[template-aliases]\n'commit_change_ids' = '''\nconcat(\n format_field(\"Commit ID\", commit_id),\n format_field(\"Change ID\", commit_id),\n)\n'''\n'format_field(key, value)' = 'key ++ \": \" ++ value ++ \"\\n\"'\n
"},{"location":"templates/#examples","title":"Examples","text":"Get short commit IDs of the working-copy parents:
jj log --no-graph -r @ -T 'parents.map(|c| c.commit_id().short()).join(\",\")'\n
Show machine-readable list of full commit and change IDs:
jj log --no-graph -T 'commit_id ++ \" \" ++ change_id ++ \"\\n\"'\n
"},{"location":"testimonials/","title":"Testimonials","text":"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 \u2014 from our loving users, our silly developers \u2014 all to tip the scales and get you on our side!
"},{"location":"testimonials/#what-the-users-have-to-say","title":"What the users have to say","text":"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 \u2014 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.
\u2014 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
\u2014 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.
\u2014 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\".
\u2014 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
\u2014 jyn514, Rust contributor
Jujutsu is pretty cool, you can even keep most of your existing workflows
\u2014 Ben, who doesn't want you keeping your existing workflow
Wait, it's not called Jujitsu?
\u2014 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.
\u2014 gul banana, computer programmer
"},{"location":"testimonials/#what-the-developers-have-to-say","title":"What the developers have to say","text":"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 \u2014 developed over years of experience \u2014 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.
\u2014 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.
\u2014 Anton Bulakh, contributor and dopamine enthusiast
I'm sometimes still surprised that navigating with jj next
and jj prev
works.
\u2014 Philip Metzger, author of jj next
and jj prev
I'm surprised when it works.
\u2014 Martin von Zweigbergk, project creator and leader
"},{"location":"testimonials/#spread-the-word-yourself","title":"Spread the word yourself","text":"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.
"},{"location":"tutorial/","title":"Tutorial","text":"Hint: This tutorial has become somewhat out of date. Many people find the alternative (not quite finished) tutorial by Steve Klabnik helpful.
This text assumes that the reader is familiar with Git.
"},{"location":"tutorial/#preparation","title":"Preparation","text":"If you haven't already, make sure you install and configure Jujutsu.
"},{"location":"tutorial/#cloning-a-git-repo","title":"Cloning a Git repo","text":"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\n# repos yet)\n$ jj git clone https://github.com/octocat/Hello-World\nFetching into new repo in \"/tmp/tmp.O1DWMiaKd4/Hello-World\"\nWorking copy now at: kntqzsqt d7439b06 (empty) (no description set)\nParent commit : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1\nAdded 1 files, modified 0 files, removed 0 files\n$ cd Hello-World\n
Running jj st
(short for jj status
) now yields something like this:
$ jj st\nThe working copy is clean\nWorking copy : kntqzsqt d7439b06 (empty) (no description set)\nParent commit: orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1\n
We can see from the output above that our working copy is a real commit with a commit ID (d7439b06
in the example). When you make a change in the working copy, the working-copy commit gets automatically amended by the next jj
command.
Now let's say we want to edit the README
file in the repo to say \"Goodbye\" instead of \"Hello\". Let's start by describing the change (adding a commit message) so we don't forget what we're working on:
# This will bring up $EDITOR (or `pico` or `Notepad` by default). Enter\n# something like \"Say goodbye\" in the editor and then save the file and close\n# the editor.\n$ jj describe\nWorking copy now at: kntqzsqt e427edcf (empty) Say goodbye\nParent commit : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1\n
Now make the change in the README:
# Adjust as necessary for compatibility with your flavor of `sed`\n$ sed -i 's/Hello/Goodbye/' README\n$ jj st\nWorking copy changes:\nM README\nWorking copy : kntqzsqt 5d39e19d Say goodbye\nParent commit: orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1\n
Note that you didn't have to tell Jujutsu to add the change like you would with git add
. You actually don't even need to tell it when you add new files or remove existing files. To untrack a path, add it to your .gitignore
and run jj file untrack <path>
.
To see the diff, run jj diff
:
$ jj diff --git # Feel free to skip the `--git` flag\ndiff --git a/README b/README\nindex 980a0d5f19...1ce3f81130 100644\n--- a/README\n+++ b/README\n@@ -1,1 +1,1 @@\n-Hello World!\n+Goodbye World!\n
Jujutsu's diff format currently defaults to inline coloring of the diff (like git diff --color-words
), so we used --git
above to make the diff readable in this tutorial.
As you may have noticed, the working-copy commit's ID changed both when we edited the description and when we edited the README. However, the parent commit stayed the same. Each change to the working-copy commit amends the previous version. So how do we tell Jujutsu that we are done amending the current change and want to start working on a new one? That is what jj new
is for. That will create a new commit on top of your current working-copy commit. The new commit is for the working-copy changes.
So, let's say we're now done with this change, so we create a new change:
$ jj new\nWorking copy now at: mpqrykyp aef4df99 (empty) (no description set)\nParent commit : kntqzsqt 5d39e19d Say goodbye\n$ jj st\nThe working copy is clean\nWorking copy : mpqrykyp aef4df99 (empty) (no description set)\nParent commit: kntqzsqt 5d39e19d Say goodbye\n
If we later realize that we want to make further changes, we can make them in the working copy and then run jj squash
. That command squashes 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
, and jj amend
is in fact an alias for jj squash
.
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.
You're probably familiar with git log
. Jujutsu has very similar functionality in its jj log
command:
$ jj log\n@ mpqrykyp martinvonz@google.com 2023-02-12 15:00:22.000 -08:00 aef4df99\n\u2502 (empty) (no description set)\n\u25c9 kntqzsqt martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19d\n\u2502 Say goodbye\n\u2502 \u25c9 tpstlust support+octocat@github.com 2018-05-10 12:55:19.000 -05:00 octocat-patch-1@origin b1b3f972\n\u251c\u2500\u256f sentence case\n\u2502 \u25c9 kowxouwz octocat@nowhere.com 2014-06-10 15:22:26.000 -07:00 test@origin b3cbd5bb\n\u251c\u2500\u256f Create CONTRIBUTING.md\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2502 (empty) Merge pull request #6 from Spaceghost/patch-1\n~\n
The @
indicates the working-copy commit. The first ID on a line (e.g. \"mpqrykyp\" above) is the \"change ID\", which is an ID that follows the commit as it's rewritten (similar to Gerrit's Change-Id). The second ID is the commit ID, which changes when you rewrite the commit. 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()'\n@ mpqrykyp martinvonz@google.com 2023-02-12 15:00:22.000 -08:00 aef4df99\n\u2577 (empty) (no description set)\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2577 (empty) Merge pull request #6 from Spaceghost/patch-1\n\u25c9 zzzzzzzz root() 00000000\n
The 00000000
commit (change ID zzzzzzzz
) is a virtual commit that's called the \"root commit\". It's the root commit of every repo. The root()
function in the revset matches it.
There are also operators for getting the parents (foo-
), children (foo+
), ancestors (::foo
), descendants (foo::
), DAG range (foo::bar
, like git log --ancestry-path
), range (foo..bar
, same as Git's). See the revset documentation for all revset operators and functions.
Hint: If the default jj log
omits some commits you expect to see, you can always run jj log -r ::
(or, equivalently, jj log -r 'all()'
) to see all the commits.
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\n$ jj new master -m A; echo a > file1\nWorking copy now at: nuvyytnq 00a2aeed (empty) A\nParent commit : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1\nAdded 0 files, modified 1 files, removed 0 files\n$ jj new -m B1; echo b1 > file1\nWorking copy now at: ovknlmro 967d9f9f (empty) B1\nParent commit : nuvyytnq 5dda2f09 A\n$ jj new -m B2; echo b2 > file1\nWorking copy now at: puqltutt 8ebeaffa (empty) B2\nParent commit : ovknlmro 7d7c6e6b B1\n$ jj new -m C; echo c > file2\nWorking copy now at: qzvqqupx 62a3c6d3 (empty) C\nParent commit : puqltutt daa6ffd5 B2\n$ jj log\n@ qzvqqupx martinvonz@google.com 2023-02-12 15:07:41.946 -08:00 2370ddf3\n\u2502 C\n\u25c9 puqltutt martinvonz@google.com 2023-02-12 15:07:33.000 -08:00 daa6ffd5\n\u2502 B2\n\u25c9 ovknlmro martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6b\n\u2502 B1\n\u25c9 nuvyytnq martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f09\n\u2502 A\n\u2502 \u25c9 kntqzsqt martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19d\n\u251c\u2500\u256f Say goodbye\n\u2502 \u25c9 tpstlust support+octocat@github.com 2018-05-10 12:55:19.000 -05:00 octocat-patch-1@origin b1b3f972\n\u251c\u2500\u256f sentence case\n\u2502 \u25c9 kowxouwz octocat@nowhere.com 2014-06-10 15:22:26.000 -07:00 test@origin b3cbd5bb\n\u251c\u2500\u256f Create CONTRIBUTING.md\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2502 (empty) Merge pull request #6 from Spaceghost/patch-1\n~\n
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 --destination
/-d
option on A.
$ jj rebase -s puqltutt -d nuvyytnq # Replace the IDs by what you have for B2 and A\nRebased 2 commits\nNew conflicts appeared in these commits:\n qzvqqupx 1978b534 (conflict) C\n puqltutt f7fb5943 (conflict) B2\nTo resolve the conflicts, start by updating to the first one:\n jj new puqltuttzvly\nThen use `jj resolve`, or edit the conflict markers in the file directly.\nOnce the conflicts are resolved, you may want to inspect the result with `jj diff`.\nThen run `jj squash` to move the resolution into the conflicted commit.\nWorking copy now at: qzvqqupx 1978b534 (conflict) C\nParent commit : puqltutt f7fb5943 (conflict) B2\nAdded 0 files, modified 1 files, removed 0 files\n$ jj log\n@ qzvqqupx martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 1978b534 conflict\n\u2502 C\n\u25c9 puqltutt martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 f7fb5943 conflict\n\u2502 B2\n\u2502 \u25c9 ovknlmro martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6b\n\u251c\u2500\u256f B1\n\u25c9 nuvyytnq martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f09\n\u2502 A\n\u2502 \u25c9 kntqzsqt martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19d\n\u251c\u2500\u256f Say goodbye\n\u2502 \u25c9 tpstlust support+octocat@github.com 2018-05-10 12:55:19.000 -05:00 octocat-patch-1@origin b1b3f972\n\u251c\u2500\u256f sentence case\n\u2502 \u25c9 kowxouwz octocat@nowhere.com 2014-06-10 15:22:26.000 -07:00 test@origin b3cbd5bb\n\u251c\u2500\u256f Create CONTRIBUTING.md\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2502 (empty) Merge pull request #6 from Spaceghost/patch-1\n~\n
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:
$ jj new puqltutt # Replace the ID by what you have for B2\nWorking copy now at: zxoosnnp c7068d1c (conflict) (empty) (no description set)\nParent commit : puqltutt f7fb5943 (conflict) B2\nAdded 0 files, modified 0 files, removed 1 files\n$ jj st\nThe working copy is clean\nThere are unresolved conflicts at these paths:\nfile1 2-sided conflict\nWorking copy : zxoosnnp c7068d1c (conflict) (empty) (no description set)\nParent commit: puqltutt f7fb5943 (conflict) B2\n$ cat file1\n<<<<<<<\n%%%%%%%\n-b1\n+a\n+++++++\nb2\n>>>>>>>\n$ echo resolved > file1\n$ jj squash\nRebased 1 descendant commits\nExisting conflicts were resolved or abandoned from these commits:\n qzvqqupx hidden 1978b534 (conflict) C\n puqltutt hidden f7fb5943 (conflict) B2\nWorking copy now at: ntxxqymr e3c279cc (empty) (no description set)\nParent commit : puqltutt 2c7a658e B2\n$ jj log\n@ ntxxqymr martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 e3c279cc\n\u2502 (empty) (no description set)\n\u2502 \u25c9 qzvqqupx martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 b9da9d28\n\u251c\u2500\u256f C\n\u25c9 puqltutt martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 2c7a658e\n\u2502 B2\n\u2502 \u25c9 ovknlmro martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6b\n\u251c\u2500\u256f B1\n\u25c9 nuvyytnq martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f09\n\u2502 A\n\u2502 \u25c9 kntqzsqt martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19d\n\u251c\u2500\u256f Say goodbye\n\u2502 \u25c9 tpstlust support+octocat@github.com 2018-05-10 12:55:19.000 -05:00 octocat-patch-1@origin b1b3f972\n\u251c\u2500\u256f sentence case\n\u2502 \u25c9 kowxouwz octocat@nowhere.com 2014-06-10 15:22:26.000 -07:00 test@origin b3cbd5bb\n\u251c\u2500\u256f Create CONTRIBUTING.md\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2502 (empty) Merge pull request #6 from Spaceghost/patch-1\n~\n
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.
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\n@ d3b77addea49 martinvonz@vonz.svl.corp.google.com 3 minutes ago, lasted 3 milliseconds\n\u2502 squash commit 63874fe6c4fba405ffc38b0dd926f03b715cf7ef\n\u2502 args: jj squash\n\u25c9 6fc1873c1180 martinvonz@vonz.svl.corp.google.com 3 minutes ago, lasted 1 milliseconds\n\u2502 snapshot working copy\n\u2502 args: jj squash\n\u25c9 ed91f7bcc1fb martinvonz@vonz.svl.corp.google.com 6 minutes ago, lasted 1 milliseconds\n\u2502 new empty commit\n\u2502 args: jj new puqltutt\n\u25c9 367400773f87 martinvonz@vonz.svl.corp.google.com 12 minutes ago, lasted 3 milliseconds\n\u2502 rebase commit daa6ffd5a09a8a7d09a65796194e69b7ed0a566d and descendants\n\u2502 args: jj rebase -s puqltutt -d nuvyytnq\n[many more lines]\n
The most useful command is jj undo
(alias for jj op undo
), which will undo an operation. By default, it will undo the most recent operation. Let's try it:
$ jj undo\nNew conflicts appeared in these commits:\n qzvqqupx 1978b534 (conflict) C\n puqltutt f7fb5943 (conflict) B2\nTo resolve the conflicts, start by updating to the first one:\n jj new puqltuttzvly\nThen use `jj resolve`, or edit the conflict markers in the file directly.\nOnce the conflicts are resolved, you may want to inspect the result with `jj diff`.\nThen run `jj squash` to move the resolution into the conflicted commit.\nWorking copy now at: zxoosnnp 63874fe6 (no description set)\nParent commit : puqltutt f7fb5943 (conflict) B2\n$ jj log\n@ zxoosnnp martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 63874fe6\n\u2502 (no description set)\n\u2502 \u25c9 qzvqqupx martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 1978b534 conflict\n\u251c\u2500\u256f C\n\u25c9 puqltutt martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 f7fb5943 conflict\n\u2502 B2\n\u2502 \u25c9 ovknlmro martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6b\n\u251c\u2500\u256f B1\n\u25c9 nuvyytnq martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f09\n\u2502 A\n\u2502 \u25c9 kntqzsqt martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19d\n\u251c\u2500\u256f Say goodbye\n\u2502 \u25c9 tpstlust support+octocat@github.com 2018-05-10 12:55:19.000 -05:00 octocat-patch-1@origin b1b3f972\n\u251c\u2500\u256f sentence case\n\u2502 \u25c9 kowxouwz octocat@nowhere.com 2014-06-10 15:22:26.000 -07:00 test@origin b3cbd5bb\n\u251c\u2500\u256f Create CONTRIBUTING.md\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2502 (empty) Merge pull request #6 from Spaceghost/patch-1\n~\n
As you can perhaps see, that undid the jj squash
invocation we used for squashing the conflict resolution into commit B2 earlier. Notice that it also updated the working copy.
You can also view the repo the way it looked after some earlier operation. For example, if you want to see jj log
output right after the jj rebase
operation, try jj log --at-op=367400773f87
but use the hash from your own jj op log
.
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\nWorking copy now at: ztqrpvnw f94e49cf (empty) abc\nParent commit : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1\nAdded 0 files, modified 0 files, removed 1 files\n$ jj new -m ABC; printf 'A\\nB\\nc\\n' > file\nWorking copy now at: kwtuwqnm 6f30cd1f (empty) ABC\nParent commit : ztqrpvnw 51002261 ab\n$ jj new -m ABCD; printf 'A\\nB\\nC\\nD\\n' > file\nWorking copy now at: mrxqplyk a6749154 (empty) ABCD\nParent commit : kwtuwqnm 30aecc08 ABC\n$ jj log -r master::@\n@ mrxqplyk martinvonz@google.com 2023-02-12 19:38:21.000 -08:00 b98c607b\n\u2502 ABCD\n\u25c9 kwtuwqnm martinvonz@google.com 2023-02-12 19:38:12.000 -08:00 30aecc08\n\u2502 ABC\n\u25c9 ztqrpvnw martinvonz@google.com 2023-02-12 19:38:03.000 -08:00 51002261\n\u2502 abc\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2502 (empty) Merge pull request #6 from Spaceghost/patch-1\n~\n
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\nUsing default editor ':builtin'; you can change this by setting ui.diff-editor\nWorking copy now at: mrxqplyk 52a6c7fd ABCD\nParent commit : kwtuwqnm 643061ac ABC\n
That will bring up the built-in diff editor1 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).
If we look at the diff of the second commit, we now see that all three lines got capitalized:
$ jj diff -r @-\nModified regular file file:\n 1 1: aA\n 2 2: bB\n 3 3: cC\n
The child change (\"ABCD\" in our case) will have the same content state after the jj squash
command. That means that you can move any changes you want into the parent change, even if they touch the same word, and it won't cause any conflicts.
Let's try one final command for changing the contents of an exiting commit. That command is jj diffedit
, which lets you edit the contents of a commit without checking it out.
$ jj diffedit -r @-\nUsing default editor ':builtin'; you can change this by setting ui.diff-editor\nCreated kwtuwqnm 70985eaa (empty) ABC\nRebased 1 descendant commits\nNew conflicts appeared in these commits:\n mrxqplyk 1c72cd50 (conflict) ABCD\nTo resolve the conflicts, start by updating to it:\n jj new mrxqplykmyqv\nThen use `jj resolve`, or edit the conflict markers in the file directly.\nOnce the conflicts are resolved, you may want to inspect the result with `jj diff`.\nThen run `jj squash` to move the resolution into the conflicted commit.\nWorking copy now at: mrxqplyk 1c72cd50 (conflict) ABCD\nParent commit : kwtuwqnm 70985eaa (empty) ABC\nAdded 0 files, modified 1 files, removed 0 files\n
In the diff editor, edit the right side by e.g. adding something to the first line. 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 addition to the first 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).
There are many other diff editors you could use. For example, if you have Meld installed and in the PATH, you can use it via jj squash -i --tool meld
or a fancier config with jj squash -i --tool meld-3
. You can configure the default with the ui.diff-editor
option; those docs also explain how to specify a path to an executable if it is not in the PATH.\u00a0\u21a9
Jujutsu works the same on all platforms, but there are some caveats that Windows users should be aware of.
"},{"location":"windows/#line-endings-are-not-converted","title":"Line endings are not converted","text":"Jujutsu does not currently honor .gitattributes
and does not have a setting like Git's core.autocrlf
. This means that line endings will be checked out exactly as they are committed and committed exactly as authored. This is true on all platforms, but Windows users are most likely to miss CRLF conversion.
If your Git repository expects Windows users to have core.autocrlf
set to true
, then the files are committed with LF line endings but are checked out with CRLF line endings. Jujutsu doesn't understand this and will convert the committed line endings to CRLF.
After creating a colocated repository on Windows, you most likely want to set core.autocrlf
to input
, then jj abandon
to convert all files on disk to LF line endings:
PS> git config core.autocrlf input\n\n# Abandoning the working copy will cause Jujutsu to overwrite all files with\n# CRLF line endings with the line endings they are committed with, probably LF\nPS> jj abandon\n
This setting ensures 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.
"},{"location":"windows/#pagination","title":"Pagination","text":"On Windows, jj
will use its integrated pager called minus
by default, unless the environment variable %PAGER%
or 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\"]'\nPS> jj config set --user ui.paginate auto\n
"},{"location":"windows/#typing-in-powershell","title":"Typing @
in PowerShell","text":"PowerShell uses @
as part the array sub-expression operator, so it often needs to be escaped or quoted in commands:
PS> jj log -r `@\nPS> jj log -r '@'\n
One solution is to create a revset alias. For example, to make HEAD
an alias for @
:
PS> jj config set --user revset-aliases.HEAD '@'\nPS> jj log -r HEAD\n
"},{"location":"windows/#wsl-sets-the-execute-bit-on-all-files","title":"WSL sets the execute bit on all files","text":"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-repo\n
Then only use the ~/my-repo
workspace from Linux.
jj
supports symlinks on Windows only when they are enabled by the operating system. This requires Windows 10 version 14972 or higher, as well as Developer Mode. If those conditions are not satisfied, jj
will materialize symlinks as ordinary files.
For colocated repositories, Git support must also be enabled using the git config
option core.symlinks=true
.
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.
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.
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/martinvonz/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.
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 $HOME/.gitignore
and $GIT_DIR/info/exclude
.
Ignored files are never tracked automatically (regardless of the value of snapshot.auto-track
), but they can still end up being tracked for a few reasons:
jj file track
commandYou can untrack such files with the jj file untrack command.
"},{"location":"working-copy/#workspaces","title":"Workspaces","text":"You can have multiple working copies backed by a single repo. Use jj workspace add
to create a new working copy. The working copy will have a .jj/
directory linked to the main repo. The working copy and the .jj/
directory together is called a \"workspace\". Each workspace can have a different commit checked out.
Having multiple workspaces can be useful for running long-running tests in a one while you continue developing in another, for example. If needed, jj workspace root
prints the root path of the current workspace.
When you're done using a workspace, use jj workspace forget
to make the repo forget about it. The files can be deleted from disk separately (either before or after).
Almost all commands go through three main steps:
@
should point toIf 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.
Authors: Daniel Ploch
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.
"},{"location":"design/copy-tracking/#objective","title":"Objective","text":"Implement extensible APIs for recording and retrieving copy info for the purposes of diffing and rebasing across renames and copies more accurately. This should be performant both for Git, which synthesizes copy info on the fly between arbitrary trees, and for custom extensions 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.
"},{"location":"design/copy-tracking/#interface-design","title":"Interface Design","text":""},{"location":"design/copy-tracking/#read-api","title":"Read API","text":"Copy information will be served both by a new Backend trait method described below, as well as a new field on Commit objects for backends that support copy tracking:
/// An individual copy source.\npub struct CopySource {\n /// The source path a target was copied from.\n ///\n /// It is not required that the source path is different than the target\n /// path. A custom backend may choose to represent 'rollbacks' as copies\n /// from a file unto itself, from a specific prior commit.\n path: RepoPathBuf,\n file: FileId,\n /// The source commit the target was copied from. If not specified, then the\n /// parent of the target commit is the source commit. Backends may use this\n /// field to implement 'integration' logic, where a source may be\n /// periodically merged into a target, similar to a branch, but the\n /// branching occurs at the file level rather than the repository level. It\n /// also follows naturally that any copy source targeted to a specific\n /// commit should avoid copy propagation on rebasing, which is desirable\n /// for 'fork' style copies.\n ///\n /// If specified, it is required that the commit id is an ancestor of the\n /// commit with which this copy source is associated.\n commit: Option<CommitId>,\n}\n\npub enum CopySources {\n Resolved(CopySource),\n Conflict(HashSet<CopySource>),\n}\n\n/// An individual copy event, from file A -> B.\npub struct CopyRecord {\n /// The destination of the copy, B.\n target: RepoPathBuf,\n /// The CommitId where the copy took place.\n id: CommitId,\n /// The source of the copy, A.\n sources: CopySources,\n}\n\n/// Backend options for fetching copy records.\npub struct CopyRecordOpts {\n // TODO: Probably something for git similarity detection\n}\n\npub type CopyRecordStream = BoxStream<BackendResult<CopyRecord>>;\n\npub trait Backend {\n /// Get all copy records for `paths` in the dag range `roots..heads`.\n ///\n /// The exact order these are returned is unspecified, but it is guaranteed\n /// to be reverse-topological. That is, for any two copy records with\n /// different commit ids A and B, if A is an ancestor of B, A is streamed\n /// after B.\n ///\n /// Streaming by design to better support large backends which may have very\n /// large single-file histories. This also allows more iterative algorithms\n /// like blame/annotate to short-circuit after a point without wasting\n /// unnecessary resources.\n async fn get_copy_records(&self, paths: &[RepoPathBuf], roots: &[CommitId], heads: &[CommitId]) -> CopyRecordStream;\n}\n
Obtaining copy records for a single commit requires first computing the files list for that commit, then calling get_copy_records with heads = [id]
and roots = parents()
. This enables commands like jj diff
to produce better diffs that take copy sources into account.
Backends that support tracking copy records at the commit level will do so through a new field on backend::Commit
objects:
pub struct Commit {\n ...\n copies: Option<HashMap<RepoPathBuf, CopySources>>,\n}\n\npub trait Backend {\n /// Whether this backend supports storing explicit copy records on write.\n fn supports_copy_tracking(&self) -> bool;\n}\n
This field will be ignored by backends that do not support copy tracking, and always set to None
when read from such backends. Backends that do support copy tracking are required to preserve the field value always.
This API will enable the creation of new jj
commands for recording copies:
jj cp $SRC $DEST [OPTIONS]\njj mv $SRC $DEST [OPTIONS]\n
These commands will rewrite the target commit to reflect the given move/copy instructions in its tree, as well as recording the rewrites on the Commit object itself for backends that support it (for backends that do not, these copy records will be silently discarded).
Flags for the first two commands will include:
-r/--revision\n perform the copy or move at the specified revision\n defaults to the working copy commit if unspecified\n-f\n force overwrite the destination path\n--after\n record the copy retroactively, without modifying the targeted commit tree\n--resolve\n overwrite all previous copy intents for this $DEST\n--allow-ignore-copy\n don't error if the backend doesn't support copy tracking\n--from REV\n specify a commit id for the copy source that isn't the parent commit\n
For backends which do not support copy tracking, it will be an error to use --after
, since this has no effect on anything and the user should know that. The supports_copy_tracking()
trait method is used to determine this.
An additional command is provided to deliberately discard copy info for a destination path, possibly as a means of resolving a conflict.
jj forget-cp $DEST [-r REV]\n
"},{"location":"design/copy-tracking/#behavioral-changes","title":"Behavioral Changes","text":""},{"location":"design/copy-tracking/#rebase-changes","title":"Rebase Changes","text":"In general, we want to support the following use cases:
Using the aforementioned copy tracing API, both of these should be feasible. A detailed approach to a specific edge case is detailed in the next section.
"},{"location":"design/copy-tracking/#rename-of-an-added-file","title":"Rename of an added file","text":"A well known and thorny problem in Mercurial occurs in the following scenario:
In jj, we have an opportunity to fix this because all rebasing occurs atomically and transactionally within memory. The exact implementation of this is yet to be determined, but conceptually the following should produce desirable results:
With copy-tracking, a whole new class of conflicts become possible. These need to be well-defined and have well documented resolution paths. Because copy info in a commit is keyed by destination, conflicts can only occur at the destination of a copy, not at a source (that's called forking).
"},{"location":"design/copy-tracking/#split-conflicts","title":"Split conflicts","text":"Suppose we create commit A by renaming file F1 -> F2, then we split A. What happens to the copy info? I argue that this is straightforward:
Things get a little messier if A also modifies F1, and this modification is separated from the copy, but I think this is messy only in an academic sense and the user gets a sane result either way. If they want to separate the modification from the copy while still putting it in an earlier commit, they can express this intent after with jj cp --after --from
.
Suppose we create commit A by renaming file F1 -> F, then we create a sibling commit B by renaming file F2 -> F. What happens when we create a merge commit with parents A and B?
In terms of copy info there is no conflict here, because C does not have copy info and needs none, but resolving the contents of F becomes more complicated. We need to (1) identify the greatest common ancestor of A and B (D) (which we do anyway), and (2) invoke get_copy_records()
on F for each of D::A
and D::B
to identify the 'real' source file id for each parent. If these are the same, then we can use that as the base for a better 3-way merge. Otherwise, we must treat it as an add+add conflict where the base is the empty file id.
It is possible that F1 and F2 both came from a common source file G, but that these copies precede D. In such case, we will not produce as good of a merge resolution as we theoretically could, but (1) this seems extremely niche and unlikely, and (2) we cannot reasonably achieve this without implementing some analogue of Mercurial's linknodes concept, and it would be nice to avoid that additional complexity.
"},{"location":"design/copy-tracking/#squash-conflicts","title":"Squash conflicts","text":"Suppose we create commit A by renaming file F1 -> F, then we create child commit B in which we replace F by renaming F2 -> F. This touches on two issues.
Firstly, if these two commits are squashed together, then we have a destination F with two copy sources, F1 and F2. In this case, we can store a CopySources::Conflict([F1, F2])
as the copy source for F, and treat this commit as 'conflicted' in jj log
. jj status
will need modification to show this conflicted state, and jj resolve
will need some way of handling the conflicted copy sources (possibly printing them in some structured text form, and using the user's merge tool to resolve them). Alternatively, the user can 'resolve directly' by running jj cp --after --resolve
with the desired copy info.
Secondly, who is to say that commit B is 'replacing' F at all? In some version control systems, it is possible to 'integrate' a file X into an existing file Y, by e.g. propagating changes in X since its previous 'integrate' into Y, without erasing Y's prior history in that moment for the purpose of archaeology. With the commit metadata currently defined, it is not possible to distinguish between a 'replacement' operation and an 'integrate' operation.
"},{"location":"design/copy-tracking/#track-replacements-explicitly","title":"Track replacements explicitly","text":"One solution is to add a destructive: bool
field or similar to the CopySource
struct, to explicitly distinguish between these two types of copy records. It then becomes possible to record a non-destructive copy using --after
to recognize that a file F was 'merged into' its destination, which can be useful in handling parallel edits of F that later sync this information.
Alternatively, we can keep copy-tracking simple in jj by taking a stronger stance here and treating all copies-onto-existing-files as 'replacement' operations. This makes integrations with more complex VCSs that do support 'integrate'-style operations trickier, but it is possible that a more generic commit extension system is better suited to such backends.
"},{"location":"design/copy-tracking/#future-changes","title":"Future Changes","text":"An implementation of jj blame
or jj annotate
does not currently exist, but when it does we'll definitely want it to be copy-tracing aware to provide better annotations for users doing archaeology. The Read APIs provided are expected to be sufficient for these use cases.
Git uses rename detection rather than copy tracking, generating copy info on the fly between two arbitrary trees. It does not have any place for explicit copy info that exchanges with other users of the same git repo, so any enhancements jj adds here would be local only and could potentially introduce confusion when collaborating with other users.
"},{"location":"design/copy-tracking/#directory-copiesmoves","title":"Directory copies/moves","text":"All copy/move information will be read and written at the file level. While jj cp|mv
may accept directory paths as a convenience and perform the appropriate tree modification operations, the renames will be recorded at the file level, one for each copied/moved file.
Decide what approach(es) to Git submodule storage we should pursue. The decision will be recorded in ./git-submodules.md.
"},{"location":"design/git-submodule-storage/#use-cases-to-consider","title":"Use cases to consider","text":"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.
"},{"location":"design/git-submodule-storage/#fetching-submodule-commits","title":"Fetching submodule commits","text":"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.
"},{"location":"design/git-submodule-storage/#jj-op-restore-and-operation-log-format","title":"\"jj op restore\" and operation log format","text":"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.
"},{"location":"design/git-submodule-storage/#nested-submodules","title":"Nested submodules","text":"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.
"},{"location":"design/git-submodule-storage/#supporting-future-extensions","title":"Supporting future extensions","text":"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:
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:
"},{"location":"design/git-submodule-storage/#fetching-submodule-commits_1","title":"Fetching submodule commits","text":"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.
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.
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.
"},{"location":"design/git-submodule-storage/#extending-to-colocated-git-repos","title":"Extending to colocated Git repos","text":"Git expects submodules to be in .git/modules
, so it will not understand this storage format. To support colocated Git repos, 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.
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.
"},{"location":"design/git-submodule-storage/#store-git-submodules-as-alternate-git-backends","title":"Store Git submodules as alternate Git backends","text":"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.
"},{"location":"design/git-submodules/","title":"Git submodules","text":"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.
"},{"location":"design/git-submodules/#objective","title":"Objective","text":"This proposal aims to replicate the workflows users are used to with Git submodules, e.g.:
When it is convenient, this proposal will also aim to make submodules easier to use than Git's implementation.
"},{"location":"design/git-submodules/#non-goals","title":"Non-goals","text":"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.
"},{"location":"design/git-submodules/#intro-to-git-submodules","title":"Intro to Git Submodules","text":"Git submodules are a feature of Git that allow a repository (submodule) to be embedded inside another repository (the superproject). Notably, a submodule is a full repository, complete with its own index, object store and ref store. It can be interacted with like any other repository, regardless of the superproject.
In a superproject commit, submodule information is captured in two places:
A gitlink
entry in the commit's tree, where the value of the gitlink
entry is the submodule commit id. This tells Git what to populate in the working tree.
A top level .gitmodules
file. This file is in Git's config syntax and entries take the form submodule.<submodule-name>.*
. These include many settings about the submodules, but most importantly:
submodule<submodule-name>.path
contains the path from the root of the tree to the gitlink
being described.
submodule<submodule-name>.url
contains the url to clone the submodule from.
In the working tree, Git notices the presence of a submodule by the .git
entry (signifying the root of a Git repository working tree). This is either the submodule's actual Git directory (an \"old-form\" submodule), or a .git
file pointing to <superproject-git-directory>/modules/<submodule-name>
. The latter is sometimes called the \"absorbed form\", and is Git's preferred mode of operation.
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 repositories, while colocated repositories will be supported when convenient.
This section should be treated as a set of guidelines, not a strict order of work.
"},{"location":"design/git-submodules/#phase-1-readonly-submodules","title":"Phase 1: Readonly submodules","text":"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.
"},{"location":"design/git-submodules/#outcomes","title":"Outcomes","text":"This allows a user to write new contents to a submodule and its remote.
"},{"location":"design/git-submodules/#outcomes_1","title":"Outcomes","text":"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.
"},{"location":"design/git-submodules/#outcomes_2","title":"Outcomes","text":"I.e. outcomes we would like to see if there were no constraints whatsoever.
TODO
"},{"location":"design/git-submodules/#storing-submodules","title":"Storing submodules","text":"Possible approaches under discussion. See ./git-submodule-storage.md.
"},{"location":"design/git-submodules/#snapshotting-new-submodule-changes","title":"Snapshotting new submodule changes","text":"TODO
"},{"location":"design/git-submodules/#mergingrebasing-with-submodules","title":"Merging/rebasing with submodules","text":"TODO
"},{"location":"design/run/","title":"Introducing JJ run","text":"Authors: Philip Metzger, Martin von Zweigberk, Danny Hooper, Waleed Khan
Initial Version, 10.12.2022 (view full history here)
Summary: This Document documents the design of a new run
command for Jujutsu which will be used to seamlessly integrate with build systems, linters and formatters. This is achieved by running a user-provided command or script across multiple revisions. For more details, read the Use-Cases of jj run.
The goal of this Design Document is to specify the correct behavior of jj run
. The points we decide on here I (Philip Metzger) will try to implement. There exists some prior work in other DVCS:
git test
: part of git-branchless. Similar to this proposal for jj run
.hg run
: Google's internal Mercurial extension. Similar to this proposal for jj run
. Details not available.hg fix
: Google's open source Mercurial extension: source code. A more specialized approach to rewriting file content without full context of the working directory.git rebase -x
: runs commands opportunistically as part of rebase.git bisect run
: run a command to determine which commit introduced a bug.The initial need for some kind of command runner integrated in the VCS, surfaced in a github discussion. In a discussion on discord about the git-hook model, there was consensus about not repeating their mistakes.
For jj run
there is prior art in Mercurial, git branchless and Google's internal Mercurial. Currently git-branchless git test
and hg fix
implement some kind of command runner. The Google internal hg run
works in conjunction with CitC (Clients in the Cloud) which allows it to lazily apply the current command to any affected file. Currently no Jujutsu backend (Git, Native) has a fancy virtual filesystem supporting it, so we can't apply this optimization. We could do the same once we have an implementation of the working copy based on a virtual file system. Until then, we have to run the commands in regular local-disk working copies.
jj test
, jj fix
and jj format
.jj test
, jj format
and jj fix
, we shouldn't mash their use-cases into jj run
.fix
subcommand as it cuts too much design space.Linting and Formatting:
jj run 'pre-commit run' -r $revset
jj run 'cargo clippy' -r $revset
jj run 'cargo +nightly fmt'
Large scale changes across repositories, local and remote:
jj run 'sed /some/test/' -r 'mine() & ~remote_bookmarks(exact:\"origin\")'
jj run '$rewrite-tool' -r '$revset'
Build systems:
jj run 'bazel build //some/target:somewhere'
jj run 'ninja check-lld'
Some of these use-cases should get a specialized command, as this allows further optimization. A command could be jj format
, which runs a list of formatters over a subset of a file in a revision. Another command could be jj fix
, which runs a command like rustfmt --fix
or cargo clippy --fix
over a subset of a file in a revision.
All the work will be done in the .jj/
directory. This allows us to hide all complexity from the users, while preserving the user's current workspace.
We will copy the approach from git-branchless's git test
of creating a temporary working copy for each parallel command. The working copies will be reused between jj run
invocations. They will also be reused within jj run
invocation if there are more commits to run on than there are parallel jobs.
We will leave ignored files in the temporary directory between runs. That enables incremental builds (e.g. by letting cargo reuse its target/
directory). However, it also means that runs potentially become less reproducible. We will provide a flag for removing ignored files from the temporary working copies to address that.
Another problem with leaving ignored files in the temporary directories is that they take up space. That is especially problematic in the case of cargo (the target/
directory often takes up tens of GBs). The same flag for cleaning up ignored files can be used to address that. We may want to also have a flag for cleaning up temporary working copies after running the command.
An early version of the command will directly use Treestate to to manage the temporary working copies. That means that running jj
inside the temporary working copies will not work . We can later extend that to use a full Workspace. To prevent operations in the working copies from impacting the repo, we can use a separate OpHeadsStore for it.
Since the subprocesses will run in temporary working copies, they won't interfere with the user's working copy. The user can therefore continue to work in it while jj run
is running.
We want subprocesses to be able to make changes to the repo by updating their assigned working copy. Let's say the user runs jj run
on just commits A and B, where B's parent is A. Any changes made on top of A would be squashed into A, forming A'. Similarly B' would be formed by squasing it into B. We can then either do a normal rebase of B' onto A', or we can simply update its parent to A'. The former is useful, e.g. when the subprocess only makes a partial update of the tree based on the parent commit. In addition to these two modes, we may want to have an option to ignore any changes made in the subprocess's working copy.
Once we give the subprocess access to a fork of the repo via separate OpHeadsStore, it will be able to create new operations in its fork. If the user runs jj run -r foo
and the subprocess checks out another commit, it's not clear what that should do. We should probably just verify that the working-copy commit's parents are unchanged after the subprocess returns. Any operations created by the subprocess will be ignored.
Like all commands, jj run
will refuse to rewrite public/immutable commits. For private/unpublished revisions, we either amend or reparent the changes, which are available as command options.
It may be useful to execute commands in topological order. For example, commands with costs proportional to incremental changes, like build systems. There may also be other relevant heuristics, but topological order is an easy and effective way to start.
Parallel execution of commands on different commits may choose to schedule commits to still reduce incremental changes in the working copy used by each execution slot/\"thread\". However, running the command on all commits concurrently should be possible if desired.
Executing commands in topological order allows for more meaningful use of any potential features that stop execution \"at the first failure\". For example, when running tests on a chain of commits, it might be useful to proceed in topological/chronological order, and stop on the first failure, because it might imply that the remaining executions will be undesirable because they will also fail.
"},{"location":"design/run/#dealing-with-failure","title":"Dealing with failure","text":"It will be useful to have multiple strategies to deal with failures on a single or multiple revisions. The reason for these strategies is to allow customized conflict handling. These strategies then can be exposed in the ui with a matching option.
Continue: If any subprocess fails, we will continue the work on child revisions. Notify the user on exit about the failed revisions.
Stop: Signal a fatal failure and cancel any scheduled work that has not yet started running, but let any already started subprocess finish. Notify the user about the failed command and display the generated error from the subprocess.
Fatal: Signal a fatal failure and immediately stop processing and kill any running processes. Notify the user that we failed to apply the command to the specific revision.
We will leave any affected commit in its current state, if any subprocess fails. This allows us to provide a better user experience, as leaving revisions in an undesirable state, e.g partially formatted, may confuse users.
"},{"location":"design/run/#resource-constraints","title":"Resource constraints","text":"It will be useful to constrain the execution to prevent resource exhaustion. Relevant resources could include:
jj run
can provide some simple mitigations like limiting parallelism to \"number of CPUs\" by default, and limiting parallelism by dividing \"available memory\" by some estimate or measurement of per-invocation memory use of the commands.The base command of any jj command should be usable. By default jj run
works on the @
the current working copy.
continue|stop|fatal
, see Dealing with failurejj log
: No special handling needed jj diff
: No special handling needed jj st
: For now reprint the final output of jj run
jj op log
: No special handling needed, but awaits further discussion in #963 jj undo/jj op undo
: No special handling needed
Should the command be working copy backend specific? How do we manage the Processes which the command will spawn? Configuration options, User and Repository Wide?
"},{"location":"design/run/#future-possibilities","title":"Future possibilities","text":"select(..., message = \"arch not supported for $project\")
.jj run
asynchronous by spawning a main
process, directly return to the user and incrementally updating the output of jj st
.Authors: Daniel Ploch
Summary: This Document documents a redesign of the sparse command and it's internal storage format in jj, in order to facilitate several desirable improvements for large repos. It covers both the migration path and the planned end state.
"},{"location":"design/sparse-v2/#objective","title":"Objective","text":"Redesign Sparse Patterns to accommodate more advanced features for native and custom implementations. This includes three main goals:
Sparse patterns are an effectively unordered list of prefix strings:
path/one\npath/to/dir/two\n
The set of files identified by the Sparse Patterns is all paths which match any provided prefix. This governs what gets materialized in the working copy on checkout, and what is updated on snapshot. The set is stored in working copy state files which are not versioned in the Op Store.
Because all paths are bare strings with no escaping or higher-level formatting, the current design makes it difficult to add new features like exclusions or path remappings.
"},{"location":"design/sparse-v2/#proposed-state-sparse-patterns-v2","title":"Proposed State (Sparse Patterns v2)","text":"Sparse Patterns v2 will be stored as objects in the Op Store, referenced by a WorkingCopyPatternsId
from the active View
. They will have a new, ordered structure which can fully represent previous patterns.
/// Analogues of RepoPath, specifically describing paths in the working copy.\nstruct WorkingCopyPathBuf {\n String\n}\nstruct WorkingCopyPath {\n str\n}\n\npub enum SparsePatternsPathType {\n Dir, // Everything under <path>/...\n Files, // Files under <path>/*\n Exact, // <path> exactly\n}\n\npub struct SparsePatternsPath {\n path_type: SparsePatternsPathType,\n include: bool, // True if included, false if excluded.\n path: RepoPathBuf,\n}\n\npub struct WorkingCopyMapping {\n src_path: RepoPathBuf,\n dst_path: WorkingCopyPathBuf,\n recursive: bool, // If false, only immediate children of src_path (files) are renamed.\n}\n\npub struct WorkingCopyPatterns {\n sparse_paths: Vec<SparsePatternsPath>,\n mappings: Vec<WorkingCopyMapping>,\n}\n\npub trait OpStore {\n ...\n pub fn read_working_copy_patterns(&self, id: &WorkingCopyPatternsId) -> OpStoreResult<WorkingCopyPatterns> { ... }\n pub fn write_working_copy_patterns(&self, sparse_patterns: &WorkingCopyPatterns) -> OpStoreResult<WorkingCopyPatternsId> { .. }\n}\n
To support these more complex behaviors, a new WorkingCopyPatterns
trait will be introduced, initially only as a thin wrapper around the existing prefix format, but soon to be expanded with richer types and functionality.
impl WorkingCopyPatterns {\n pub fn to_matcher(&self) -> Box<dyn Matcher> {\n ...\n }\n\n ...\n}\n
"},{"location":"design/sparse-v2/#command-syntax","title":"Command Syntax","text":"SparsePatternsPath
rules can be specified on the CLI and in an editor via a compact syntax:
(include|exclude):(dir|files|exact):<path>\n
If both prefix terms are omitted, then include:dir:
is assumed. If any prefix is specified, both must be specified. The editor and CLI will both accept path rules in either format going forward.
jj sparse set --add foo/bar
is equal to jj sparse set --add include:dir:foo/bar
jj sparse set --add exclude:dir:foo/bar
adds a new Dir
type rule with include = false
jj sparse set --exclude foo/bar
as a possible shorthand for the abovejj sparse list
will print the explicit rulesPaths will be stored in an ordered, canonical form which unambiguously describes the set of files to be included. Every --add
command will append to the end of this list before the patterns are canonicalized. Whether a file is included is determined by the first matching rule in reverse order.
For example:
include:dir:foo\nexclude:dir:foo/bar\ninclude:dir:foo/bar/baz\nexclude:dir:foo/bar/baz/qux\n
Produces rule set which includes \"foo/file.txt\", excludes \"foo/bar/file.txt\", includes \"foo/bar/baz/file.txt\", and excludes \"foo/bar/baz/qux/file.txt\".
If the rules are subtly re-ordered, they become canonicalized to a smaller, but functionally equivalent form:
# Before\ninclude:dir:foo\nexclude:dir:foo/bar/baz/qux\ninclude:dir:foo/bar/baz\nexclude:dir:foo/bar\n\n# Canonicalized\ninclude:dir:foo\nexclude:dir:foo/bar\n
"},{"location":"design/sparse-v2/#canonicalization","title":"Canonicalization","text":"There are many ways to represent functionally equivalent WorkingCopyPatterns
. For instance, the following 4 rule sets are all functionally equivalent:
# Set 1\ninclude:dir:bar\ninclude:dir:foo\n\n# Set 2\ninclude:dir:foo\ninclude:dir:bar\n\n# Set 3\ninclude:dir:bar\ninclude:dir:bar/baz/qux\ninclude:dir:foo\n\n# Set 4\ninclude:dir:foo\nexclude:dir:foo/baz\ninclude:dir:bar\ninclude:dir:foo/baz\n
Because these patterns are stored in the Op Store now, it is useful for all of these representations to be rewritten into a minimal, canonical form before serialization. In this case, Set 1
will be the canonical set. The canonical form of a WorkingCopyPatterns
is defined as the form such that:
WARNING: This section is intentionally lacking, more research is needed.
All WorkingCopyPatterns
will come equipped with a default no-op mapping. These mappings are inspired by and similar to Perforce client views.
vec![WorkingCopyMapping {\n src_path: RepoPathBuf::root(),\n dst_path: WorkingCopyPathBuf::root(),\n recursive: true,\n}]\n
WorkingCopyPatterns
will provide an interface to map working copy paths into repo paths and vice versa. The `WorkingCopy`` trait will apply this mapping to all snapshot and checkout operations, and jj commands which accept relative paths will need to be updated to perform working copy path -> repo path translations as needed. It's not clear at this time which commands will need changing, as some are more likely to refer to repo paths rather than working copy paths.
TODO: Expand this section.
In particular, the path rules for sparse patterns will always be repo paths, not working copy paths. Thus, if the working copy wants to track \"foo\" and rename it to \"subdir/bar\", they must jj sparse set --add foo
and jj map set --from foo --to bar
. In other words, the mapping operation can be thought of as always after the sparse operation.
New commands will enable editing of the WorkingCopyMapping
s:
TODO: Maybe this should be jj workspace map ...
?
jj map list
will print all mapping pairs.jj map add --from foo --to bar
will add a new mapping to the end of the list.jj map remove --from foo
will remove a specific mapping rule.jj map edit
will pull up a text editor for manual editing.Like sparse paths, mappings will have a compact text syntax for editing in file form, or for adding a rule textually on the CLI:
\"<from>\" -> \"<to>\" [nonrecursive]\n
Like sparse paths, mapping rules are defined to apply in order and on any save operation will be modified to a minimal canonical form. Thus, jj map set --from \"\" --to \"\"
will always completely wipe the map. The first matching rule in reverse list order determines how a particular repo path should be mapped into the working copy, and likewise how a particular working copy path should be mapped into the repo. For simplicity, the 'last rule wins' applies both for repo->WC conversions, as well as WC->repo conversions, using the same ordering.
If a working copy mapping places the same repo file at two distinct working copy paths, snapshotting will fail unless these files are identical. Some specialized filesystems may even treat these as the 'same' file, allowing this to work in some cases.
If a working copy mapping places two distinct repo files at the same working copy path, checkout will fail with an error regardless of equivalence.
"},{"location":"design/sparse-v2/#versioning-and-storage","title":"Versioning and Storage","text":"Updating the active WorkingCopyPatterns
for a particular working copy will now take place in two separate steps: one transaction which updates the op store, and a separate LockedWorkingCopy
operation which actually updates the working copy. The working copy proto will no longer store WorkingCopyPatterns
directly, instead storing only a WorkingCopyPatternsId
. On mismatch with the current op head, the user will be prompted to run jj workspace update-stale
.
This gives the user the ability to update the active WorkingCopyPatterns
whilst not interacting with the local working copy, which is useful for custom integrations which may not be able to check out particular working copy patterns due to problems with the backend (encoding, permission errors, etc.). A bad jj sparse set --add oops
command can thus be undone, even via jj op undo
if desired.
The View object will be migrated to store working copy patterns via id. The indirection will save on storage since working copy patterns are not expected to change very frequently.
// Before:\npub wc_commit_ids: HashMap<WorkspaceId, CommitId>,\n\n// After:\npub struct WorkingCopyInfo {\n pub commit_id: CommitId,\n pub wc_patterns_id: WorkingCopyPatternsId,\n}\n...\npub wc_info: HashMap<WorkspaceId, WorkingCopyInfo>,\n
A View object with no stored working copy patterns will be modified at read time to include the current working copy patterns, thus all read_view
operations will need to pass in the current working copy patterns for a migration period of at least 6 months. After that, we may choose to auto-fill missing working copy infos with a default WorkingCopyPatterns
as needed.
Perforce client maps are very similar in concept to the entirety of WorkingCopyPatterns
, and this design aims to achieve similar functionality.
The Josh Project implements partial git clones in a way similar to how sparse patterns try to work.
"},{"location":"design/sparse-v2/#patterns-via-configuration","title":"Patterns via configuration","text":"There may be some scenarios where it is valuable to configure working copy patterns via a configuration file, rather than through explicit commands. Generally this only makes sense for automated repos, with the configuration coming from outside the repo - there are too many caveats and edge cases if the configuration comes from inside the repo and/or is fought with by a human.
No configuration syntax is planned at this time but if we add any, we should probably reuse the compact line syntaxes as much as possible for consistency.
"},{"location":"design/tracking-branches/","title":"Remote/@git
tracking branches","text":"This is a plan to implement more Git-like remote tracking branch UX.
"},{"location":"design/tracking-branches/#objective","title":"Objective","text":"jj
imports all remote branches to local branches by default. As described in #1136, this doesn't interact nicely with Git if we have multiple Git remotes with a number of branches. The git.auto-local-bookmark
config can mitigate this problem, but we'll get locally-deleted branches instead.
The goal of this plan is to implement
Under the current model, all remote branches are \"tracking\" branches, and remote changes are merged into the local counterparts.
branches\n [name]:\n local_target?\n remote_targets[remote]: target\ntags\n [name]: target\ngit_refs\n [\"refs/heads/{name}\"]: target # last-known local branches\n [\"refs/remotes/{remote}/{name}\"]: target # last-known remote branches\n # (copied to remote_targets)\n [\"refs/tags/{name}\"]: target # last-known tags\ngit_head: target?\n
branches[name].remote_targets
and git_refs[\"refs/remotes\"]
. These two are mostly kept in sync, but there are two scenarios where remote-tracking branches and git refs can diverge:jj branch forget
jj op undo
/restore
in colocated repo@git
tracking branches are stored in git_refs[\"refs/heads\"]
. We need special case to resolve @git
branches, and their behavior is slightly different from the other remote-tracking branches.We'll add a per-remote-branch state
to distinguish non-tracking branches from tracking ones.
state = new # not merged in the local branch or tag\n | tracking # merged in the local branch or tag\n# `ignored` state could be added if we want to manage it by view, not by\n# config file. target of ignored remote branch would be absent.\n
We'll add a per-remote view-like object to record the last known remote branches. It will replace branches[name].remote_targets
in the current model. @git
branches will be stored in remotes[\"git\"]
.
branches\n [name]: target\ntags\n [name]: target\nremotes\n [\"git\"]:\n branches\n [name]: target, state # refs/heads/{name}\n tags\n [name]: target, state = tracking # refs/tags/{name}\n head: target?, state = TBD # refs/HEAD\n [remote]:\n branches\n [name]: target, state # refs/remotes/{remote}/{name}\n tags: (empty)\n head: (empty)\ngit_refs # last imported/exported refs\n [\"refs/heads/{name}\"]: target\n [\"refs/remotes/{remote}/{name}\"]: target\n [\"refs/tags/{name}\"]: target\n
With the proposed data model, we can
branches[name].remote_targets
and git_refs[\"refs/remotes\"]
export flow import flow\n ----------- -----------\n +----------------+ --.\n +------------------->|backing Git repo|---+ :\n | +----------------+ | : unchanged\n |[update] |[copy] : on \"op restore\"\n | +----------+ | :\n | +-------------->| git_refs |<------+ :\n | | +----------+ | --'\n +--[compare] [diff]--+\n | .-- +---------------+ | | --.\n | : +--->|remotes[\"git\"] | | | :\n +---: | | |<---+ | :\n : | |remotes[remote]| | : restored\n '-- | +---------------+ |[merge] : on \"op restore\"\n | | : by default\n [copy]| +---------------+ | :\n +----| (local) |<---------+ :\n | branches/tags | :\n +---------------+ --'\n
jj git import
applies diff between git_refs
and remotes[]
. git_refs
is always copied from the backing Git repo.jj git export
copies jj's remotes
view back to the Git repo. If a ref in the Git repo has been updated since the last import, the ref isn't exported.jj op restore
never rolls back git_refs
.The git.auto-local-bookmark
config knob is applied when importing new remote branch. jj branch
sub commands will be added to change the tracking state.
fn default_state_for_newly_imported_branch(config, remote) {\n if remote == \"git\" {\n State::Tracking\n } else if config[\"git.auto-local-bookmark\"] {\n State::Tracking\n } else {\n State::New\n }\n}\n
A branch target to be merged is calculated based on the state
.
fn target_in_merge_context(known_target, state) {\n match state {\n State::New => RefTarget::absent(),\n State::Tracking => known_target,\n }\n}\n
"},{"location":"design/tracking-branches/#mapping-to-the-current-data-model","title":"Mapping to the current data model","text":"remotes[\"git\"].branches
corresponds to git_refs[\"refs/heads\"]
, but forgotten branches are removed from remotes[\"git\"].branches
.remotes[\"git\"].tags
corresponds to git_refs[\"refs/tags\"]
.remotes[\"git\"].head
corresponds to git_head
.remotes[remote].branches
corresponds to branches[].remote_targets[remote]
.state = new|tracking
doesn't exist in the current model. It's determined by git.auto-local-bookmark
config.In the following sections, a merge is expressed as adds - removes
. In particular, a merge of local and remote targets is [local, remote] - [known_remote]
.
jj git fetch
remotes[remote].branches[glob]
(see below).tags
?jj git import
git_refs
from the backing Git repo.remotes
to the new git_refs
.git_refs[\"refs/heads\"] - remotes[\"git\"].branches
git_refs[\"refs/tags\"] - remotes[\"git\"].tags
\"HEAD\" - remotes[\"git\"].head
(unused)git_refs[\"refs/remotes/{remote}\"] - remotes[remote]
branches
and tags
if state
is tracking
.target
is absent
, the default state
should be calculated. This also applies to previously-forgotten branches.remotes
reflecting the import.jj git push
remotes[remote]
to the local changes.branches - remotes[remote].branches
state
is new
(i.e. untracked), the known remote branch target
is considered absent
.state
is new
, and if the local branch target
is absent
, the diff [absent, remote] - absent
is noop. So it's not allowed to push deleted branch to untracked remote.--force-with-lease
behavior?tags
~ (not implemented, but should be the same as branches
)remotes[remote]
and git_refs
reflecting the push.jj git export
branches
/tags
back to remotes[\"git\"]
.remotes[\"git\"].branches[name].state
can be set to untracked. Untracked local branches won't be exported to Git.remotes[\"git\"].branches[name]
is absent
, the default state = tracking
applies. This also applies to forgotten branches.tags
~ (not implemented, but should be the same as branches
)git_refs
to the new remotes[remote]
.git_refs
reflecting the export.If a ref failed to export at the step 3, the preceding steps should also be rolled back for that ref.
jj init
git.auto_local_branch
config.!git.auto_local_branch
, no tracking
state will be set.jj git clone
git.auto_local_branch
config.git.auto_local_branch
config. This isn't technically needed, but will help users coming from Git.jj branch set {name}
branches[name]
entry.jj branch delete {name}
branches[name]
entry.jj branch forget {name}
branches[name]
entry if exists.remotes[remote].branches[name]
entries if exist. TODO: maybe better to not remove non-tracking remote branches?jj branch track {name}@{remote}
(new command)[local, remote] - [absent]
in local branch.remotes[remote].branches[name].state = tracking
.jj branch untrack {name}@{remote}
(new command)remotes[remote].branches[name].state = new
.jj branch list
Note: desired behavior of jj branch forget
is to
state = new|tracking
based on git.auto_local_branch
state
is tracking
, merges [absent, new_remote] - [absent]
(i.e. creates local branch with new_remote
target)remotes[remote].branches[name].state
[local, new_remote] - [known_remote]
state = new|tracking
based on git.auto_local_branch
state
is tracking
, merges [local, new_remote] - [absent]
remotes[remote].branches[name].state
[local, absent] - [known_remote]
remotes[remote].branches[name]
(target
becomes absent
) (i.e. the remote branch is no longer tracked)state = new|tracking
based on git.auto_local_branch
[local, absent] - [absent]
-> local
state = new|tracking
based on git.auto_local_branch
state
is tracking
, merges [absent, new_remote] - [absent]
-> new_remote
remotes[remote].branches[name].state
state = new
[local, absent] - [absent]
-> local
remotes[remote].branches[name].target = local
, .state = tracking
[local, remote] - [absent]
local
moved backwards or sidewaysremotes[remote].branches[name].target = local
, .state = tracking
[local, remote] - [remote]
-> local
local
moved backwards or sideways, and if remote
is out of syncremotes[remote].branches[name].target = local
[absent, remote] - [remote]
-> absent
remote
is out of sync?remotes[remote].branches[name]
(target
becomes absent
)[absent, remote] - [absent]
-> remote
target
of forgotten remote branch is absent
remotes[\"git\"].branches[name].target = local
, .state = tracking
[local, absent] - [absent]
-> local
[local, git] - [absent]
-> failremotes[\"git\"].branches[name].target = local
[local, git] - [git]
-> local
remotes[\"git\"].branches[name]
[absent, git] - [git]
-> absent
[absent, git] - [git]
-> absent
for forgotten local/remote branches[old, git] - [git]
-> old
for undone local/remote branchesgit_refs
isn't diffed against the refs in the backing Git repo.@git
remote","text":"jj branch untrack {name}@git
jj git fetch --remote git
git::import_refs()
only for local branches.jj git push --remote git
jj branch track
and git::export_refs()
only for local branches.tracking
remotes?We're introducing a temporary process to describe how we'll gain approval to adopt permanent governance policies - basically, how we make social and technical decisions as a community. This temporary process describes how the governance working group can propose these policies and how community members can influence them and vote on them. Once permanent governance policies are in place, the temporary process will stop being used, and the permanent governance policies will be used instead.
"},{"location":"governance/temporary-voting/#context","title":"Context","text":"The governance working group was appointed by recommendation from Martin (jj's original author and current sole maintainer), without recommendation or approval from the broader jj community. This isn't a problem in itself - but it does mean that the governance working group (Austin Seipp/aseipp, Waleed Khan/arxanas, Martin von Zweigbergk/martinvonz, and Emily Shaffer/nasamuffin) needs to get some community approval before setting policy for the entire jj project. If we skip this step, we risk being perceived as exercising excessive control over the project.
"},{"location":"governance/temporary-voting/#goals-and-non-goals","title":"Goals and Non-Goals","text":"governance.md
(describing the formal structure of governance used for this project), technical design approval process, and code review process.The working group lets the community know about upcoming policy drafts they're intending to share for approval. This must happen at least a week before entering stage 3, and ideally should happen even earlier.
At this time, the working group should:
At this time, the community is invited to:
The working group will consider these recommendations in good faith, but may choose not to adopt them.
"},{"location":"governance/temporary-voting/#stage-2-proposal-review-period","title":"Stage 2: Proposal Review Period","text":"This stage lasts until the working group feels major concerns have been addressed and the proposal is ready for a vote. However, at least 72 hours must elapse between the proposal being published and the vote starting, to allow community members around the globe to read and comment. Typically, this stage should last at least one week.
At this time, the working group should:
At this time, the community is invited to:
Think of this like a code review; the goal of this stage is to build a proposal that is representative of the community's will. Keep recommendations actionable and constructive: \"This clause discourages X; if we phrase it like \"foo bar baz\" it could be less exclusive\" is much more productive than \"It's obvious that the governance working group doesn't want X!\"
At the discretion of the working group, but based on the outcome of the discussion, the proposal will go to a vote or the proposal will be dropped.
"},{"location":"governance/temporary-voting/#stage-3-proposal-voting-period","title":"Stage 3: Proposal Voting Period","text":"When the working group feels that major concerns have been addressed and is happy with the text of the proposal, the working group will open voting on the proposal.
Proposals with 2/3 or more votes in favor at the end of the voting period will be approved.
After voting has concluded, either:
Deciding whether to revise or abandon is up to the discretion of the governance working group. The working group is expected to double-check their assumption that the goals the proposal is attempting to meet are desirable after the proposal fails to be accepted.
"},{"location":"governance/temporary-voting/#stage-4-implementation","title":"Stage 4: Implementation","text":"Typically, implementation will look like merging the document with the policy into the jj codebase and remembering to use that policy in conversations moving forward.
In some cases, implementation may also involve nomination of individuals to a group or committee. When this is necessary, expect the policy being proposed to describe how these individuals will be nominated, both initially and moving into the future.
It's possible (but unlikely) that during implementation, some obstacle will arise that means the policy doesn't actually work. If this does happen, expect the working group to be transparent with the community about the situation. We may reuse some of all of this process to figure out how to move forward.
"},{"location":"technical/architecture/","title":"Architecture","text":""},{"location":"technical/architecture/#data-model","title":"Data model","text":"The commit data model is similar to Git's object model , but with some differences.
"},{"location":"technical/architecture/#separation-of-library-from-ui","title":"Separation of library from UI","text":"The jj
binary consists of two Rust crates: the library crate (jj-lib
) and the CLI crate (jj-cli
). The library crate is currently only used by the CLI crate, but it is meant to also be usable from a GUI or TUI, or in a server serving requests from multiple users. As a result, the library should avoid interacting directly with the user via the terminal or by other means; all input/output is handled by the CLI crate 1. Since the library crate is meant to usable in a server, it also cannot read configuration from the user's home directory, or from user-specific environment variables.
A lot of thought has gone into making the library crate's API easy to use, but not much has gone into \"details\" such as which collection types are used, or which symbols are exposed in the API.
"},{"location":"technical/architecture/#storage-independent-apis","title":"Storage-independent APIs","text":"One overarching principle in the design is that it should be easy to change where data is stored. The goal was to be able to put storage on local-disk by default but also be able to move storage to the cloud at Google (and for anyone). To that end, commits (and trees, files, etc.) are stored by the commit backend, operations (and views) are stored by the operation backend, the heads of the operation log are stored by the \"op heads\" backend, the commit index is stored by the index backend, and the working copy is stored by the working copy backend. The interfaces are defined in terms of plain Rust data types, not tied to a specific format. The working copy doesn't have its own trait defined yet, but its interface is small and easy to create traits for when needed.
The commit backend to use when loading a repo is specified in the .jj/repo/store/type
file. There are similar files for the other backends (.jj/repo/index/type
, .jj/repo/op_store/type
, .jj/repo/op_heads/type
).
Here's a diagram showing some important types in the library crate. The following sections describe each component.
graph TD;\n ReadonlyRepo-->Store;\n ReadonlyRepo-->OpStore;\n ReadonlyRepo-->OpHeadsStore;\n ReadonlyRepo-->ReadonlyIndex\n MutableIndex-->ReadonlyIndex;\n Store-->Backend;\n GitBackend-->Backend;\n LocalBackend-->Backend;\n LocalBackend-->StackedTable;\n MutableRepo-->ReadonlyRepo;\n MutableRepo-->MutableIndex;\n Transaction-->MutableRepo;\n WorkingCopy-->TreeState;\n Workspace-->WorkingCopy;\n Workspace-->RepoLoader;\n RepoLoader-->Store;\n RepoLoader-->OpStore;\n RepoLoader-->OpHeadsStore;\n RepoLoader-->ReadonlyRepo;\n Git-->GitBackend;\n GitBackend-->StackedTable;
"},{"location":"technical/architecture/#backend","title":"Backend","text":"The Backend
trait defines the interface each commit backend needs to implement. The current in-tree commit backends are GitBackend
and LocalBackend
.
Since there are non-commit backends, the Backend
trait should probably be renamed to CommitBackend
.
The GitBackend
stores commits in a Git repository. It uses libgit2
to read and write commits and refs.
To prevent GC from deleting commits that are still reachable from the operation log, the GitBackend
stores a ref for each commit in the operation log in the refs/jj/keep/
namespace.
Commit data that is available in Jujutsu's model but not in Git's model is stored in a StackedTable
in .jj/repo/store/extra/
. That is currently the change ID and the list of predecessors. For commits that don't have any data in that table, which is any commit created by git
, we use an empty list as predecessors, and the bit-reversed commit ID as change ID.
Because we use the Git Object ID as commit ID, two commits that differ only in their change ID, for example, will get the same commit ID, so we error out when trying to write the second one of them.
"},{"location":"technical/architecture/#localbackend","title":"LocalBackend","text":"The LocalBackend
is just a proof of concept. It stores objects addressed by their hash, with one file per object.
The Store
type wraps the Backend
and returns wrapped types for commits and trees to make them easier to use. The wrapped objects have a reference to the Store
itself, so you can do e.g. commit.parents()
without having to provide the Store
as an argument.
The Store
type also provides caching of commits and trees.
A ReadonlyRepo
represents the state of a repo at a specific operation. It keeps the view object associated with that operation.
The repository doesn't know where on disk any working copies live. It knows, via the view object, which commit is supposed to be the current working-copy commit in each workspace.
"},{"location":"technical/architecture/#mutablerepo","title":"MutableRepo","text":"A MutableRepo
is a mutable version of ReadonlyRepo
. It has a reference to its base ReadonlyRepo
, but it has its own copy of the view object and lets the caller modify it.
The Transaction
object has a MutableRepo
and metadata that will go into the operation log. When the transaction commits, the MutableRepo
becomes a view object in the operation log on disk, and the Transaction
object becomes an operation object. In memory, Transaction::commit()
returns a new ReadonlyRepo
.
The RepoLoader
represents a repository at an unspecified operation. You can think of as a pointer to the .jj/repo/
directory. It can create a ReadonlyRepo
given an operation ID.
The TreeState
type represents the state of the files in a working copy. It keep track of the mtime and size for each tracked file. It knows the TreeId
that the working copy represents. It has a snapshot()
method that will use the recorded mtimes and sizes and detect changes in the working copy. If anything changed, it will return a new TreeId
. It also has checkout()
for updating the files on disk to match a requested TreeId
.
The TreeState
type supports sparse checkouts. In fact, all working copies are sparse; they simply track the full repo in most cases.
The WorkingCopy
type has a TreeState
but also knows which WorkspaceId
it has and at which operation it was most recently updated.
The Workspace
type represents the combination of a repo and a working copy ( like Git's 'worktree' concept).
The repo view at the current operation determines the desired working-copy commit in each workspace. The WorkingCopy
determines what is actually in the working copy. The working copy can become stale if the working-copy commit was changed from another workspace (or if the process updating the working copy crashed, for example).
The git
module contains functionality for interoperating with a Git repo, at a higher level than the GitBackend
. The GitBackend
is restricted by the Backend
trait; the git
module is specifically for Git-backed repos. It has functionality for importing refs from the Git repo and for exporting to refs in the Git repo. It also has functionality for pushing and pulling to/from Git remotes.
A user-provided revset expression string goes through a few different stages to be evaluated:
RevsetExpression
, which is close to an ASTtags()
into specific commits. After this stage, the expression is still a RevsetExpression
, but it won't have any CommitRef
variants in it.visible_heads()
and all()
and produces a ResolvedExpression
.ResolvedExpression
into a Revset
.This evaluation step is performed by Index::evaluate_revset()
, allowing the Revset
implementation to leverage the specifics of a custom index implementation. The first three steps are independent of the index implementation.
StackedTable
(actually ReadonlyTable
and MutableTable
) is a simple disk format for storing key-value pairs sorted by key. The keys have to have the same size but the values can have different sizes. We use our own format because we want lock-free concurrency and there doesn't seem to be an existing key-value store we could use.
The file format contains a lookup table followed by concatenated values. The lookup table is a sorted list of keys, where each key is followed by the associated value's offset in the concatenated values.
A table can have a parent table. When looking up a key, if it's not found in the current table, the parent table is searched. We never update a table in place. If the number of new entries to write is less than half the number of entries in the parent table, we create a new table with the new entries and a pointer to the parent. Otherwise, we copy the entries from the parent table and the new entries into a new table with the grandparent as the parent. We do that recursively so parent tables are at least 2 times as large as child tables. This results in O(log N) amortized insertion time and lookup time.
There's no garbage collection of unreachable tables yet.
The tables are named by their hash. We keep a separate directory of pointers to the current leaf tables, in the same way as we do for the operation log.
"},{"location":"technical/architecture/#design-of-the-cli-crate","title":"Design of the CLI crate","text":""},{"location":"technical/architecture/#templates","title":"Templates","text":"The concept is copied from Mercurial, but the syntax is different. The main difference is that the top-level expression is a template expression, not a string like in Mercurial. There is also no string interpolation (e.g. \"Commit ID: {node}\"
in Mercurial).
Diff-editing works by creating two very sparse working copies, containing only the files we want the user to edit. We then let the user edit the right-hand side of the diff. Then we simply snapshot that working copy to create the new tree.
There are a few exceptions, such as for messages printed during automatic upgrades of the repo format\u00a0\u21a9
Concurrent editing is a key feature of DVCSs -- that's why they're called Distributed Version Control Systems. A DVCS that didn't let users edit files and create commits on separate machines at the same time wouldn't be much of a distributed VCS.
When conflicting changes are made in different clones, a DVCS will have to deal with that when you push or pull. For example, when using Mercurial, if the remote has updated a bookmark called main
(Mercurial's bookmarks are similar to a Git's branches) and you had updated the same bookmark locally but made it point to a different target, Mercurial would add a bookmark called main@origin
to indicate the conflict. Git instead prevents the conflict by renaming pulled branches to origin/main
whether or not there was a conflict. However, most DVCSs treat local concurrency quite differently, typically by using lock files to prevent concurrent edits. Unlike those DVCSs, Jujutsu treats concurrent edits the same whether they're made locally or remotely.
One problem with using lock files is that they don't work when the clone is in a distributed file system. Most clones are of course not stored in distributed file systems, but it is a big problem when they are (Mercurial repos frequently get corrupted, for example).
Another problem with using lock files is related to complexity of implementation. The simplest way of using lock files is to take coarse-grained locks early: every command that may modify the repo takes a lock at the very beginning. However, that means that operations that wouldn't actually conflict would still have to wait for each other. The user experience can be improved by using finer-grained locks and/or taking the locks later. The drawback of that is complexity. For example, you need to verify that any assumptions you made before locking are still valid after you take the lock.
To avoid depending on lock files, Jujutsu takes a different approach by accepting that concurrent changes can always happen. It instead exposes any conflicting changes to the user, much like other DVCSs do for conflicting changes made remotely.
"},{"location":"technical/concurrency/#syncing-with-rsync-nfs-dropbox-etc","title":"Syncing withrsync
, NFS, Dropbox, etc","text":"Jujutsu's lock-free concurrency means that it's possible to update copies of the clone on different machines and then let rsync
(or Dropbox, or NFS, etc.) merge them. The working copy may mismatch what's supposed to be checked out, but no changes to the repo will be lost (added commits, moved bookmarks, etc.). If conflicting changes were made, they will appear as conflicts. For example, if a bookmark was moved to two different locations, they will appear in jj log
in both locations but with a \"?\" after the name, and jj status
will also inform the user about the conflict.
Note that, for now, there are known bugs in this area. Most notably, with the Git backend, repository corruption is possible because the backend is not entirely lock-free. If you know about the bug, it is relatively easy to recover from.
Moreover, such use of Jujutsu is not currently thoroughly tested, especially in the context of co-located repositories. While the contents of commits should be safe, concurrent modification of a repository from different computers might conceivably lose some bookmark pointers. Note that, unlike in pure Git, losing a bookmark pointer does not lead to losing commits.
"},{"location":"technical/concurrency/#operation-log","title":"Operation log","text":"The most important piece in the lock-free design is the \"operation log\". That is what allows us to detect and merge divergent operations.
The operation log is similar to a commit DAG (such as in Git's object model), but each commit object is instead an \"operation\" and each tree object is instead a \"view\". The view object contains the set of visible head commits, bookmarks, tags, and the working-copy commit in each workspace. The operation object contains a pointer to the view object (like how commit objects point to tree objects), pointers to parent operation(s) (like how commit objects point to parent commit(s)), and metadata about the operation. These types are defined in op_store.proto
The operation log is normally linear. It becomes non-linear if there are divergent operations.
When a command starts, it loads the repo at the latest operation. Because the associated view object completely defines the repo state, the running command will not see any changes made by other processes thereafter. When the operation completes, it is written with the start operation as parent. The operation cannot fail to commit (except for disk failures and such). It is left for the next command to notice if there were divergent operations. It will have to be able to do that anyway since the concurrent operation could have arrived via a distributed file system. This model -- where each operation sees a consistent view of the repo and is guaranteed to be able to commit their changes -- greatly simplifies the implementation of commands.
It is possible to load the repo at a particular operation with jj --at-operation=<operation ID> <command>
. If the command is mutational, that will result in a fork in the operation log. That works exactly the same as if any later operations had not existed when the command started. In other words, running commands on a repo loaded at an earlier operation works the same way as if the operations had been concurrent. This can be useful for simulating divergent operations.
If Jujutsu tries to load the repo and finds multiple heads in the operation log, it will do a 3-way merge of the view objects based on their common ancestor (possibly several 3-way merges if there were more than two heads). Conflicts are recorded in the resulting view object. For example, if bookmark main
was moved from commit A to commit B in one operation and moved to commit C in a concurrent operation, then main
will be recorded as \"moved from A to B or C\". See the RefTarget
definition in op_store.proto
.
Because we allow bookmarks (etc.) to be in a conflicted state rather than just erroring out when there are multiple heads, the user can continue to use the repo, including performing further operations on the repo. Of course, some commands will fail when using a conflicted bookmark. For example, jj checkout main
when main
is in a conflicted state will result in an error telling you that main
resolved to multiple revisions.
The operation objects and view objects are stored in content-addressed storage just like Git commits are. That makes them safe to write without locking.
We also need a way of finding the current head of the operation log. We do that by keeping the ID of the current head(s) as a file in a directory. The ID is the name of the file; it has no contents. When an operation completes, we add a file pointing to the new operation and then remove the file pointing to the old operation. Writing the new file is what makes the operation visible (if the old file didn't get properly deleted, then future readers will take care of that). This scheme ensures that transactions are atomic.
"},{"location":"technical/conflicts/","title":"First-class conflicts","text":""},{"location":"technical/conflicts/#introduction","title":"Introduction","text":"Conflicts can happen when two changes are applied to some state. This document is about conflicts between changes to files (not about conflicts between changes to bookmark targets, for example).
For example, if you merge two branches in a repo, there may be conflicting changes between the two branches. Most DVCSs require you to resolve those conflicts before you can finish the merge operation. Jujutsu instead records the conflicts in the commit and lets you resolve the conflict when you feel like it.
"},{"location":"technical/conflicts/#data-model","title":"Data model","text":"When a merge conflict happens, it is recorded as an ordered list of tree objects linked from the commit (instead of the usual single tree per commit). There will always be an odd number of trees linked from the commit. You can think of the first tree as a start tree, and the subsequent pairs of trees to apply the diff between onto the start. Examples:
The resulting tree contents is calculated on demand. Note that we often don't need to merge the entire tree. For example, when checking out a commit in the working copy, we only need to merge parts of the tree that differs from the tree that was previously checked out in the working copy. As another example, when listing paths with conflicts, we only need to traverse parts of the tree that cannot be trivially resolved; if only one side modified lib/
, then we don't need to look for conflicts in that sub-tree.
When merging trees, if we can't resolve a sub-tree conflict trivially by looking at just the tree id, we recurse into the sub-tree. Similarly, if we can't resolve a file conflict trivially by looking at just the id, we recursive into the hunks within the file.
See here for how conflicts are stored when using the Git commit backend.
"},{"location":"technical/conflicts/#conflict-simplification","title":"Conflict simplification","text":"Remember that a 3-way merge can be written A+C-B
. If one of those states is itself a conflict, then we simply insert the conflict expression there. Then we simplify by removing canceling terms. These two steps are implemented in Merge::flatten()
and Merge::simplify()
in merge.rs
.
For example, let's say commit B is based on A and is rebased to C, where it results in conflicts (B+C-A
), which the user leaves unresolved. If the commit is then rebased to D, the result will be (B+C-A)+(D-C)
(D-C
comes from changing the base from C to D). That expression can be simplified to B+D-A
, which is a regular 3-way merge between B and D with A as base (no trace of C). This is what lets the user keep old commits rebased to head without resolving conflicts and still not get messy recursive conflicts.
As another example, let's go through what happens when you back out a conflicted commit. Let's say we have the usual B+C-A
conflict on top of non-conflict state C. We then back out that change. Backing out (\"reverting\" in Git-speak) a change means applying its reverse diff, so the result is (B+C-A)+(A-(B+C-A))
, which we can simplify to just A
(i.e. no conflict).
jj
's documentation website!","text":"The complete list of the available documentation pages is located in the sidebar on the left of the page. The sidebar may be hidden; if so, you can open it either by widening your browser window or by clicking on the hamburger menu that appears in this situation.
Additional help is available using the jj help
command if you have jj
installed.
You may want to jump to:
jj
.jj
. This version of the docs corresponds to the main
branch of the jj
repo.jj
jj
in the repo's READMEjj new/commit
?","text":"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 set
.
jj git push --all
says \"Nothing changed\" instead of pushing it. What do I do?","text":"jj git push --all
pushes all bookmarks, not all revisions. You have two options:
jj git push --change
will automatically create a bookmark and push it.jj bookmark
commands to create or move a bookmark to either the commit you want to push or a descendant on it. Unlike Git, Jujutsu doesn't do this automatically (see previous question).jj log
?","text":"Is your commit visible with jj log -r 'all()'
?
If yes, you should be aware that jj log
only shows the revisions matching revsets.log
by default. You can change it as described in config to show more revisions.
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.
"},{"location":"FAQ/#how-can-i-get-jj-log-to-show-me-what-git-log-would-show-me","title":"How can I getjj log
to show me what git log
would show me?","text":"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).
jj log
evolves?","text":"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=always\n
This will continuously update the (colored) log output in the terminal. The --ignore-working-copy
option avoids conflicts with manual operations during the creation of snapshots. Martin used watch in a tmux pane during his presentation Jujutsu - A Git-compatible VCS.
Alternatively, you can use jj-fzf, where the central piece is the jj log
view and common operations can be carried out via key bindings while the log view updates.
The wiki lists additional TUIs and GUIs beyond the terminal: GUI-and-TUI
"},{"location":"FAQ/#should-i-co-locate-my-repository","title":"Should I co-locate my repository?","text":"Co-locating 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., bisection), or you simply prefer Git in some situations.
Tooling that expects a Git repository still works (IDEs, build tooling, etc.)
The co-location documentation describes the drawbacks but the most important ones are:
Interleaving git
and jj
commands may create confusing bookmark conflicts or divergent changes.
If the working copy commit or its parent contain any conflicted files, tools expecting a Git repo may interpret the commit contents or its diff in a wrong and confusing way. You should avoid doing mutating operations with Git tools and ignore the confusing information such tools present for conflicted commits (unless you are curious about the details of how jj
stores conflicts). See #3979 for plans to improve this situation.
Jujutsu commands may be a little slower in very large repositories due to importing and exporting changes to Git. Most repositories are not noticeably affected by this.
If you primarily use Jujutsu to modify the repository, the drawbacks are unlikely to affect you. Try co-locating while you learn Jujutsu, then switch if you find a specific reason not to co-locate.
"},{"location":"FAQ/#jj-is-said-to-record-the-working-copy-after-jj-log-and-every-other-command-where-can-i-see-these-automatic-saves","title":"jj
is said to record the working copy after jj log
and every other command. Where can I see these automatic \"saves\"?","text":"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.
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.
"},{"location":"FAQ/#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","title":"Can I interactively create a new commit from only some of the changes in the working copy, likegit add -p && git commit
or hg commit -i
?","text":"Since the changes are already in the working-copy commit, the equivalent to git add -p && git commit
/git commit -p
/hg commit -i
is to split the working-copy commit with jj split -i
(or the practically identical jj commit -i
).
For the equivalent of git commit --amend -p
/hg amend -i
, use jj squash -i
.
git rebase --interactive
or hg histedit
?","text":"Not yet, you can check this issue for updates.
To reorder commits, it is for now recommended to rebase commits individually, which may require multiple invocations of jj rebase -r
or jj rebase -s
.
To squash or split commits, use jj squash
and jj split
.
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.
You can keep your notes and other scratch files in the repository, if you add a wildcard pattern to either the repo's gitignore
or your global gitignore
. Something like *.scratch
or *.scratchpad
should do, after that rename the files you want to keep around to match the pattern.
If you keep your scratch files in their own directory with no tracked files, you can create a .gitignore
file in that directory containing only *
. This will ignore everything in the directory including the .gitignore
file itself.
If $EDITOR
integration is important, something like scratchpad.*
may be more helpful, as you can keep the filename extension intact (it matches scratchpad.md
, scratchpad.rs
and more). Another option is to add a directory to the global .gitignore
which then stores all your temporary files and notes. For example, you could add scratch/
to ~/.git/ignore
and then store arbitrary files in <your-git-repo>/scratch/
.
You can find more details on gitignore
files here.
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\n@ xxxxxxxx me@example.com 2024-08-21 11:13:21 ef612875\n\u2502 Add new feature\n\u25c9 yyyyyyyy me@example.com 2024-08-21 11:13:09 main b624cf12\n\u2502 Existing work\n~\n
First, create a new commit branched from main and add your private changes:
$ jj new main -m \"private: my credentials\"\nWorking copy now at: wwwwwwww 861de9eb (empty) private: my credentials\nParent commit : yyyyyyyy b624cf12 main | Existing work\nAdded 0 files, modified 1 files, removed 0 files\n\n$ echo '{ \"password\": \"p@ssw0rd1\" }' > secret_config.json\n
Now create a merge commit with the branch you're working on and the private commit:
$ jj new xxxxxxxx wwwwwwww\nWorking copy now at: vvvvvvvv ac4d9fbe (empty) (no description set)\nParent commit : xxxxxxxx ef612875 Add new feature\nParent commit : wwwwwwww 2106921e private: my credentials\nAdded 0 files, modified 1 files, removed 0 files\n\n$ jj log\n@ vvvvvvvv me@example.com 2024-08-22 08:57:40 ac4d9fbe\n\u251c\u2500\u256e (empty) (no description set)\n\u2502 \u25c9 wwwwwwww me@example.com 2024-08-22 08:57:40 2106921e\n\u2502 \u2502 private: my credentials\n\u25c9 \u2502 xxxxxxxx me@example.com 2024-08-21 11:13:21 ef612875\n\u251c\u2500\u256f Add new feature\n\u25c9 yyyyyyyy me@example.com 2024-08-21 11:13:09 main b624cf12\n\u2502 Existing work\n~\n
Now you're ready to work:
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\n$ jj new --no-edit -A xxxxxxxx -m \"Another feature\"\nWorking copy now at: uuuuuuuu 1c3cff09 (empty) Another feature\nParent commit : xxxxxxxx ef612875 Add new feature\n\n# Insert a new commit between yyyyyyyy and vvvvvvvv\n$ jj new --no-edit -A yyyyyyyy -B vvvvvvvv -m \"Yet another feature\"\nWorking copy now at: tttttttt 938ab831 (empty) Yet another feature\nParent commit : yyyyyyyy b624cf12 Existing work\n
To avoid pushing change wwwwwwww by mistake, use the configuration git.private-commits:
$ jj config set --user git.private-commits 'description(glob:\"private:*\")'\n
"},{"location":"FAQ/#i-accidentally-changed-files-in-the-wrong-commit-how-do-i-move-the-recent-changes-into-another-commit","title":"I accidentally changed files in the wrong commit, how do I move the recent changes into another commit?","text":"Use jj evolog -p
to see how your working-copy commit has evolved. Find the commit you want to restore the contents to. Let's say the current commit (with the changes intended for a new commit) are in commit X and the state you wanted is in commit Y. Note the commit id (normally in blue at the end of the line in the log output) of each of them. Now use jj new
to create a new working-copy commit, then run jj restore --from Y --to @-
to restore the parent commit to the old state, and jj restore --from X
to restore the new working-copy commit to the new state.
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 realising.
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 behaviour. Other workflows may prefer jj edit
as well.
A divergent change represents a change that has two or more visible commits associated with it. To refer to such commits, you must use their commit ID. Most commonly, the way to resolve this is to abandon the unneeded commits (using jj abandon <commit ID>
). If you would like to keep both commits with this change ID, you can jj duplicate
one of them before abandoning it.
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 set <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 list
to show all the commits associated with it.
At the moment you'll need a script, which adds the required fields for Gerrit like the Change-Id
footer. Then jj
can invoke it via an $EDITOR
override in an aliased command. Here's an example from an contributor (look for the jj signoff
alias).
After you have attached the Change-Id:
footer to the commit series, you'll have to manually invoke git push
of HEAD
on the underlying git repository into the remote Gerrit bookmark refs/for/$BRANCH
, where $BRANCH
is the base bookmark you want your changes to go to (e.g., git push origin HEAD:refs/for/main
). Using a co-located repo will make the underlying git repo directly accessible from the working directory.
We hope to integrate with Gerrit natively in the future.
"},{"location":"FAQ/#i-want-to-write-a-tool-which-integrates-with-jujutsu-should-i-use-the-library-or-parse-the-cli","title":"I want to write a tool which integrates with Jujutsu. Should I use the library or parse the CLI?","text":"There are some trade-offs and there is no definitive answer yet.
jj-lib
avoids parsing command output and makes error handling easier.jj-lib
is not a stable API, so you may have to make changes to your tool when the API changes.jj
binaries, like the one at Google (if you're using the library, you will not be able to detect custom backends and more).Bookmarks are named pointers to revisions (just like branches are in Git). You can move them without affecting the target revision's identity. Bookmarks automatically move when revisions are rewritten (e.g. by jj rebase
). You can pass a bookmark's name to commands that want a revision as argument. For example, jj new main
will create a new revision on top of the main
bookmark. Use jj bookmark list
to list bookmarks and jj bookmark <subcommand>
to create, move, or delete bookmarks. There is currently no concept of an active/current/checked-out bookmark.
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 repos).
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 <bookmark name>@<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.
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 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.
"},{"location":"bookmarks/#terminology-summary","title":"Terminology summary","text":"jj
can find out its actual state only when it's actively communicating with the remote. However, jj
does store the last-seen position of the remote bookmark; this is the commit jj show <bookmark name>@<remote name>
would show. This notion is completely analogous to Git's \"remote-tracking bookmarks\".jj bookmark track
command, for example.jj
tries to keep in sync with the tracked remote bookmark. For example, after jj bookmark track mybookmark@origin
, there will be a local bookmark mybookmark
that's tracking the remote mybookmark@origin
bookmark. A local bookmark can track a bookmark of the same name on 0 or more remotes.The notion of tracked bookmarks serves a similar function to the Git notion of an \"upstream branch\". Unlike Git, a single local bookmark can be tracking remote bookmarks on multiple remotes, and the names of the local and remote bookmarks must match.
"},{"location":"bookmarks/#manually-tracking-a-bookmark","title":"Manually tracking a bookmark","text":"To track a bookmark permanently use jj bookmark track <bookmark name>@<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.\n$ jj bookmark list --all\n$ # Find the bookmark.\n$ # [...]\n$ # Actually track the bookmark.\n$ jj bookmark track <bookmark name>@<remote name> # Example: jj bookmark track my-feature@origin\n$ # From this point on, <bookmark name> will be imported when fetching from <remote name>.\n$ jj git fetch --remote <remote name>\n$ # A local bookmark <bookmark name> should have been created or updated while fetching.\n$ jj new <bookmark name> # Do some local testing, etc.\n
"},{"location":"bookmarks/#untracking-a-bookmark","title":"Untracking a bookmark","text":"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.\n$ jj bookmark list --all\n$ # Find the bookmark we no longer want to track.\n$ # [...]\n# # Actually untrack it.\n$ jj bookmark untrack <bookmark name>@<remote name> # Example: jj bookmark untrack stuff@origin\n$ # From this point on, this remote bookmark won't be imported anymore.\n$ # The local bookmark (e.g. stuff) is unaffected. It may or may not still\n$ # be tracking bookmarks on other remotes (e.g. stuff@upstream).\n
"},{"location":"bookmarks/#listing-tracked-bookmarks","title":"Listing tracked bookmarks","text":"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 <bookmark name>
.
git.auto-local-bookmark
option","text":"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 git.auto-local-bookmark = true
. 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 bookmarks) by default.
Currently Jujutsu automatically moves local bookmarks when these conditions are met:
You could describe the movement as following along the change-id of the current bookmark commit, even if it isn't entirely accurate.
"},{"location":"bookmarks/#pushing-bookmarks-safety-checks","title":"Pushing bookmarks: Safety checks","text":"Before jj git push
actually moves, creates, or deletes a remote bookmark, it makes several safety checks.
jj
will contact the remote and check that the actual state of the remote bookmark matches jj
's record of its last known position. If there is a conflict, jj
will refuse to push the bookmark. In this case, you need to run jj git fetch --remote <remote name>
and resolve the resulting bookmark conflict. Then, you can try jj git push
again.
If you are familiar with Git, this makes jj git push
similar to git push --force-with-lease
.
There are a few cases where jj git push
will succeed even though the remote bookmark is in an unexpected location. These are the cases where jj git fetch
would not create a bookmark conflict and would not move the local bookmark, e.g. if the unexpected location is identical to the local position of the bookmark.
The local bookmark must not be conflicted. If it is, you would need to use jj bookmark set
, for example, to resolve the conflict.
This makes jj git push
safe even if jj git fetch
is performed on a timer in the background (this situation is a known issue1 with some forms of git push --force-with-lease
). If the bookmark moves on a remote in a problematic way, jj git fetch
will create a conflict. This should ensure that the user becomes aware of the conflict before they can jj git push
and override the bookmark on the remote.
If the remote bookmark already exists on the remote, it must be tracked. If the bookmark does not already exist on the remote, there is no problem; jj git push
will create the remote bookmark and mark it as tracked.
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 'all: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).
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
can be abbreviated as jj b c BOOKMARK-NAME
.
See \"A general note on safety\" in https://git-scm.com/docs/git-push#Documentation/git-push.txt---no-force-with-lease \u21a9
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.
"},{"location":"cli-reference/#command-line-help-for-jj","title":"Command-Line Help forjj
","text":"This document contains the help content for the jj
command-line program.
Command Overview:
jj
\u21b4jj abandon
\u21b4jj backout
\u21b4jj bookmark
\u21b4jj bookmark create
\u21b4jj bookmark delete
\u21b4jj bookmark forget
\u21b4jj bookmark list
\u21b4jj bookmark move
\u21b4jj bookmark rename
\u21b4jj bookmark set
\u21b4jj bookmark track
\u21b4jj bookmark untrack
\u21b4jj commit
\u21b4jj config
\u21b4jj config edit
\u21b4jj config get
\u21b4jj config list
\u21b4jj config path
\u21b4jj config set
\u21b4jj config unset
\u21b4jj describe
\u21b4jj diff
\u21b4jj diffedit
\u21b4jj duplicate
\u21b4jj edit
\u21b4jj evolog
\u21b4jj file
\u21b4jj file annotate
\u21b4jj file chmod
\u21b4jj file list
\u21b4jj file show
\u21b4jj file track
\u21b4jj file untrack
\u21b4jj fix
\u21b4jj git
\u21b4jj git clone
\u21b4jj git export
\u21b4jj git fetch
\u21b4jj git import
\u21b4jj git init
\u21b4jj git push
\u21b4jj git remote
\u21b4jj git remote add
\u21b4jj git remote list
\u21b4jj git remote remove
\u21b4jj git remote rename
\u21b4jj git remote set-url
\u21b4jj help
\u21b4jj init
\u21b4jj interdiff
\u21b4jj log
\u21b4jj new
\u21b4jj next
\u21b4jj operation
\u21b4jj operation abandon
\u21b4jj operation diff
\u21b4jj operation log
\u21b4jj operation restore
\u21b4jj operation show
\u21b4jj operation undo
\u21b4jj parallelize
\u21b4jj prev
\u21b4jj rebase
\u21b4jj resolve
\u21b4jj restore
\u21b4jj root
\u21b4jj show
\u21b4jj simplify-parents
\u21b4jj sparse
\u21b4jj sparse edit
\u21b4jj sparse list
\u21b4jj sparse reset
\u21b4jj sparse set
\u21b4jj split
\u21b4jj squash
\u21b4jj status
\u21b4jj tag
\u21b4jj tag list
\u21b4jj util
\u21b4jj util completion
\u21b4jj util config-schema
\u21b4jj util gc
\u21b4jj util mangen
\u21b4jj util markdown-help
\u21b4jj undo
\u21b4jj version
\u21b4jj workspace
\u21b4jj workspace add
\u21b4jj workspace forget
\u21b4jj workspace list
\u21b4jj workspace rename
\u21b4jj workspace root
\u21b4jj workspace update-stale
\u21b4jj
","text":"Jujutsu (An experimental VCS)
To get started, see the tutorial at https://martinvonz.github.io/jj/latest/tutorial/.
Usage: jj [OPTIONS] [COMMAND]
'jj help --help' list available keywords. Use 'jj help -k' to show help for one of these keywords.
"},{"location":"cli-reference/#subcommands","title":"Subcommands:","text":"abandon
\u2014 Abandon a revisionbackout
\u2014 Apply the reverse of a revision on top of another revisionbookmark
\u2014 Manage bookmarkscommit
\u2014 Update the description and create a new change on topconfig
\u2014 Manage config optionsdescribe
\u2014 Update the change description or other metadata [aliases: desc]diff
\u2014 Compare file contents between two revisionsdiffedit
\u2014 Touch up the content changes in a revision with a diff editorduplicate
\u2014 Create a new change with the same content as an existing oneedit
\u2014 Sets the specified revision as the working-copy revisionevolog
\u2014 Show how a change has evolved over timefile
\u2014 File operationsfix
\u2014 Update files with formatting fixes or other changesgit
\u2014 Commands for working with Git remotes and the underlying Git repohelp
\u2014 Print this message or the help of the given subcommand(s)init
\u2014 Create a new repo in the given directoryinterdiff
\u2014 Compare the changes of two commitslog
\u2014 Show revision historynew
\u2014 Create a new, empty change and (by default) edit it in the working copynext
\u2014 Move the working-copy commit to the child revisionoperation
\u2014 Commands for working with the operation logparallelize
\u2014 Parallelize revisions by making them siblingsprev
\u2014 Change the working copy revision relative to the parent revisionrebase
\u2014 Move revisions to different parent(s)resolve
\u2014 Resolve a conflicted file with an external merge toolrestore
\u2014 Restore paths from another revisionroot
\u2014 Show the current workspace root directoryshow
\u2014 Show commit description and changes in a revisionsimplify-parents
\u2014 Simplify parent edges for the specified revision(s)sparse
\u2014 Manage which paths from the working-copy commit are present in the working copysplit
\u2014 Split a revision in twosquash
\u2014 Move changes from a revision into another revisionstatus
\u2014 Show high-level repo status [aliases: st]tag
\u2014 Manage tagsutil
\u2014 Infrequently used commands such as for generating shell completionsundo
\u2014 Undo an operation (shortcut for jj op undo
)version
\u2014 Display version informationworkspace
\u2014 Commands for working with workspaces-R
, --repository <REPOSITORY>
\u2014 Path to repository to operate on
By default, Jujutsu searches for the closest .jj/ directory in an ancestor of the current working directory.
--ignore-working-copy
\u2014 Don't snapshot the working copy, and don't update it
By default, Jujutsu snapshots the working copy at the beginning of every command. The working copy is also updated at the end of the command, if the command modified the working-copy commit (@
). If you want to avoid snapshotting the working copy and instead see a possibly stale working copy commit, you can use --ignore-working-copy
. This may be useful e.g. in a command prompt, especially if you have another process that commits the working copy.
Loading the repository at a specific operation with --at-operation
implies --ignore-working-copy
.
--ignore-immutable
\u2014 Allow rewriting immutable commits
By default, Jujutsu prevents rewriting commits in the configured set of immutable commits. This option disables that check and lets you rewrite any commit but the root commit.
This option only affects the check. It does not affect the immutable_heads()
revset or the immutable
template keyword.
--at-operation <AT_OPERATION>
\u2014 Operation to load the repo at
Operation to load the repo at. By default, Jujutsu loads the repo at the most recent operation, or at the merge of the divergent operations if any.
You can use --at-op=<operation ID>
to see what the repo looked like at an earlier operation. For example jj --at-op=<operation ID> st
will show you what jj st
would have shown you when the given operation had just finished. --at-op=@
is pretty much the same as the default except that divergent operations will never be merged.
Use jj op log
to find the operation ID you want. Any unambiguous prefix of the operation ID is enough.
When loading the repo at an earlier operation, the working copy will be ignored, as if --ignore-working-copy
had been specified.
It is possible to run mutating commands when loading the repo at an earlier operation. Doing that is equivalent to having run concurrent commands starting at the earlier operation. There's rarely a reason to do that, but it is possible.
--debug
\u2014 Enable debug logging
--color <WHEN>
\u2014 When to colorize output (always, never, debug, auto)--quiet
\u2014 Silence non-primary command output
For example, jj file list
will still list files, but it won't tell you if the working copy was snapshotted or if descendants were rebased.
Warnings and errors will still be printed.
--no-pager
\u2014 Disable the pager
--config-toml <TOML>
\u2014 Additional configuration options (can be repeated)jj abandon
","text":"Abandon a revision
Abandon a revision, rebasing descendants onto its parent(s). The behavior is similar to jj restore --changes-in
; the difference is that jj abandon
gives you a new change, while jj restore
updates the existing change.
If a working-copy commit gets abandoned, it will be given a new, empty commit. This is true in general; it is not specific to this command.
Usage: jj abandon [OPTIONS] [REVISIONS]...
<REVISIONS>
\u2014 The revision(s) to abandon
Default value: @
-s
, --summary
\u2014 Do not print every abandoned commit on a separate line--restore-descendants
\u2014 Do not modify the content of the children of the abandoned commitsjj backout
","text":"Apply the reverse of a revision on top of another revision
Usage: jj backout [OPTIONS]
-r
, --revisions <REVISIONS>
\u2014 The revision(s) to apply the reverse of
Default value: @
-d
, --destination <DESTINATION>
\u2014 The revision to apply the reverse changes on top of
Default value: @
jj bookmark
","text":"Manage bookmarks
For information about bookmarks, see https://martinvonz.github.io/jj/latest/bookmarks.
Usage: jj bookmark <COMMAND>
create
\u2014 Create a new bookmarkdelete
\u2014 Delete an existing bookmark and propagate the deletion to remotes on the next pushforget
\u2014 Forget everything about a bookmark, including its local and remote targetslist
\u2014 List bookmarks and their targetsmove
\u2014 Move existing bookmarks to target revisionrename
\u2014 Rename old
bookmark name to new
bookmark nameset
\u2014 Create or update a bookmark to point to a certain committrack
\u2014 Start tracking given remote bookmarksuntrack
\u2014 Stop tracking given remote bookmarksjj bookmark create
","text":"Create a new bookmark
Usage: jj bookmark create [OPTIONS] <NAMES>...
<NAMES>
\u2014 The bookmarks to create-r
, --revision <REVISION>
\u2014 The bookmark's target revisionjj bookmark delete
","text":"Delete an existing bookmark and propagate the deletion to remotes on the next push
Usage: jj bookmark delete <NAMES>...
<NAMES>
\u2014 The bookmarks to delete
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
jj bookmark forget
","text":"Forget everything about a bookmark, including its local and remote targets
A forgotten bookmark will not impact remotes on future pushes. It will be recreated on future pulls if it still exists in the remote.
Usage: jj bookmark forget <NAMES>...
<NAMES>
\u2014 The bookmarks to forget
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
jj bookmark list
","text":"List bookmarks and their targets
By default, a tracking remote bookmark will be included only if its target is different from the local target. A non-tracking remote bookmark won't be listed. For a conflicted bookmark (both local and remote), old target revisions are preceded by a \"-\" and new target revisions are preceded by a \"+\".
For information about bookmarks, see https://martinvonz.github.io/jj/latest/bookmarks/.
Usage: jj bookmark list [OPTIONS] [NAMES]...
<NAMES>
\u2014 Show bookmarks whose local name matches
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
-a
, --all-remotes
\u2014 Show all tracking and non-tracking remote bookmarks including the ones whose targets are synchronized with the local bookmarks--remote <REMOTE>
\u2014 Show all tracking and non-tracking remote bookmarks belonging to this remote
Can be combined with --tracked
or --conflicted
to filter the bookmarks shown (can be repeated.)
By default, the specified remote name matches exactly. Use glob:
prefix to select remotes by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
-t
, --tracked
\u2014 Show remote tracked bookmarks only. Omits local Git-tracking bookmarks by default
-c
, --conflicted
\u2014 Show conflicted bookmarks only-r
, --revisions <REVISIONS>
\u2014 Show bookmarks whose local targets are in the given revisions
Note that -r deleted_bookmark
will not work since deleted_bookmark
wouldn't have a local target.
-T
, --template <TEMPLATE>
\u2014 Render each bookmark using the given template
All 0-argument methods of the RefName
type are available as keywords.
For the syntax, see https://martinvonz.github.io/jj/latest/templates/
jj bookmark move
","text":"Move existing bookmarks to target revision
If bookmark names are given, the specified bookmarks will be updated to point to the target revision.
If --from
options are given, bookmarks currently pointing to the specified revisions will be updated. The bookmarks can also be filtered by names.
Example: pull up the nearest bookmarks to the working-copy parent
$ jj bookmark move --from 'heads(::@- & bookmarks())' --to @-
Usage: jj bookmark move [OPTIONS] <--from <REVISIONS>|NAMES>
<NAMES>
\u2014 Move bookmarks matching the given name patterns
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
--from <REVISIONS>
\u2014 Move bookmarks from the given revisions--to <REVISION>
\u2014 Move bookmarks to this revision
Default value: @
-B
, --allow-backwards
\u2014 Allow moving bookmarks backwards or sideways
jj bookmark rename
","text":"Rename old
bookmark name to new
bookmark name
The new bookmark name points at the same commit as the old bookmark name.
Usage: jj bookmark rename <OLD> <NEW>
<OLD>
\u2014 The old name of the bookmark<NEW>
\u2014 The new name of the bookmarkjj bookmark set
","text":"Create or update a bookmark to point to a certain commit
Usage: jj bookmark set [OPTIONS] <NAMES>...
<NAMES>
\u2014 The bookmarks to update-r
, --revision <REVISION>
\u2014 The bookmark's target revision-B
, --allow-backwards
\u2014 Allow moving the bookmark backwards or sidewaysjj bookmark track
","text":"Start tracking given remote bookmarks
A tracking remote bookmark will be imported as a local bookmark of the same name. Changes to it will propagate to the existing local bookmark on future pulls.
Usage: jj bookmark track <BOOKMARK@REMOTE>...
<BOOKMARK@REMOTE>
\u2014 Remote bookmarks to track
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
Examples: bookmark@remote, glob:main@, glob:jjfan-@upstream
jj bookmark untrack
","text":"Stop tracking given remote bookmarks
A non-tracking remote bookmark is just a pointer to the last-fetched remote bookmark. It won't be imported as a local bookmark on future pulls.
Usage: jj bookmark untrack <BOOKMARK@REMOTE>...
<BOOKMARK@REMOTE>
\u2014 Remote bookmarks to untrack
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
Examples: bookmark@remote, glob:main@, glob:jjfan-@upstream
jj commit
","text":"Update the description and create a new change on top
Usage: jj commit [OPTIONS] [PATHS]...
<PATHS>
\u2014 Put these paths in the first commit-i
, --interactive
\u2014 Interactively choose which changes to include in the first commit--tool <NAME>
\u2014 Specify diff editor to be used (implies --interactive)-m
, --message <MESSAGE>
\u2014 The change description to use (don't open editor)--reset-author
\u2014 Reset the author to the configured user
This resets the author name, email, and timestamp.
You can use it in combination with the JJ_USER and JJ_EMAIL environment variables to set a different author:
$ JJ_USER='Foo Bar' JJ_EMAIL=foo@bar.com jj commit --reset-author
--author <AUTHOR>
\u2014 Set author to the provided string
This changes author name and email while retaining author timestamp for non-discardable commits.
jj config
","text":"Manage config options
Operates on jj configuration, which comes from the config file and environment variables.
For file locations, supported config options, and other details about jj config, see https://martinvonz.github.io/jj/latest/config/.
Usage: jj config <COMMAND>
edit
\u2014 Start an editor on a jj config fileget
\u2014 Get the value of a given config option.list
\u2014 List variables set in config file, along with their valuespath
\u2014 Print the path to the config fileset
\u2014 Update config file to set the given option to a given valueunset
\u2014 Update config file to unset the given optionjj config edit
","text":"Start an editor on a jj config file.
Creates the file if it doesn't already exist regardless of what the editor does.
Usage: jj config edit <--user|--repo>
--user
\u2014 Target the user-level config--repo
\u2014 Target the repo-level configjj config get
","text":"Get the value of a given config option.
Unlike jj config list
, the result of jj config get
is printed without extra formatting and therefore is usable in scripting. For example:
$ jj config list user.name user.name=\"Martin von Zweigbergk\" $ jj config get user.name Martin von Zweigbergk
Usage: jj config get <NAME>
<NAME>
jj config list
","text":"List variables set in config file, along with their values
Usage: jj config list [OPTIONS] [NAME]
<NAME>
\u2014 An optional name of a specific config option to look up--include-defaults
\u2014 Whether to explicitly include built-in default values in the list--include-overridden
\u2014 Allow printing overridden values--user
\u2014 Target the user-level config--repo
\u2014 Target the repo-level config-T
, --template <TEMPLATE>
\u2014 Render each variable using the given template
The following keywords are defined:
name: String
: Config name.value: String
: Serialized value in TOML syntax.overridden: Boolean
: True if the value is shadowed by other.For the syntax, see https://martinvonz.github.io/jj/latest/templates/
jj config path
","text":"Print the path to the config file
A config file at that path may or may not exist.
See jj config edit
if you'd like to immediately edit the file.
Usage: jj config path <--user|--repo>
--user
\u2014 Target the user-level config--repo
\u2014 Target the repo-level configjj config set
","text":"Update config file to set the given option to a given value
Usage: jj config set <--user|--repo> <NAME> <VALUE>
<NAME>
<VALUE>
--user
\u2014 Target the user-level config--repo
\u2014 Target the repo-level configjj config unset
","text":"Update config file to unset the given option
Usage: jj config unset <--user|--repo> <NAME>
<NAME>
--user
\u2014 Target the user-level config--repo
\u2014 Target the repo-level configjj describe
","text":"Update the change description or other metadata [aliases: desc]
Starts an editor to let you edit the description of changes. The editor will be $EDITOR, or pico
if that's not defined (Notepad
on Windows).
Usage: jj describe [OPTIONS] [REVISIONS]...
<REVISIONS>
\u2014 The revision(s) whose description to edit
Default value: @
-m
, --message <MESSAGE>
\u2014 The change description to use (don't open editor)
If multiple revisions are specified, the same description will be used for all of them.
--stdin
\u2014 Read the change description from stdin
If multiple revisions are specified, the same description will be used for all of them.
--no-edit
\u2014 Don't open an editor
This is mainly useful in combination with e.g. --reset-author
.
--reset-author
\u2014 Reset the author to the configured user
This resets the author name, email, and timestamp.
You can use it in combination with the JJ_USER and JJ_EMAIL environment variables to set a different author:
$ JJ_USER='Foo Bar' JJ_EMAIL=foo@bar.com jj describe --reset-author
--author <AUTHOR>
\u2014 Set author to the provided string
This changes author name and email while retaining author timestamp for non-discardable commits.
jj diff
","text":"Compare file contents between two revisions
With the -r
option, which is the default, shows the changes compared to the parent revision. If there are several parent revisions (i.e., the given revision is a merge), then they will be merged and the changes from the result to the given revision will be shown.
With the --from
and/or --to
options, shows the difference from/to the given revisions. If either is left out, it defaults to the working-copy commit. For example, jj diff --from main
shows the changes from \"main\" (perhaps a bookmark name) to the working-copy commit.
Usage: jj diff [OPTIONS] [PATHS]...
<PATHS>
\u2014 Restrict the diff to these paths-r
, --revision <REVISION>
\u2014 Show changes in this revision, compared to its parent(s)
If the revision is a merge commit, this shows changes from the automatic merge of the contents of all of its parents to the contents of the revision itself.
--from <FROM>
\u2014 Show changes from this revision
--to <TO>
\u2014 Show changes to this revision-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show-w
, --ignore-all-space
\u2014 Ignore whitespace when comparing lines-b
, --ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj diffedit
","text":"Touch up the content changes in a revision with a diff editor
With the -r
option, which is the default, starts a diff editor on the changes in the revision.
With the --from
and/or --to
options, starts a diff editor comparing the \"from\" revision to the \"to\" revision.
Edit the right side of the diff until it looks the way you want. Once you close the editor, the revision specified with -r
or --to
will be updated. Unless --restore-descendants
is used, descendants will be rebased on top as usual, which may result in conflicts.
See jj restore
if you want to move entire files from one revision to another. For moving changes between revisions, see jj squash -i
.
Usage: jj diffedit [OPTIONS]
-r
, --revision <REVISION>
\u2014 The revision to touch up
Defaults to @ if neither --to nor --from are specified.
--from <FROM>
\u2014 Show changes from this revision
Defaults to @ if --to is specified.
--to <TO>
\u2014 Edit changes in this revision
Defaults to @ if --from is specified.
--tool <NAME>
\u2014 Specify diff editor to be used
--restore-descendants
\u2014 Preserve the content (not the diff) when rebasing descendants
When rebasing a descendant on top of the rewritten revision, its diff compared to its parent(s) is normally preserved, i.e. the same way that descendants are always rebased. This flag makes it so the content/state is preserved instead of preserving the diff.
jj duplicate
","text":"Create a new change with the same content as an existing one
Usage: jj duplicate [REVISIONS]...
<REVISIONS>
\u2014 The revision(s) to duplicate
Default value: @
jj edit
","text":"Sets the specified revision as the working-copy revision
Note: it is generally recommended to instead use jj new
and jj squash
.
For more information, see https://martinvonz.github.io/jj/latest/FAQ#how-do-i-resume-working-on-an-existing-change
Usage: jj edit <REVISION>
<REVISION>
\u2014 The commit to editjj evolog
","text":"Show how a change has evolved over time
Lists the previous commits which a change has pointed to. The current commit of a change evolves when the change is updated, rebased, etc.
Usage: jj evolog [OPTIONS]
-r
, --revision <REVISION>
Default value: @
-n
, --limit <LIMIT>
\u2014 Limit number of revisions to show
--no-graph
\u2014 Don't show the graph, show a flat list of revisions-T
, --template <TEMPLATE>
\u2014 Render each revision using the given template
For the syntax, see https://martinvonz.github.io/jj/latest/templates/
-p
, --patch
\u2014 Show patch compared to the previous version of this change
If the previous version has different parents, it will be temporarily rebased to the parents of the new version, so the diff is not contaminated by unrelated changes.
-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted
--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show--ignore-all-space
\u2014 Ignore whitespace when comparing lines--ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj file
","text":"File operations
Usage: jj file <COMMAND>
annotate
\u2014 Show the source change for each line of the target filechmod
\u2014 Sets or removes the executable bit for paths in the repolist
\u2014 List files in a revisionshow
\u2014 Print contents of files in a revisiontrack
\u2014 Start tracking specified paths in the working copyuntrack
\u2014 Stop tracking specified paths in the working copyjj file annotate
","text":"Show the source change for each line of the target file.
Annotates a revision line by line. Each line includes the source change that introduced the associated line. A path to the desired file must be provided. The per-line prefix for each line can be customized via template with the templates.annotate_commit_summary
config variable.
Usage: jj file annotate [OPTIONS] <PATH>
<PATH>
\u2014 the file to annotate-r
, --revision <REVISION>
\u2014 an optional revision to start atjj file chmod
","text":"Sets or removes the executable bit for paths in the repo
Unlike the POSIX chmod
, jj file chmod
also works on Windows, on conflicted files, and on arbitrary revisions.
Usage: jj file chmod [OPTIONS] <MODE> <PATHS>...
<MODE>
Possible values:
n
: Make a path non-executable (alias: normal)x
: Make a path executable (alias: executable)<PATHS>
\u2014 Paths to change the executable bit for
-r
, --revision <REVISION>
\u2014 The revision to update
Default value: @
jj file list
","text":"List files in a revision
Usage: jj file list [OPTIONS] [PATHS]...
<PATHS>
\u2014 Only list files matching these prefixes (instead of all files)-r
, --revision <REVISION>
\u2014 The revision to list files in
Default value: @
jj file show
","text":"Print contents of files in a revision
If the given path is a directory, files in the directory will be visited recursively.
Usage: jj file show [OPTIONS] <PATHS>...
<PATHS>
\u2014 Paths to print-r
, --revision <REVISION>
\u2014 The revision to get the file contents from
Default value: @
jj file track
","text":"Start tracking specified paths in the working copy
Without arguments, all paths that are not ignored will be tracked.
New files in the working copy can be automatically tracked. You can configure which paths to automatically track by setting snapshot.auto-track
(e.g. to \"none()\"
or \"glob:**/*.rs\"
). Files that don't match the pattern can be manually tracked using this command. The default pattern is all()
and this command has no effect.
Usage: jj file track <PATHS>...
<PATHS>
\u2014 Paths to trackjj file untrack
","text":"Stop tracking specified paths in the working copy
Usage: jj file untrack <PATHS>...
<PATHS>
\u2014 Paths to untrack. They must already be ignored.
The paths could be ignored via a .gitignore or .git/info/exclude (in colocated repos).
jj fix
","text":"Update files with formatting fixes or other changes
The primary use case for this command is to apply the results of automatic code formatting tools to revisions that may not be properly formatted yet. It can also be used to modify files with other tools like sed
or sort
.
The changed files in the given revisions will be updated with any fixes determined by passing their file content through any external tools the user has configured for those files. Descendants will also be updated by passing their versions of the same files through the same tools, which will ensure that the fixes are not lost. This will never result in new conflicts. Files with existing conflicts will be updated on all sides of the conflict, which can potentially increase or decrease the number of conflict markers.
The external tools must accept the current file content on standard input, and return the updated file content on standard output. A tool's output will not be used unless it exits with a successful exit code. Output on standard error will be passed through to the terminal.
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 the substring $path
, which will be replaced with the repo-relative path of the file being fixed. It is useful to provide the path to tools that include the path in error messages, or behave differently based on the directory or file name.patterns
: Determines which files the tool will affect. 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.For example, the following configuration defines how two code formatters (clang-format
and black
) will apply to three different file extensions (.cc
, .h
, and .py
):
[fix.tools.clang-format]\ncommand = [\"/usr/bin/clang-format\", \"--assume-filename=$path\"]\npatterns = [\"glob:'**/*.cc'\",\n \"glob:'**/*.h'\"]\n\n[fix.tools.black]\ncommand = [\"/usr/bin/black\", \"-\", \"--stdin-filename=$path\"]\npatterns = [\"glob:'**/*.py'\"]\n
Execution order of tools that affect the same file is deterministic, but currently unspecified, and may change between releases. If two tools affect the same file, the second tool to run will receive its input from the output of the first tool.
There is also a deprecated configuration schema that defines a single command that will affect all changed files in the specified revisions. For example, the following configuration would apply the Rust formatter to all changed files (whether they are Rust files or not):
[fix]\ntool-command = [\"rustfmt\", \"--emit\", \"stdout\"]\n
The tool defined by tool-command
acts as if it was the first entry in fix.tools
, and uses pattern = \"all()\"``. Support for
tool-command` will be removed in a future version.
Usage: jj fix [OPTIONS] [PATHS]...
<PATHS>
\u2014 Fix only these paths-s
, --source <SOURCE>
\u2014 Fix files in the specified revision(s) and their descendants. If no revisions are specified, this defaults to the revsets.fix
setting, or reachable(@, mutable())
if it is not set--include-unchanged-files
\u2014 Fix unchanged files in addition to changed ones. If no paths are specified, all files in the repo will be fixedjj git
","text":"Commands for working with Git remotes and the underlying Git repo
For a comparison with Git, including a table of commands, see https://martinvonz.github.io/jj/latest/git-comparison/.
Usage: jj git <COMMAND>
clone
\u2014 Create a new repo backed by a clone of a Git repoexport
\u2014 Update the underlying Git repo with changes made in the repofetch
\u2014 Fetch from a Git remoteimport
\u2014 Update repo with changes made in the underlying Git repoinit
\u2014 Create a new Git backed repopush
\u2014 Push to a Git remoteremote
\u2014 Manage Git remotesjj git clone
","text":"Create a new repo backed by a clone of a Git repo
The Git repo will be a bare git repo stored inside the .jj/
directory.
Usage: jj git clone [OPTIONS] <SOURCE> [DESTINATION]
<SOURCE>
\u2014 URL or path of the Git repo to clone<DESTINATION>
\u2014 Specifies the target directory for the Jujutsu repository clone. If not provided, defaults to a directory named after the last component of the source URL. The full directory path will be created if it doesn't exist--remote <REMOTE_NAME>
\u2014 Name of the newly created remote
Default value: origin
--colocate
\u2014 Whether or not to colocate the Jujutsu repo with the git repo
--depth <DEPTH>
\u2014 Create a shallow clone of the given depthjj git export
","text":"Update the underlying Git repo with changes made in the repo
Usage: jj git export
jj git fetch
","text":"Fetch from a Git remote
If a working-copy commit gets abandoned, it will be given a new, empty commit. This is true in general; it is not specific to this command.
Usage: jj git fetch [OPTIONS]
-b
, --branch <BRANCH>
\u2014 Fetch only some of the branches
By default, the specified name matches exactly. Use glob:
prefix to expand *
as a glob. The other wildcard characters aren't supported.
Default value: glob:*
--remote <REMOTE>
\u2014 The remote to fetch from (only named remotes are supported, can be repeated)
--all-remotes
\u2014 Fetch from all remotesjj git import
","text":"Update repo with changes made in the underlying Git repo
If a working-copy commit gets abandoned, it will be given a new, empty commit. This is true in general; it is not specific to this command.
Usage: jj git import
jj git init
","text":"Create a new Git backed repo
Usage: jj git init [OPTIONS] [DESTINATION]
<DESTINATION>
\u2014 The destination directory where the jj
repo will be created. If the directory does not exist, it will be created. If no directory is given, the current directory is used.
By default the git
repo is under $destination/.jj
Default value: .
--colocate
\u2014 Specifies that the jj
repo should also be a valid git
repo, allowing the use of both jj
and git
commands in the same directory.
This is done by placing the backing git repo into a .git
directory in the root of the jj
repo along with the .jj
directory. If the .git
directory already exists, all the existing commits will be imported.
This option is mutually exclusive with --git-repo
.
--git-repo <GIT_REPO>
\u2014 Specifies a path to an existing git repository to be used as the backing git repo for the newly created jj
repo.
If the specified --git-repo
path happens to be the same as the jj
repo path (both .jj and .git directories are in the same working directory), then both jj
and git
commands will work on the same repo. This is called a co-located repo.
This option is mutually exclusive with --colocate
.
jj git push
","text":"Push to a Git remote
By default, pushes any bookmarks pointing to remote_bookmarks(remote=<remote>)..@
. Use --bookmark
to push specific bookmarks. Use --all
to push all bookmarks. Use --change
to generate bookmark names based on the change IDs of specific commits.
Before the command actually moves, creates, or deletes a remote bookmark, it makes several safety checks. If there is a problem, you may need to run jj git fetch --remote <remote name>
and/or resolve some bookmark conflicts.
Usage: jj git push [OPTIONS]
--remote <REMOTE>
\u2014 The remote to push to (only named remotes are supported)-b
, --bookmark <BOOKMARK>
\u2014 Push only this bookmark, or bookmarks matching a pattern (can be repeated)
By default, the specified name matches exactly. Use glob:
prefix to select bookmarks by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets#string-patterns.
--all
\u2014 Push all bookmarks (including deleted bookmarks)
--tracked
\u2014 Push all tracked bookmarks (including deleted bookmarks)
This usually means that the bookmark was already pushed to or fetched from the relevant remote. For details, see https://martinvonz.github.io/jj/latest/bookmarks#remotes-and-tracked-bookmarks
--deleted
\u2014 Push all deleted bookmarks
Only tracked bookmarks can be successfully deleted on the remote. A warning will be printed if any untracked bookmarks on the remote correspond to missing local bookmarks.
--allow-empty-description
\u2014 Allow pushing commits with empty descriptions
--allow-private
\u2014 Allow pushing commits that are private-r
, --revisions <REVISIONS>
\u2014 Push bookmarks pointing to these commits (can be repeated)-c
, --change <CHANGE>
\u2014 Push this commit by creating a bookmark based on its change ID (can be repeated)--dry-run
\u2014 Only display what will change on the remotejj git remote
","text":"Manage Git remotes
The Git repo will be a bare git repo stored inside the .jj/
directory.
Usage: jj git remote <COMMAND>
add
\u2014 Add a Git remotelist
\u2014 List Git remotesremove
\u2014 Remove a Git remote and forget its bookmarksrename
\u2014 Rename a Git remoteset-url
\u2014 Set the URL of a Git remotejj git remote add
","text":"Add a Git remote
Usage: jj git remote add <REMOTE> <URL>
<REMOTE>
\u2014 The remote's name<URL>
\u2014 The remote's URLjj git remote list
","text":"List Git remotes
Usage: jj git remote list
jj git remote remove
","text":"Remove a Git remote and forget its bookmarks
Usage: jj git remote remove <REMOTE>
<REMOTE>
\u2014 The remote's namejj git remote rename
","text":"Rename a Git remote
Usage: jj git remote rename <OLD> <NEW>
<OLD>
\u2014 The name of an existing remote<NEW>
\u2014 The desired name for old
jj git remote set-url
","text":"Set the URL of a Git remote
Usage: jj git remote set-url <REMOTE> <URL>
<REMOTE>
\u2014 The remote's name<URL>
\u2014 The desired url for remote
jj help
","text":"Print this message or the help of the given subcommand(s)
Usage: jj help [OPTIONS] [COMMAND]...
<COMMAND>
\u2014 Print help for the subcommand(s)-k
, --keyword <KEYWORD>
\u2014 Show help for keywords instead of commands
Possible values:
revsets
: A functional language for selecting a set of revisiontutorial
: Show a tutorial to get started with jjjj init
","text":"Create a new repo in the given directory
If the given directory does not exist, it will be created. If no directory is given, the current directory is used.
Usage: jj init [DESTINATION]
<DESTINATION>
\u2014 The destination directory
Default value: .
jj interdiff
","text":"Compare the changes of two commits
This excludes changes from other commits by temporarily rebasing --from
onto --to
's parents. If you wish to compare the same change across versions, consider jj evolog -p
instead.
Usage: jj interdiff [OPTIONS] <--from <FROM>|--to <TO>> [PATHS]...
<PATHS>
\u2014 Restrict the diff to these paths--from <FROM>
\u2014 Show changes from this revision--to <TO>
\u2014 Show changes to this revision-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show-w
, --ignore-all-space
\u2014 Ignore whitespace when comparing lines-b
, --ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj log
","text":"Show revision history
Renders a graphical view of the project's history, ordered with children before parents. By default, the output only includes mutable revisions, along with some additional revisions for context.
Spans of revisions that are not included in the graph per --revisions
are rendered as a synthetic node labeled \"(elided revisions)\".
Usage: jj log [OPTIONS] [PATHS]...
<PATHS>
\u2014 Show revisions modifying the given paths-r
, --revisions <REVISIONS>
\u2014 Which revisions to show
If no paths nor revisions are specified, this defaults to the revsets.log
setting.
--reversed
\u2014 Show revisions in the opposite order (older revisions first)
-n
, --limit <LIMIT>
\u2014 Limit number of revisions to show
Applied after revisions are filtered and reordered.
--no-graph
\u2014 Don't show the graph, show a flat list of revisions
-T
, --template <TEMPLATE>
\u2014 Render each revision using the given template
For the syntax, see https://martinvonz.github.io/jj/latest/templates/
-p
, --patch
\u2014 Show patch
-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show--ignore-all-space
\u2014 Ignore whitespace when comparing lines--ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj new
","text":"Create a new, empty change and (by default) edit it in the working copy
By default, jj
will edit the new change, making the working copy represent the new commit. This can be avoided with --no-edit
.
Note that you can create a merge commit by specifying multiple revisions as argument. For example, jj new main @
will create a new commit with the main
bookmark and the working copy as parents.
For more information, see https://martinvonz.github.io/jj/latest/working-copy/.
Usage: jj new [OPTIONS] [REVISIONS]...
<REVISIONS>
\u2014 Parent(s) of the new change
Default value: @
-m
, --message <MESSAGE>
\u2014 The change description to use--no-edit
\u2014 Do not edit the newly created change-A
, --insert-after <INSERT_AFTER>
\u2014 Insert the new change after the given commit(s)-B
, --insert-before <INSERT_BEFORE>
\u2014 Insert the new change before the given commit(s)jj next
","text":"Move the working-copy commit to the child revision
The command creates a new empty working copy revision that is the child of a descendant offset
revisions ahead of the parent of the current working copy.
For example, when the offset is 1:
D D @\n| |/\nC @ => C\n|/ |\nB B\n
If --edit
is passed, the working copy revision is changed to the child of the current working copy revision.
D D\n| |\nC C\n| |\nB => @\n| |\n@ A\n
Usage: jj next [OPTIONS] [OFFSET]
<OFFSET>
\u2014 How many revisions to move forward. Advances to the next child by default
Default value: 1
-e
, --edit
\u2014 Instead of creating a new working-copy commit on top of the target commit (like jj new
), edit the target commit directly (like jj edit
)
Takes precedence over config in ui.movement.edit
; i.e. will negate ui.movement.edit = false
-n
, --no-edit
\u2014 The inverse of --edit
Takes precedence over config in ui.movement.edit
; i.e. will negate ui.movement.edit = true
--conflict
\u2014 Jump to the next conflicted descendant
jj operation
","text":"Commands for working with the operation log
For information about the operation log, see https://martinvonz.github.io/jj/latest/operation-log/.
Usage: jj operation <COMMAND>
abandon
\u2014 Abandon operation historydiff
\u2014 Compare changes to the repository between two operationslog
\u2014 Show the operation logrestore
\u2014 Create a new operation that restores the repo to an earlier stateshow
\u2014 Show changes to the repository in an operationundo
\u2014 Create a new operation that undoes an earlier operationjj operation abandon
","text":"Abandon operation history
To discard old operation history, use jj op abandon ..<operation ID>
. It will abandon the specified operation and all its ancestors. The descendants will be reparented onto the root operation.
To discard recent operations, use jj op restore <operation ID>
followed by jj op abandon <operation ID>..@-
.
The abandoned operations, commits, and other unreachable objects can later be garbage collected by using jj util gc
command.
Usage: jj operation abandon <OPERATION>
<OPERATION>
\u2014 The operation or operation range to abandonjj operation diff
","text":"Compare changes to the repository between two operations
Usage: jj operation diff [OPTIONS]
--operation <OPERATION>
\u2014 Show repository changes in this operation, compared to its parent--from <FROM>
\u2014 Show repository changes from this operation--to <TO>
\u2014 Show repository changes to this operation--no-graph
\u2014 Don't show the graph, show a flat list of modified changes-p
, --patch
\u2014 Show patch of modifications to changes
If the previous version has different parents, it will be temporarily rebased to the parents of the new version, so the diff is not contaminated by unrelated changes.
-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted
--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show--ignore-all-space
\u2014 Ignore whitespace when comparing lines--ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj operation log
","text":"Show the operation log
Like other commands, jj op log
snapshots the current working-copy changes and reconciles divergent operations. Use --at-op=@ --ignore-working-copy
to inspect the current state without mutation.
Usage: jj operation log [OPTIONS]
-n
, --limit <LIMIT>
\u2014 Limit number of operations to show--no-graph
\u2014 Don't show the graph, show a flat list of operations-T
, --template <TEMPLATE>
\u2014 Render each operation using the given template
For the syntax, see https://martinvonz.github.io/jj/latest/templates/
--op-diff
\u2014 Show changes to the repository at each operation
-p
, --patch
\u2014 Show patch of modifications to changes (implies --op-diff)
If the previous version has different parents, it will be temporarily rebased to the parents of the new version, so the diff is not contaminated by unrelated changes.
-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted
--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show--ignore-all-space
\u2014 Ignore whitespace when comparing lines--ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj operation restore
","text":"Create a new operation that restores the repo to an earlier state
This restores the repo to the state at the specified operation, effectively undoing all later operations. It does so by creating a new operation.
Usage: jj operation restore [OPTIONS] <OPERATION>
<OPERATION>
\u2014 The operation to restore to
Use jj op log
to find an operation to restore to. Use e.g. jj --at-op=<operation ID> log
before restoring to an operation to see the state of the repo at that operation.
--what <WHAT>
\u2014 What portions of the local state to restore (can be repeated)
This option is EXPERIMENTAL.
Default values: repo
, remote-tracking
Possible values:
repo
: The jj repo state and local bookmarksremote-tracking
: The remote-tracking bookmarks. Do not restore these if you'd like to push after the undojj operation show
","text":"Show changes to the repository in an operation
Usage: jj operation show [OPTIONS] [OPERATION]
<OPERATION>
\u2014 Show repository changes in this operation, compared to its parent(s)
Default value: @
--no-graph
\u2014 Don't show the graph, show a flat list of modified changes-p
, --patch
\u2014 Show patch of modifications to changes
If the previous version has different parents, it will be temporarily rebased to the parents of the new version, so the diff is not contaminated by unrelated changes.
-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted
--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show--ignore-all-space
\u2014 Ignore whitespace when comparing lines--ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj operation undo
","text":"Create a new operation that undoes an earlier operation
This undoes an individual operation by applying the inverse of the operation.
Usage: jj operation undo [OPTIONS] [OPERATION]
<OPERATION>
\u2014 The operation to undo
Use jj op log
to find an operation to undo.
Default value: @
--what <WHAT>
\u2014 What portions of the local state to restore (can be repeated)
This option is EXPERIMENTAL.
Default values: repo
, remote-tracking
Possible values:
repo
: The jj repo state and local bookmarksremote-tracking
: The remote-tracking bookmarks. Do not restore these if you'd like to push after the undojj parallelize
","text":"Parallelize revisions by making them siblings
Running jj parallelize 1::2
will transform the history like this:
3\n| 3\n2 / \\\n| -> 1 2\n1 \\ /\n| 0\n0\n
The command effectively says \"these revisions are actually independent\", meaning that they should no longer be ancestors/descendants of each other. However, revisions outside the set that were previously ancestors of a revision in the set will remain ancestors of it. For example, revision 0 above remains an ancestor of both 1 and 2. Similarly, revisions outside the set that were previously descendants of a revision in the set will remain descendants of it. For example, revision 3 above remains a descendant of both 1 and 2.
Therefore, jj parallelize '1 | 3'
is a no-op. That's because 2, which is not in the target set, was a descendant of 1 before, so it remains a descendant, and it was an ancestor of 3 before, so it remains an ancestor.
Usage: jj parallelize [REVISIONS]...
<REVISIONS>
\u2014 Revisions to parallelizejj prev
","text":"Change the working copy revision relative to the parent revision
The command creates a new empty working copy revision that is the child of an ancestor offset
revisions behind the parent of the current working copy.
For example, when the offset is 1:
D @ D\n|/ |\nA => A @\n| |/\nB B\n
If --edit
is passed, the working copy revision is changed to the parent of the current working copy revision.
D @ D\n|/ |\nC => @\n| |\nB B\n| |\nA A\n
Usage: jj prev [OPTIONS] [OFFSET]
<OFFSET>
\u2014 How many revisions to move backward. Moves to the parent by default
Default value: 1
-e
, --edit
\u2014 Edit the parent directly, instead of moving the working-copy commit
Takes precedence over config in ui.movement.edit
; i.e. will negate ui.movement.edit = false
-n
, --no-edit
\u2014 The inverse of --edit
Takes precedence over config in ui.movement.edit
; i.e. will negate ui.movement.edit = true
--conflict
\u2014 Jump to the previous conflicted ancestor
jj rebase
","text":"Move revisions to different parent(s)
There are three different ways of specifying which revisions to rebase: -b
to rebase a whole branch, -s
to rebase a revision and its descendants, and -r
to rebase a single commit. If none of them is specified, it defaults to -b @
.
With -s
, the command rebases the specified revision and its descendants onto the destination. For example, jj rebase -s M -d O
would transform your history like this (letters followed by an apostrophe are post-rebase versions):
O N'\n| |\n| N M'\n| | |\n| M O\n| | => |\n| | L | L\n| |/ | |\n| K | K\n|/ |/\nJ J\n
With -b
, the command rebases the whole \"branch\" containing the specified revision. A \"branch\" is the set of commits that includes:
In other words, jj rebase -b X -d Y
rebases commits in the revset (Y..X)::
(which is equivalent to jj rebase -s 'roots(Y..X)' -d Y
for a single root). For example, either jj rebase -b L -d O
or jj rebase -b M -d O
would transform your history like this (because L
and M
are on the same \"branch\", relative to the destination):
O N'\n| |\n| N M'\n| | |\n| M | L'\n| | => |/\n| | L K'\n| |/ |\n| K O\n|/ |\nJ J\n
With -r
, the command rebases only the specified revisions onto the destination. Any \"hole\" left behind will be filled by rebasing descendants onto the specified revision's parent(s). For example, jj rebase -r K -d M
would transform your history like this:
M K'\n| |\n| L M\n| | => |\n| K | L'\n|/ |/\nJ J\n
Note that you can create a merge commit by repeating the -d
argument. For example, if you realize that commit L actually depends on commit M in order to work (in addition to its current parent K), you can run jj rebase -s L -d K -d M
:
M L'\n| |\\\n| L M |\n| | => | |\n| K | K\n|/ |/\nJ J\n
If a working-copy commit gets abandoned, it will be given a new, empty commit. This is true in general; it is not specific to this command.
Usage: jj rebase [OPTIONS] <--destination <DESTINATION>|--insert-after <INSERT_AFTER>|--insert-before <INSERT_BEFORE>>
-b
, --branch <BRANCH>
\u2014 Rebase the whole branch relative to destination's ancestors (can be repeated)
jj rebase -b=br -d=dst
is equivalent to jj rebase '-s=roots(dst..br)' -d=dst
.
If none of -b
, -s
, or -r
is provided, then the default is -b @
.
-s
, --source <SOURCE>
\u2014 Rebase specified revision(s) together with their trees of descendants (can be repeated)
Each specified revision will become a direct child of the destination revision(s), even if some of the source revisions are descendants of others.
If none of -b
, -s
, or -r
is provided, then the default is -b @
.
-r
, --revisions <REVISIONS>
\u2014 Rebase the given revisions, rebasing descendants onto this revision's parent(s)
Unlike -s
or -b
, you may jj rebase -r
a revision A
onto a descendant of A
.
If none of -b
, -s
, or -r
is provided, then the default is -b @
.
-d
, --destination <DESTINATION>
\u2014 The revision(s) to rebase onto (can be repeated to create a merge commit)
-A
, --insert-after <INSERT_AFTER>
\u2014 The revision(s) to insert after (can be repeated to create a merge commit)
Only works with -r
and -s
.
-B
, --insert-before <INSERT_BEFORE>
\u2014 The revision(s) to insert before (can be repeated to create a merge commit)
Only works with -r
and -s
.
--skip-emptied
\u2014 If true, when rebasing would produce an empty commit, the commit is abandoned. It will not be abandoned if it was already empty before the rebase. Will never skip merge commits with multiple non-empty parents
jj resolve
","text":"Resolve a conflicted file with an external merge tool
Only conflicts that can be resolved with a 3-way merge are supported. See docs for merge tool configuration instructions.
Note that conflicts can also be resolved without using this command. You may edit the conflict markers in the conflicted file directly with a text editor.
Usage: jj resolve [OPTIONS] [PATHS]...
<PATHS>
\u2014 Restrict to these paths when searching for a conflict to resolve. We will attempt to resolve the first conflict we can find. You can use the --list
argument to find paths to use here-r
, --revision <REVISION>
Default value: @
-l
, --list
\u2014 Instead of resolving one conflict, list all the conflicts
--tool <NAME>
\u2014 Specify 3-way merge tool to be usedjj restore
","text":"Restore paths from another revision
That means that the paths get the same content in the destination (--to
) as they had in the source (--from
). This is typically used for undoing changes to some paths in the working copy (jj restore <paths>
).
If only one of --from
or --to
is specified, the other one defaults to the working copy.
When neither --from
nor --to
is specified, the command restores into the working copy from its parent(s). jj restore
without arguments is similar to jj abandon
, except that it leaves an empty revision with its description and other metadata preserved.
See jj diffedit
if you'd like to restore portions of files rather than entire files.
Usage: jj restore [OPTIONS] [PATHS]...
<PATHS>
\u2014 Restore only these paths (instead of all paths)--from <FROM>
\u2014 Revision to restore from (source)--to <TO>
\u2014 Revision to restore into (destination)-c
, --changes-in <REVISION>
\u2014 Undo the changes in a revision as compared to the merge of its parents.
This undoes the changes that can be seen with jj diff -r REVISION
. If REVISION
only has a single parent, this option is equivalent to jj restore --to REVISION --from REVISION-
.
The default behavior of jj restore
is equivalent to jj restore --changes-in @
.
--restore-descendants
\u2014 Preserve the content (not the diff) when rebasing descendants
jj root
","text":"Show the current workspace root directory
Usage: jj root
jj show
","text":"Show commit description and changes in a revision
Usage: jj show [OPTIONS] [REVISION]
<REVISION>
\u2014 Show changes in this revision, compared to its parent(s)
Default value: @
-T
, --template <TEMPLATE>
\u2014 Render a revision using the given template
For the syntax, see https://martinvonz.github.io/jj/latest/templates/
-s
, --summary
\u2014 For each path, show only whether it was modified, added, or deleted
--stat
\u2014 Show a histogram of the changes--types
\u2014 For each path, show only its type before and after
The diff is shown as two letters. The first letter indicates the type before and the second letter indicates the type after. '-' indicates that the path was not present, 'F' represents a regular file, `L' represents a symlink, 'C' represents a conflict, and 'G' represents a Git submodule.
--name-only
\u2014 For each path, show only its path
Typically useful for shell commands like: jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g
--git
\u2014 Show a Git-format diff
--color-words
\u2014 Show a word-level diff with changes indicated only by color--tool <TOOL>
\u2014 Generate diff by external command--context <CONTEXT>
\u2014 Number of lines of context to show--ignore-all-space
\u2014 Ignore whitespace when comparing lines--ignore-space-change
\u2014 Ignore changes in amount of whitespace when comparing linesjj simplify-parents
","text":"Simplify parent edges for the specified revision(s).
Removes all parents of each of the specified revisions that are also indirect ancestors of the same revisions through other parents. This has no effect on any revision's contents, including the working copy.
In other words, for all (A, B, C) where A has (B, C) as parents and C is an ancestor of B, A will be rewritten to have only B as a parent instead of B+C.
Usage: jj simplify-parents <--source <SOURCE>|--revisions <REVISIONS>>
-s
, --source <SOURCE>
\u2014 Simplify specified revision(s) together with their trees of descendants (can be repeated)-r
, --revisions <REVISIONS>
\u2014 Simplify specified revision(s) (can be repeated)jj sparse
","text":"Manage which paths from the working-copy commit are present in the working copy
Usage: jj sparse <COMMAND>
edit
\u2014 Start an editor to update the patterns that are present in the working copylist
\u2014 List the patterns that are currently present in the working copyreset
\u2014 Reset the patterns to include all files in the working copyset
\u2014 Update the patterns that are present in the working copyjj sparse edit
","text":"Start an editor to update the patterns that are present in the working copy
Usage: jj sparse edit
jj sparse list
","text":"List the patterns that are currently present in the working copy
By default, a newly cloned or initialized repo will have have a pattern matching all files from the repo root. That pattern is rendered as .
(a single period).
Usage: jj sparse list
jj sparse reset
","text":"Reset the patterns to include all files in the working copy
Usage: jj sparse reset
jj sparse set
","text":"Update the patterns that are present in the working copy
For example, if all you need is the README.md
and the lib/
directory, use jj sparse set --clear --add README.md --add lib
. If you no longer need the lib
directory, use jj sparse set --remove lib
.
Usage: jj sparse set [OPTIONS]
--add <ADD>
\u2014 Patterns to add to the working copy--remove <REMOVE>
\u2014 Patterns to remove from the working copy--clear
\u2014 Include no files in the working copy (combine with --add)jj split
","text":"Split a revision in two
Starts a diff editor on the changes in the revision. Edit the right side of the diff until it has the content you want in the first revision. Once you close the editor, your edited content will replace the previous revision. The remaining changes will be put in a new revision on top.
If the change you split had a description, you will be asked to enter a change description for each commit. If the change did not have a description, the second part will not get a description, and you will be asked for a description only for the first part.
Splitting an empty commit is not supported because the same effect can be achieved with jj new
.
Usage: jj split [OPTIONS] [PATHS]...
<PATHS>
\u2014 Put these paths in the first commit-i
, --interactive
\u2014 Interactively choose which parts to split. This is the default if no paths are provided--tool <NAME>
\u2014 Specify diff editor to be used (implies --interactive)-r
, --revision <REVISION>
\u2014 The revision to split
Default value: @
-p
, --parallel
\u2014 Split the revision into two parallel revisions instead of a parent and child
jj squash
","text":"Move changes from a revision into another revision
With the -r
option, moves the changes from the specified revision to the parent revision. Fails if there are several parent revisions (i.e., the given revision is a merge).
With the --from
and/or --into
options, moves changes from/to the given revisions. If either is left out, it defaults to the working-copy commit. For example, jj squash --into @--
moves changes from the working-copy commit to the grandparent.
If, after moving changes out, the source revision is empty compared to its parent(s), and --keep-emptied
is not set, it will be abandoned. Without --interactive
or paths, the source revision will always be empty.
If the source was abandoned and both the source and destination had a non-empty description, you will be asked for the combined description. If either was empty, then the other one will be used.
If a working-copy commit gets abandoned, it will be given a new, empty commit. This is true in general; it is not specific to this command.
Usage: jj squash [OPTIONS] [PATHS]...
<PATHS>
\u2014 Move only changes to these paths (instead of all paths)-r
, --revision <REVISION>
\u2014 Revision to squash into its parent (default: @)-f
, --from <FROM>
\u2014 Revision(s) to squash from (default: @)-t
, --into <INTO>
\u2014 Revision to squash into (default: @)-m
, --message <MESSAGE>
\u2014 The description to use for squashed revision (don't open editor)-u
, --use-destination-message
\u2014 Use the description of the destination revision and discard the description(s) of the source revision(s)-i
, --interactive
\u2014 Interactively choose which parts to squash--tool <NAME>
\u2014 Specify diff editor to be used (implies --interactive)-k
, --keep-emptied
\u2014 The source revision will not be abandonedjj status
","text":"Show high-level repo status [aliases: st]
This includes:
Usage: jj status [PATHS]...
<PATHS>
\u2014 Restrict the status display to these pathsjj tag
","text":"Manage tags
Usage: jj tag <COMMAND>
list
\u2014 List tagsjj tag list
","text":"List tags
Usage: jj tag list [OPTIONS] [NAMES]...
<NAMES>
\u2014 Show tags whose local name matches
By default, the specified name matches exactly. Use glob:
prefix to select tags by wildcard pattern. For details, see https://martinvonz.github.io/jj/latest/revsets/#string-patterns.
-T
, --template <TEMPLATE>
\u2014 Render each tag using the given template
All 0-argument methods of the RefName
type are available as keywords.
For the syntax, see https://martinvonz.github.io/jj/latest/templates/
jj util
","text":"Infrequently used commands such as for generating shell completions
Usage: jj util <COMMAND>
completion
\u2014 Print a command-line-completion scriptconfig-schema
\u2014 Print the JSON schema for the jj TOML config formatgc
\u2014 Run backend-dependent garbage collectionmangen
\u2014 Print a ROFF (manpage)markdown-help
\u2014 Print the CLI help for all subcommands in Markdownjj util completion
","text":"Print a command-line-completion script
Apply it by running one of these:
source <(jj util completion bash)
jj util completion fish | source
jj util completion nushell | save \"completions-jj.nu\"\nuse \"completions-jj.nu\" * # Or `source \"completions-jj.nu\"`\n
autoload -U compinit\ncompinit\nsource <(jj util completion zsh)\n
Usage: jj util completion [SHELL]
<SHELL>
Possible values: bash
, elvish
, fish
, nushell
, power-shell
, zsh
jj util config-schema
","text":"Print the JSON schema for the jj TOML config format
Usage: jj util config-schema
jj util gc
","text":"Run backend-dependent garbage collection
Usage: jj util gc [OPTIONS]
--expire <EXPIRE>
\u2014 Time threshold
By default, only obsolete objects and operations older than 2 weeks are pruned.
Only the string \"now\" can be passed to this parameter. Support for arbitrary absolute and relative timestamps will come in a subsequent release.
jj util mangen
","text":"Print a ROFF (manpage)
Usage: jj util mangen
jj util markdown-help
","text":"Print the CLI help for all subcommands in Markdown
Usage: jj util markdown-help
jj undo
","text":"Undo an operation (shortcut for jj op undo
)
Usage: jj undo [OPTIONS] [OPERATION]
<OPERATION>
\u2014 The operation to undo
Use jj op log
to find an operation to undo.
Default value: @
--what <WHAT>
\u2014 What portions of the local state to restore (can be repeated)
This option is EXPERIMENTAL.
Default values: repo
, remote-tracking
Possible values:
repo
: The jj repo state and local bookmarksremote-tracking
: The remote-tracking bookmarks. Do not restore these if you'd like to push after the undojj version
","text":"Display version information
Usage: jj version
jj workspace
","text":"Commands for working with workspaces
Workspaces let you add additional working copies attached to the same repo. A common use case is so you can run a slow build or test in one workspace while you're continuing to write code in another workspace.
Each workspace has its own working-copy commit. When you have more than one workspace attached to a repo, they are indicated by <workspace name>@
in jj log
.
Each workspace also has own sparse patterns.
Usage: jj workspace <COMMAND>
add
\u2014 Add a workspaceforget
\u2014 Stop tracking a workspace's working-copy commit in the repolist
\u2014 List workspacesrename
\u2014 Renames the current workspaceroot
\u2014 Show the current workspace root directoryupdate-stale
\u2014 Update a workspace that has become stalejj workspace add
","text":"Add a workspace
By default, the new workspace inherits the sparse patterns of the current workspace. You can override this with the --sparse-patterns
option.
Usage: jj workspace add [OPTIONS] <DESTINATION>
<DESTINATION>
\u2014 Where to create the new workspace--name <NAME>
\u2014 A name for the workspace
To override the default, which is the basename of the destination directory.
-r
, --revision <REVISION>
\u2014 A list of parent revisions for the working-copy commit of the newly created workspace. You may specify nothing, or any number of parents.
If no revisions are specified, the new workspace will be created, and its working-copy commit will exist on top of the parent(s) of the working-copy commit in the current workspace, i.e. they will share the same parent(s).
If any revisions are specified, the new workspace will be created, and the new working-copy commit will be created with all these revisions as parents, i.e. the working-copy commit will exist as if you had run jj new r1 r2 r3 ...
.
--sparse-patterns <SPARSE_PATTERNS>
\u2014 How to handle sparse patterns when creating a new workspace
Default value: copy
Possible values:
copy
: Copy all sparse patterns from the current workspacefull
: Include all files in the new workspaceempty
: Clear all files from the workspace (it will be empty)jj workspace forget
","text":"Stop tracking a workspace's working-copy commit in the repo
The workspace will not be touched on disk. It can be deleted from disk before or after running this command.
Usage: jj workspace forget [WORKSPACES]...
<WORKSPACES>
\u2014 Names of the workspaces to forget. By default, forgets only the current workspacejj workspace list
","text":"List workspaces
Usage: jj workspace list
jj workspace rename
","text":"Renames the current workspace
Usage: jj workspace rename <NEW_WORKSPACE_NAME>
<NEW_WORKSPACE_NAME>
\u2014 The name of the workspace to update tojj workspace root
","text":"Show the current workspace root directory
Usage: jj workspace root
jj workspace update-stale
","text":"Update a workspace that has become stale
For information about stale working copies, see https://martinvonz.github.io/jj/latest/working-copy/.
Usage: jj workspace update-stale
This document was generated automatically by clap-markdown
.
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.
"},{"location":"code-of-conduct/#our-standards","title":"Our Standards","text":"Examples of behavior that contributes to a positive environment for our community include:
Examples of unacceptable behavior include:
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.
"},{"location":"code-of-conduct/#scope","title":"Scope","text":"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.
"},{"location":"code-of-conduct/#enforcement","title":"Enforcement","text":"Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at two or more of jaraco@jaraco.com, avamsi07@gmail.com, me@waleedkhan.name, and opensource@google.com. 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.
"},{"location":"code-of-conduct/#enforcement-guidelines","title":"Enforcement Guidelines","text":"Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
"},{"location":"code-of-conduct/#1-correction","title":"1. Correction","text":"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.
"},{"location":"code-of-conduct/#2-warning","title":"2. Warning","text":"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.
"},{"location":"code-of-conduct/#3-temporary-ban","title":"3. Temporary Ban","text":"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.
"},{"location":"code-of-conduct/#4-permanent-ban","title":"4. Permanent Ban","text":"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.
"},{"location":"code-of-conduct/#attribution","title":"Attribution","text":"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.
"},{"location":"community_tools/","title":"Community-built tools around Jujutsu","text":"Important: Many of these are not complete yet, just like Jujutsu itself. But they already simplify many workflows and can improve your experience.
"},{"location":"community_tools/#diffedit3","title":"Diffedit3","text":"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
"},{"location":"community_tools/#gg-gui-for-jj","title":"GG - GUI for JJ","text":"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.
"},{"location":"community_tools/#hunknvim","title":"Hunk.nvim","text":"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.
"},{"location":"community_tools/#lazyjj","title":"LazyJJ","text":"lazyjj is a lazygit inspired TUI for Jujutsu.
Find it here.
"},{"location":"community_tools/#jj-tui","title":"JJ TUI","text":"This is TUI for Jujutsu built in Ocaml, it is unopiniated and its creator is open to feedback.
Find it here.
"},{"location":"community_tools/#visual-jujutsu","title":"Visual Jujutsu","text":"VJJ is a fzf (fuzzy finder) wrapper for Jujutsu, which is meant to be used interactively in the terminal.
Find it here.
"},{"location":"community_tools/#visualjj","title":"VisualJJ","text":"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.
"},{"location":"community_tools/#finding-other-integrations","title":"Finding other integrations","text":"You can find other community contributed tools and integrations in our Wiki.
"},{"location":"config/","title":"Configuration","text":"These are the config settings available to jj/Jujutsu.
"},{"location":"config/#config-files-and-toml","title":"Config files and TOML","text":"jj
loads several types of config settings:
The built-in settings. These cannot be edited. They can be viewed in the cli/src/config/
directory in jj
's source repo.
The user settings. These can be edited with jj config edit --user
. User settings are located in the user config file, which can be found with jj config path --user
.
The repo settings. These can be edited with jj config edit --repo
and are located in .jj/repo/config.toml
.
Settings specified in 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.
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.
In TOML, anything under a heading can be dotted instead. For example, user.name = \"YOUR NAME\"
is equivalent to:
[user]\nname = \"YOUR NAME\"\n
For future reference, here are a couple of more complicated examples,
# Dotted style\ntemplate-aliases.\"format_short_id(id)\" = \"id.shortest(12)\"\ncolors.\"commit_id prefix\".bold = true\n\n# is equivalent to:\n[template-aliases]\n\"format_short_id(id)\" = \"id.shortest(12)\"\n\n[colors]\n\"commit_id prefix\" = { bold = true }\n
Jujutsu favors the dotted style in these instructions, if only because it's easier to write down in an unconfusing way. If you are confident with TOML then use whichever suits you in your config. If you mix dotted keys and headings, put the dotted keys before the first heading.
That's probably enough TOML to keep you out of trouble but the syntax guide is very short if you ever need to check.
"},{"location":"config/#user-settings","title":"User settings","text":"user.name = \"YOUR NAME\"\nuser.email = \"YOUR_EMAIL@example.com\"\n
Don't forget to change these to your own details!
"},{"location":"config/#ui-settings","title":"UI settings","text":""},{"location":"config/#colorizing-output","title":"Colorizing output","text":"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 color\n
"},{"location":"config/#custom-colors-and-styles","title":"Custom colors and styles","text":"You can customize the colors used for various elements of the UI. For example:
colors.commit_id = \"green\"\n
The following colors are available:
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\"\n
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, or make the text bold or underlined. For that, you need to use a table:
colors.commit_id = { fg = \"green\", bg = \"#ff1525\", bold = true, underline = true }\n
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\"\ncolors.\"working_copy commit_id\" = { underline = true }\n
Parts of the style that are not overridden - such as the foreground color in the example above - are inherited from the parent style.
Which elements can be colored is not yet documented, but see the default color configuration for some examples of what's possible.
"},{"location":"config/#default-command","title":"Default command","text":"When jj
is run with no explicit subcommand, the value of the ui.default-command
setting will be used instead. Possible values are any valid subcommand name, subcommand alias, or user-defined alias (defaults to \"log\"
).
ui.default-command = [\"log\", \"--reversed\"]\n
"},{"location":"config/#default-description","title":"Default description","text":"The editor content of a commit description can be populated by the draft_commit_description
template.
[templates]\ndraft_commit_description = '''\nconcat(\n description,\n surround(\n \"\\nJJ: This commit contains the following changes:\\n\", \"\",\n indent(\"JJ: \", diff.stat(72)),\n ),\n)\n'''\n
The value of the ui.default-description
setting can also be used in order to fill in things like BUG=, TESTED= etc.
ui.default-description = \"\\n\\nTESTED=TODO\"\n
"},{"location":"config/#diff-colors-and-styles","title":"Diff colors and styles","text":"In color-words and git diffs, word-level hunks are rendered with underline. You can override the default style with the following keys:
[colors]\n# Highlight hunks with background\n\"diff removed token\" = { bg = \"#221111\", underline = false }\n\"diff added token\" = { bg = \"#002200\", underline = false }\n
"},{"location":"config/#diff-format","title":"Diff format","text":"# Possible values: \"color-words\" (default), \"git\", \"summary\"\nui.diff.format = \"git\"\n
"},{"location":"config/#color-words-diff-options","title":"Color-words diff options","text":"In color-words diffs, changed words are displayed inline by default. 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. You can also change the default number of lines of context shown.
max-inline-alternation
: Maximum number of removed/added word alternation to inline. For example, <added> ... <added>
sequence has 1 alternation, so the line will be inline if max-inline-alternation >= 1
. <added> ... <removed> ... <added>
sequence has 3 alternation.
0
: disable inlining, making --color-words
more similar to --git
1
: inline removes-only or adds-only lines2
, 3
, ..: inline up to 2
, 3
, .. alternation-1
: inline all linesThe default is 3
.
This parameter is experimental. The definition is subject to change.
context
: Number of lines of context to show in the diff. The default is 3
.
[diff.color-words]\nmax-inline-alternation = 3\ncontext = 3\n
"},{"location":"config/#git-diff-options","title":"Git diff options","text":"In git diffs you can change the default number of lines of context shown.
context
: Number of lines of context to show in the diff. The default is 3
.[diff.git]\ncontext = 3\n
"},{"location":"config/#generating-diffs-by-external-command","title":"Generating diffs by external command","text":"If ui.diff.tool
is set, the specified diff command will be called instead of the internal diff function.
[ui]\n# Use Difftastic by default\ndiff.tool = [\"difft\", \"--color=always\", \"$left\", \"$right\"]\n# Use tool named \"<name>\" (see below)\ndiff.tool = \"<name>\"\n
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>]\n# program = \"<name>\" # Defaults to the name of the tool if not specified\ndiff-args = [\"--color=always\", \"$left\", \"$right\"]\n
$left
and $right
are replaced with the paths to the left and right directories to diff respectively.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]\ndiff.tool = \"vimdiff\"\n\n[merge-tools.vimdiff]\ndiff-invocation-mode = \"file-by-file\"\n
"},{"location":"config/#set-of-immutable-commits","title":"Set of immutable commits","text":"You can configure the set of immutable commits via revset-aliases.\"immutable_heads()\"
. The default set of immutable heads is builtin_immutable_heads()
, which in turn is defined as present(trunk()) | tags() | untracked_remote_bookmarks()
. For example, to also consider the release@origin
bookmark immutable:
revset-aliases.\"immutable_heads()\" = \"builtin_immutable_heads() | release@origin\"\n
To prevent rewriting commits authored by other users:
# The `trunk().. &` bit is an optimization to scan for non-`mine()` commits\n# only among commits that are not in `trunk()`.\nrevset-aliases.\"immutable_heads()\" = \"builtin_immutable_heads() | (trunk().. & ~mine())\"\n
Ancestors of the configured set are also immutable. The root commit is always immutable even if the set is empty.
"},{"location":"config/#log","title":"Log","text":""},{"location":"config/#default-revisions","title":"Default revisions","text":"You can configure the revisions jj log
would show when neither -r
nor any paths are specified.
# Show commits that are not in `main@origin`\nrevsets.log = \"main@origin..\"\n
The default value for revsets.log
is 'present(@) | ancestors(immutable_heads().., 2) | present(trunk())'
.
You can configure the template used when no -T
is specified.
templates.log
for jj log
templates.op_log
for jj op log
templates.show
for jj show
[templates]\n# Use builtin log template\nlog = \"builtin_log_compact\"\n# Use builtin op log template\nop_log = \"builtin_log_compact\"\n# Use builtin show template\nshow = \"builtin_log_detailed\"\n
If you want to see the full description when you do jj log
you can add this to your config:
[templates]\nlog = \"builtin_log_compact_full_description\"\n
"},{"location":"config/#graph-style","title":"Graph style","text":"# Possible values: \"curved\" (default), \"square\", \"ascii\", \"ascii-large\"\nui.graph.style = \"square\"\n
"},{"location":"config/#node-style","title":"Node style","text":"The symbols used to represent commits or operations can be customized via templates.
templates.log_node
for commits (with Option<Commit>
keywords)templates.op_log_node
for operations (with Operation
keywords)For example:
[templates]\nlog_node = '''\ncoalesce(\n if(!self, \"\ud83e\udf80\"),\n if(current_working_copy, \"@\"),\n if(root, \"\u2534\"),\n if(immutable, \"\u25cf\", \"\u25cb\"),\n)\n'''\nop_log_node = 'if(current_operation, \"@\", \"\u25cb\")'\n
"},{"location":"config/#wrap-log-content","title":"Wrap log content","text":"If enabled, log
/evolog
/op log
content will be wrapped based on the terminal width.
ui.log-word-wrap = true\n
"},{"location":"config/#display-of-commit-and-change-ids","title":"Display of commit and change ids","text":"Can be customized by the format_short_id()
template alias.
[template-aliases]\n# Highlight unique prefix and show at least 12 characters (default)\n'format_short_id(id)' = 'id.shortest(12)'\n# Just the shortest possible unique prefix\n'format_short_id(id)' = 'id.shortest()'\n# Show unique prefix and the rest surrounded by brackets\n'format_short_id(id)' = 'id.shortest(12).prefix() ++ \"[\" ++ id.shortest(12).rest() ++ \"]\"'\n# Always show 12 characters\n'format_short_id(id)' = 'id.short(12)'\n
To customize these separately, use the format_short_commit_id()
and format_short_change_id()
aliases:
[template-aliases]\n# Uppercase change ids. `jj` treats change and commit ids as case-insensitive.\n'format_short_change_id(id)' = 'format_short_id(id).upper()'\n
To get shorter prefixes for certain revisions, set revsets.short-prefixes
:
# Prioritize the current bookmark\nrevsets.short-prefixes = \"(main..@)::\"\n
"},{"location":"config/#relative-timestamps","title":"Relative timestamps","text":"Can be customized by the format_timestamp()
template alias.
[template-aliases]\n# Full timestamp in ISO 8601 format\n'format_timestamp(timestamp)' = 'timestamp'\n# Relative timestamp rendered as \"x days/hours/seconds ago\"\n'format_timestamp(timestamp)' = 'timestamp.ago()'\n
jj op log
defaults to relative timestamps. To use absolute timestamps, you will need to modify the format_time_range()
template alias.
[template-aliases]\n'format_time_range(time_range)' = 'time_range.start() ++ \" - \" ++ time_range.end()'\n
"},{"location":"config/#author-format","title":"Author format","text":"Can be customized by the format_short_signature()
template alias.
[template-aliases]\n# Full email address (default)\n'format_short_signature(signature)' = 'signature.email()'\n# Both name and email address\n'format_short_signature(signature)' = 'signature'\n# Username part of the email address\n'format_short_signature(signature)' = 'signature.username()'\n
"},{"location":"config/#allow-large-revsets-by-default","title":"Allow \"large\" revsets by default","text":"Certain commands (such as jj rebase
) can take multiple revset arguments, but default to requiring each of those revsets to expand to a single revision. This restriction can be overridden by prefixing a revset that the user wants to be able to expand to more than one revision with the all:
modifier.
Another way you can override this check is by setting ui.always-allow-large-revsets
to true
. Then, jj
will allow every one of the revset arguments of such commands to expand to any number of revisions.
# Assume `all:` prefix before revsets whenever it would make a difference\nui.always-allow-large-revsets = true\n
"},{"location":"config/#pager","title":"Pager","text":"The default pager is can be set via ui.pager
or the PAGER
environment variable. The priority is as follows (environment variables are marked with a $
):
ui.pager
> $PAGER
less -FRX
is the default pager in the absence of any other setting, except on Windows where it is :builtin
.
The special value :builtin
enables usage of the integrated pager called minus
. See the docs for the minus
pager for the key bindings and some more details.
If you are using a standard Linux distro, your system likely already has $PAGER
set and that will be preferred over the built-in. To use the built-in:
jj config set --user ui.pager :builtin\n
It is possible the default will change to :builtin
for all platforms in the future.
Additionally, paging behavior can be toggled via ui.paginate
like so:
# Enable pagination for commands that support it (default)\nui.paginate = \"auto\"\n# Disable all pagination, equivalent to using --no-pager\nui.paginate = \"never\"\n
"},{"location":"config/#processing-contents-to-be-paged","title":"Processing contents to be paged","text":"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\"]\n
Some formatters (like delta
) require git style diffs for formatting. You can configure this style of diff as the default with the ui.diff
setting. For example:
[ui]\npager = \"delta\"\n\n[ui.diff]\nformat = \"git\"\n
"},{"location":"config/#aliases","title":"Aliases","text":"You can define aliases for commands, including their arguments. For example:
# `jj l` shows commits on the working-copy commit's (anonymous) bookmark\n# compared to the `main` bookmark\naliases.l = [\"log\", \"-r\", \"(main..@):: | (main..@)-\"]\n
"},{"location":"config/#editor","title":"Editor","text":"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
Pico is the default editor (Notepad on Windows) in the absence of any other setting, but you could set it explicitly too.
ui.editor = \"pico\"\n
To use NeoVim instead:
ui.editor = \"nvim\"\n
For GUI editors you possibly need to use a -w
or --wait
. Some examples:
ui.editor = \"code -w\" # VS Code\nui.editor = \"code.cmd -w\" # VS Code on Windows\nui.editor = \"bbedit -w\" # BBEdit\nui.editor = \"subl -n -w\" # Sublime Text\nui.editor = \"mate -w\" # TextMate\nui.editor = [\"C:/Program Files/Notepad++/notepad++.exe\",\n \"-multiInst\", \"-notabbar\", \"-nosession\", \"-noPlugin\"] # Notepad++\nui.editor = \"idea --temp-project --wait\" #IntelliJ\n
Obviously, you would only set one line, don't copy them all in!
"},{"location":"config/#editing-diffs","title":"Editing diffs","text":"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, e.g. Meld to use the meld
diff editor.
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\nmerge-tools.meld.program = \"/path/to/meld\" # May be necessary if `meld` is not in the PATH\nmerge-tools.meld.edit-args = [\"--newtab\", \"$left\", \"$right\"]\n
jj
makes the following substitutions:
$left
and $right
are replaced with the paths to the left and right directories to diff respectively.
If no edit-args
are specified, [\"$left\", \"$right\"]
are set by default.
Finally, ui.diff-editor
can be a list that specifies a command and its arguments.
Some examples:
# Use merge-tools.meld.edit-args\nui.diff-editor = \"meld\" # Or `kdiff3`, or `diffedit3`, ...\n# Specify edit-args inline\nui.diff-editor = [\"/path/to/binary\", \"--be-helpful\", \"$left\", \"$right\"]\n# Equivalent to [\"binary\", \"$left\", \"$right\"] arguments by default\nui.diff-editor = \"binary\"\n
"},{"location":"config/#experimental-3-pane-diff-editing","title":"Experimental 3-pane diff editing","text":"We offer two special \"3-pane\" diff editor configs:
meld-3
, which requires installing Meld, anddiffedit3
, which requires installing diffedit3
.Meld
is a graphical application that is recommended, but can be difficult to install in some situations. diffedit3
is designed to be easy to install and to be usable in environments where Meld is difficult to use (e.g. over SSH via port forwarding). diffedit3
starts a local server that can be accessed via a web browser, similarly to Jupyter.
There is also the diffedit3-ssh
which is similar to diffedit3
but does not try to open the web browser pointing to the local server (the URL printed to the terminal) automatically. diffedit3-ssh
also always uses ports in between 17376-17380 and fails if they are all busy. This can be useful when working over SSH. Open the fold below for more details of how to set that up.
ssh -L 17376:localhost:17376 \\\n -L 17377:localhost:17377 \\\n -L 17378:localhost:17378 \\\n -L 17379:localhost:17379 \\\n -L 17380:localhost:17380 \\\n myhost.example.com\n
`diffedit3-ssh` is set up to use these 5 ports by default. Usually, only the first of them will be used. The rest are used if another program happens to use one of them, or if you run multiple instances of `diffedit3` at the same time. Another way is to add a snippet to `~/.ssh/config`: Host myhost\n User myself\n Hostname myhost.example.com\n LocalForward 17376 localhost:17376\n LocalForward 17377 localhost:17377\n LocalForward 17378 localhost:17378\n LocalForward 17379 localhost:17379\n LocalForward 17380 localhost:17380\n
With that configuration, you should be able to simply `ssh myhost`. Setting either ui.diff-editor = \"meld-3\"
or ui.diff-editor = \"diffedit3\"
will result in the diff editor showing 3 panes: the diff on the left and right, and an editing pane in the middle. This allow you to see both sides of the original diff while editing.
If you use ui.diff-editor = \"meld-3\"
, note that you can still get the 2-pane Meld view using jj diff --tool meld
. diffedit3
has a button you can use to switch to a 2-pane view.
To configure other diff editors in this way, you can include $output
together with $left
and $right
in merge-tools.TOOL.edit-args
. jj
will replace $output
with the directory where the diff editor will be expected to put the result of the user's edits. Initially, the contents of $output
will be the same as the contents of $right
.
JJ-INSTRUCTIONS
","text":"When editing a diff, jj will include a synthetic file called JJ-INSTRUCTIONS
in the diff with instructions on how to edit the diff. Any changes you make to this file will be ignored. To suppress the creation of this file, set ui.diff-instructions = false
.
Using 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.
The ui.merge-editor
key specifies the tool used for three-way merge tools by jj resolve
. For example:
# Use merge-tools.meld.merge-args\nui.merge-editor = \"meld\" # Or \"vscode\" or \"vscodium\" or \"kdiff3\" or \"vimdiff\"\n# Specify merge-args inline\nui.merge-editor = [\"meld\", \"$left\", \"$base\", \"$right\", \"-o\", \"$output\"]\n
The \"vscode\", \"vscodium\", \"meld\", \"kdiff3\", and \"vimdiff\" tools can be used out of the box, as long as they are installed.
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.
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\nmerge-tools.kdiff3.merge-args = [\"$base\", \"$left\", \"$right\", \"-o\", \"$output\", \"--auto\"]\nmerge-tools.meld.merge-args = [\"$left\", \"$base\", \"$right\", \"-o\", \"$output\", \"--auto-merge\"]\n\nmerge-tools.vimdiff.merge-args = [\"-f\", \"-d\", \"$output\", \"-M\",\n \"$left\", \"$base\", \"$right\",\n \"-c\", \"wincmd J\", \"-c\", \"set modifiable\",\n \"-c\", \"set write\"]\nmerge-tools.vimdiff.program = \"vim\"\nmerge-tools.vimdiff.merge-tool-edits-conflict-markers = true # See below for an explanation\n
jj
makes the following substitutions:
$output
(REQUIRED) is replaced with the name of the file that the merge tool should output. jj
will read this file after the merge tool exits.
$left
and $right
are replaced with the paths to two files containing the content of each side of the conflict.
$base
is replaced with the path to a file containing the contents of the conflicted file in the last common ancestor of the two sides of the conflict.
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. This is appropriate for most graphical merge tools.
Some tools (e.g. vimdiff
) can present a multi-way diff but don't resolve conflict themselves. When using such tools, jj
can help you by populating the output file with conflict markers before starting the merge tool (instead of leaving the output file empty and letting the merge tool fill it in). To do that, set the merge-tools.vimdiff.merge-tool-edits-conflict-markers = true
option.
With this option set, if the output file still contains conflict markers after the conflict is done, jj
assumes that the conflict was only partially resolved and parses the conflict markers to get the new state of the conflict. The conflict is considered fully resolved when there are no conflict markers left.
The jj fix
command allows you to efficiently rewrite files in complex commit graphs with no risk of introducing conflicts, using tools like clang-format
or prettier
. The tools run as subprocesses that take file content on standard input and repeat it, with any desired changes, on standard output. The file is only rewritten if the subprocess produces a successful exit code.
Suppose you want to use clang-format
to format your *.c
and *.h
files, as well as sorting their #include
directives.
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 $path
substitution to provide the name of the file in a command argument.
[fix.tools.clang-format]\ncommand = [\"/usr/bin/clang-format\", \"--sort-includes\", \"--assume-filename=$path\"]\npatterns = [\"glob:'**/*.c'\",\n \"glob:'**/*.h'\"]\n
"},{"location":"config/#sort-and-remove-duplicate-lines-from-a-file","title":"Sort and remove duplicate lines from a file","text":"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]\ncommand = [\"sort\", \"-u\"]\npatterns = [\"word_list.txt\"]\n
"},{"location":"config/#execution-order-of-tools","title":"Execution order of tools","text":"If two or more tools affect the same file, they are executed in the ascending lexicographical order of their configured names. This will remain as a tie breaker if other ordering mechanisms are introduced in the future. If you use numbers in tool names to control execution order, remember to include enough leading zeros so that, for example, 09
sorts before 10
.
Suppose you want to keep only the 10 smallest numbers in a text file that contains one number on each line. This can be accomplished with sort
and head
, but execution order is important.
[fix.tools.1-sort-numbers-file]\ncommand = [\"sort\", \"-n\"]\npatterns = [\"numbers.txt\"]\n\n[fix.tools.2-truncate-numbers-file]\ncommand = [\"head\", \"-n\", \"10\"]\npatterns = [\"numbers.txt\"]\n
"},{"location":"config/#commit-signing","title":"Commit Signing","text":"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.
[signing]\nsign-all = true\nbackend = \"gpg\"\nkey = \"4ED556E9729E000F\"\n## You can set `key` to anything accepted by `gpg -u`\n# key = \"signing@example.com\"\n
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\"\n
Also by default the gpg backend will ignore key expiry when verifying commit signatures. To consider expired keys as invalid you can set:
signing.backends.gpg.allow-expired-keys = false\n
"},{"location":"config/#ssh-signing","title":"SSH Signing","text":"[signing]\nsign-all = true\nbackend = \"ssh\"\nkey = \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGj+J6N6SO+4P8dOZqfR1oiay2yxhhHnagH52avUqw5h\"\n## You can also use a path instead of embedding the key\n# key = \"~/.ssh/id_for_signing.pub\"\n
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\"\n
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\"\n
"},{"location":"config/#git-settings","title":"Git settings","text":""},{"location":"config/#default-remotes-for-jj-git-fetch-and-jj-git-push","title":"Default remotes for jj git fetch
and jj git push
","text":"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\"\njj config set --repo git.fetch '[\"origin\", \"upstream\"]'\n
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\"\n
Note that unlike git.fetch
, git.push
can currently only be a single remote. This is not a hard limitation, and could be changed in the future if there is demand.
When jj
imports a new remote-tracking bookmark from Git, it can also create a local bookmark with the same name. This feature is disabled by default because it may be undesirable in some repositories, e.g.:
You can enable this behavior by setting git.auto-local-bookmark
like so,
git.auto-local-bookmark = true\n
This setting is applied only to new remote bookmarks. Existing remote bookmarks can be tracked individually by using jj bookmark track
/untrack
commands.
# import feature1 bookmark and start tracking it\njj bookmark track feature1@origin\n# delete local gh-pages bookmark and stop tracking it\njj bookmark delete gh-pages\njj bookmark untrack gh-pages@upstream\n
"},{"location":"config/#abandon-commits-that-became-unreachable-in-git","title":"Abandon commits that became unreachable in Git","text":"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 = false\n
"},{"location":"config/#prefix-for-generated-bookmarks-on-push","title":"Prefix for generated bookmarks on push","text":"jj git push --change
generates bookmark names with a prefix of \"push-\" by default. You can pick a different prefix by setting git.push-bookmark-prefix
. For example:
git.push-bookmark-prefix = \"martinvonz/push-\"\n
"},{"location":"config/#set-of-private-commits","title":"Set of private commits","text":"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.
# Prevent pushing work in progress or anything explicitly labeled \"private\"\ngit.private-commits = \"description(glob:'wip:*') | description(glob:'private:*')\"\n
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.
"},{"location":"config/#filesystem-monitor","title":"Filesystem monitor","text":"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 core.fsmonitor
option. Currently, the valid values are \"none\"
or \"watchman\"
.
To configure the Watchman filesystem monitor, set core.fsmonitor = \"watchman\"
. Ensure that you have installed the Watchman executable on your system.
You can configure jj
to use watchman triggers to automatically create snapshots on filesystem changes by setting core.watchman.register_snapshot_trigger = true
.
You can check whether Watchman is enabled and whether it is installed correctly using jj debug watchman status
.
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.
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.
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\"\n# the following is equivalent\nsnapshot.max-new-file-size = 10485760\n
The value can be specified using a human readable string with typical suffixes; B
, MiB
, GB
, etc. By default, if no suffix is provided, or the value is a raw integer literal, the value is interpreted as if it were specified in bytes.
Files that already exist in the working copy are not subject to this limit.
Setting this value to zero will disable the limit entirely.
"},{"location":"config/#ways-to-specify-jj-config-details","title":"Ways to specifyjj
config: details","text":""},{"location":"config/#user-config-file","title":"User config file","text":"An easy way to find the user config file is:
jj config path --user\n
The rest of this section covers the details of where this file can be located.
On all platforms, the user's global jj
configuration file is located at either ~/.jjconfig.toml
(where ~
represents $HOME
on Unix-likes, or %USERPROFILE%
on Windows) or in a platform-specific directory. The platform-specific location is recommended for better integration with platform services. It is an error for both of these files to exist.
$XDG_CONFIG_HOME/jj/config.toml
/home/alice/.config/jj/config.toml
macOS $HOME/Library/Application Support/jj/config.toml
/Users/Alice/Library/Application Support/jj/config.toml
Windows {FOLDERID_RoamingAppData}\\jj\\config.toml
C:\\Users\\Alice\\AppData\\Roaming\\jj\\config.toml
The location of the jj
config file can also be overridden with the JJ_CONFIG
environment variable. If it is not empty, it should contain the path to a TOML file that will be used instead of any configuration file in the default locations. For example,
env JJ_CONFIG=/dev/null jj log # Ignores any settings specified in the config file.\n
"},{"location":"config/#specifying-config-on-the-command-line","title":"Specifying config on the command-line","text":"You can use one or more --config-toml
options on the command line to specify additional configuration settings. This overrides settings defined in config files or environment variables. For example,
jj --config-toml='ui.color=\"always\"' --config-toml='ui.diff-editor=\"kdiff3\"' split\n
Config specified this way must be valid TOML. In particular, string values must be surrounded by quotes. To pass these quotes to jj
, most shells require surrounding those quotes with single quotes as shown above.
In sh
-compatible shells, --config-toml
can be used to merge entire TOML files with the config specified in .jjconfig.toml
:
jj --config-toml=\"$(cat extra-config.toml)\" log\n
"},{"location":"conflicts/","title":"First-class conflicts","text":""},{"location":"conflicts/#introduction","title":"Introduction","text":"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).
"},{"location":"conflicts/#advantages","title":"Advantages","text":"The deeper understanding of conflicts has many advantages:
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.For information about how conflicts are handled in the working copy, see here.
"},{"location":"conflicts/#conflict-markers","title":"Conflict markers","text":"Conflicts are \"materialized\" using conflict markers in various contexts. For example, when you run 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). Here's an example of how Git can render a conflict using its \"diff3\" style:
<<<<<<< left\n apple\n grapefruit\n orange\n ||||||| base\n apple\n grape\n orange\n =======\n APPLE\n GRAPE\n ORANGE\n >>>>>>> right\n
In this example, the left side changed \"grape\" to \"grapefruit\", and the right side made all lines uppercase. To resolve the conflict, we would presumably keep the right side (the third section) and replace \"GRAPE\" by \"GRAPEFRUIT\". This way of visually finding the changes between the base and one side and then applying them to the other side is a common way of resolving conflicts when using Git's \"diff3\" style.
Jujutsu helps you by combining the base and one side into a unified diff for you, making it easier to spot the differences to apply to the other side. Here's how that would look for the same example as above:
<<<<<<<\n %%%%%%%\n apple\n -grape\n +grapefruit\n orange\n +++++++\n APPLE\n GRAPE\n ORANGE\n >>>>>>>\n
As in Git, the <<<<<<<
and >>>>>>>
lines mark the start and end of the conflict. The %%%%%%%
line indicates the start of a diff. The +++++++
line indicates the start of a snapshot (not a diff).
There is another reason for this format (in addition to helping you spot the differences): The format supports more complex conflicts involving more than 3 inputs. Such conflicts can arise when you merge more than 2 commits. They would typically be rendered as a single snapshot (as above) but with more than one unified diffs. The process for resolving them is similar: Manually apply each diff onto the snapshot.
"},{"location":"contributing/","title":"How to Contribute","text":""},{"location":"contributing/#policies","title":"Policies","text":"We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow.
"},{"location":"contributing/#contributor-license-agreement","title":"Contributor License Agreement","text":"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 your current agreements on file or to sign a new one.
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.
"},{"location":"contributing/#code-reviews","title":"Code reviews","text":"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.
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 start the commit message with <topic>:
and don't use conventional commits. 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.
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.
When your first PR has been approved, we typically give you contributor access, so you can address any remaining minor comments and then merge the PR yourself when you're ready. If you realize that some comments require non-trivial changes, please ask your reviewer to take another look.
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.
"},{"location":"contributing/#community-guidelines","title":"Community Guidelines","text":"This project follows Google's Open Source Community Guidelines.
"},{"location":"contributing/#contributing-large-patches","title":"Contributing large patches","text":"Before sending a PR for a large change which designs/redesigns or reworks an existing component, we require an architecture review from multiple stakeholders, which we do with Design Docs, see the process here.
"},{"location":"contributing/#contributing-to-the-documentation","title":"Contributing to the documentation","text":"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.
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++.
"},{"location":"contributing/#setting-up-a-development-environment","title":"Setting up a development environment","text":"To develop jj
, the mandatory steps are simply to install Rust (the default installer options are fine), clone the repository, and use cargo build
, cargo fmt
, cargo clippy --workspace --all-targets
, and cargo test --workspace
. If you are preparing a PR, there are some additional recommended steps.
One-time setup:
rustup toolchain add nightly # wanted for 'rustfmt'\nrustup toolchain add 1.76 # also specified in Cargo.toml\ncargo install cargo-insta\ncargo install cargo-watch\ncargo install cargo-nextest\n
During development (adapt according to your preference):
cargo watch --ignore '.jj/**' -s \\\n 'cargo clippy --workspace --all-targets \\\n && cargo +1.76 check --workspace --all-targets'\ncargo +nightly fmt # Occasionally\ncargo nextest run --workspace # Occasionally\ncargo insta test --workspace --test-runner nextest # Occasionally\n
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
.
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 the insta
crate. To make this convenient, install the cargo-insta
binary. Use cargo insta test --workspace
to run tests, and cargo insta review --workspace
to update the snapshot tests. The --workspace
flag is needed to run the tests on all crates; by default, only the crate in the current directory is tested.
GitHub CI checks require that the code is formatted with the nightly version of rustfmt
. To do this on your computer, install the nightly toolchain and use cargo +nightly fmt
.
Your code will be rejected if it cannot be compiled with the minimal supported version of Rust (\"MSRV\"). Currently, jj
follows a rather casual MSRV policy: \"The current rustc
stable version, minus one.\" As of this writing, that version is 1.76.0.
Your code needs to pass cargo clippy
. You can also use cargo +nightly clippy
if you wish to see more warnings.
You may also want to install and use cargo-watch
. In this case, you should exclude .jj
. directory from the filesystem watcher, as it gets updated on every jj log
.
To run tests more quickly, use cargo nextest run --workspace
. To use nextest
with insta
, use cargo insta test --workspace --test-runner nextest
.
On Linux, you may be able to speed up nextest
even further by using the mold
linker, as explained below.
mold
for faster tests on Linux","text":"On a machine with a multi-core CPU, one way to speed up cargo nextest
on Linux is to use the multi-threaded mold
linker. This linker may help if, currently, your CPU is underused while Rust is linking test binaries. Before proceeding with mold
, you can check whether this is an issue worth solving using a system monitoring tool such as htop
.
mold
is packaged for many distributions. On Debian, for example, sudo apt install mold
should just work.
A simple way to use mold
is via the -run
option, e.g.:
mold -run cargo insta test --workspace --test-runner nextest\n
There will be no indication that a different linker is used, except for higher CPU usage while linking and, hopefully, faster completion. You can verify that mold
was indeed used by running readelf -p .comment target/debug/jj
.
There are also ways of having Rust use mold
by default, see the \"How to use\" instructions.
On recent versions of MacOS, the default linker Rust uses is already multi-threaded. It should use all the CPU cores without any configuration.
"},{"location":"contributing/#previewing-the-html-documentation","title":"Previewing the HTML documentation","text":"The documentation for jj
is automatically published to the website at https://martinvonz.github.io/jj/.
When editing documentation, we'd appreciate it if you checked that the result will look as expected when published to the website.
"},{"location":"contributing/#setting-up-the-prerequisites","title":"Setting up the prerequisites","text":"To build the website, you must have Python and poetry 1.8+
installed (the latest version is recommended). It is easiest to install poetry
via pipx
, as explained in the Poetry installation instructions. A few helpful points from the instructions: pipx
can often be installed from your distribution, e.g. sudo apt install pipx
; this will usually also install Python for you if necessary. Any version of pipx
will do. If you are installing pipx
manually, you may first need to follow the Python installation instructions.
Once you have poetry
installed, you should ask it to install the rest of the required tools into a virtual environment as follows:
poetry install\n
You may get requests to \"unlock a keyring\", an error messages about failing to do so, or Poetry may simply hang indefinitely. The workaround is to either to unlock the keyring or to run the following, and then to try poetry install
again:
# For sh-compatible shells or recent versions of `fish`\nexport PYTHON_KEYRING_BACKEND=keyring.backends.fail.Keyring\n
"},{"location":"contributing/#building-the-html-docs-locally-with-live-reload","title":"Building the HTML docs locally (with live reload)","text":"The HTML docs are built with MkDocs. After following the above steps, you should be able to view the docs by running
# Note: this and all the commands below should be run from the root of\n# the `jj` source tree.\npoetry run -- mkdocs serve\n
and opening http://127.0.0.1:8000 in your browser.
As you edit the md
files, the website should be rebuilt and reloaded in your browser automatically, unless build errors occur.
You should occasionally check the terminal from which you ran mkdocs serve
for any build errors or warnings. Warnings about \"GET /versions.json HTTP/1.1\" code 404
are expected and harmless.
The full jj
website includes the documentation for several jj
versions (prerelease
, latest release, and the older releases). The top-level URL https://martinvonz.github.io/jj redirects to https://martinvonz.github.io/jj/latest, which in turn redirects to the docs for the last stable version.
The different versions of documentation are managed and deployed with mike
, which can be run with poetry 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
as a co-located jj + git
repository (jj clone --colocate
), cloned from your fork of jj
(e.g. jjfan.github.com/jj
). You can also use a pure Git repo if you prefer.
Make sure jjfan.github.com/jj
includes the gh-pages
bookmark of the jj repo and run git fetch origin gh-pages
.
Go to the GitHub repository settings, enable GitHub Pages, and configure them to use the gh-pages
bookmark (this is usually the default).
Run the same sh
script that is used in GitHub CI (details below):
.github/scripts/docs-build-deploy 'https://jjfan.github.io/jj/'\\\n prerelease main --push\n
This should build the version of the docs from the current commit, deploy it as a new commit to the gh-pages
bookmark, and push the gh-pages
bookmark to the origin.
Now, you should be able to see the full website, including your latest changes to the prerelease
version, at https://jjfan.github.io/jj/prerelease/
.
(Optional) The previous steps actually only rebuild https://jjfan.github.io/jj/prerelease/
and its alias https://jjfan.github.io/jj/main/
. If you'd like to test out version switching back and forth, you can also rebuild the docs for the latest release as follows.
jj new v1.33.1 # Let's say `jj 1.33.1` is the currently the latest release\n.github/scripts/docs-build-deploy 'https://jjfan.github.io/jj/'\\\n v1.33.1 latest --push\n
(Optional) When you are done, you may want to reset the gh-bookmarks
to the same spot as it is in the upstream. If you configured the upstream
remote, this can be done with:
# This will LOSE any changes you made to `gh-pages`\njj git fetch --remote upstream\njj bookmark set gh-pages -r gh-pages@upstream\njj git push --remote origin --bookmark gh-pages\n
If you want to preserve some of the changes you made, you can do jj bookmark set my-changes -r gh-pages
BEFORE running the above commands.
docs-build-deploy
script","text":"The script sets up the site_url
mkdocs config to 'https://jjfan.github.io/jj/'
. If this config does not match the URL where you loaded the website, some minor website features (like the version switching widget) will have reduced functionality.
Then, the script passes the rest of its arguments to potery run -- mike deploy
, which does the rest of the job. Run poetry run -- mike help deploy
to find out what the arguments do.
If you need to do something more complicated, you can use poetry 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://martinvonz.github.com/jj (instead of the usual main
bookmark).
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.
protoc
compiler. This usually means either apt-get install protobuf-compiler
or downloading an official release. The prost
library docs have additional advice.cargo run -p gen-protos
regularly (or after every edit to a .proto
file). This is the same as running cargo run
from lib/gen-protos
. The gen-protos
binary will use the prost-build
library to compile the .proto
files into .rs
files..proto
file, you will need to edit the list of these files in lib/gen-protos/src/main.rs
.The .rs
files generated from .proto
files are included in the repository, and there is a GitHub CI check that will complain if they do not match.
One easy-to-use sampling profiler is samply. For example:
cargo install samply\nsamply record jj diff\n
Then just open the link it prints. Another option is to use the instrumentation we've added manually (using tracing::instrument
) in various places. For example:
JJ_TRACE=/tmp/trace.json jj diff\n
Then go to https://ui.perfetto.dev/
in Chrome and load /tmp/trace.json
from there."},{"location":"design_doc_blueprint/","title":"Title","text":"A cool name for your Project
Author: Your-Name
If there are multiple authors, just list them all
"},{"location":"design_doc_blueprint/#summary","title":"Summary","text":"A short summary of your project/re-design/component and what problems it addresses in about 3-10 sentences.
"},{"location":"design_doc_blueprint/#state-of-the-feature-as-of-version-optional","title":"State of the Feature as of$VERSION
(optional)","text":"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.
"},{"location":"design_doc_blueprint/#prior-work-optional","title":"Prior work (optional)","text":"Does this feature exist somewhere else and which tradeoffs it made.
If there's no prior work, then use the related work section below.
"},{"location":"design_doc_blueprint/#goals-and-non-goals","title":"Goals and non-goals","text":"Direct goals of the project and features deemed not worth pursuing.
"},{"location":"design_doc_blueprint/#overview","title":"Overview","text":"A detailed overview of the project and the improvements it brings.
"},{"location":"design_doc_blueprint/#detailed-design","title":"Detailed Design","text":"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.
"},{"location":"design_doc_blueprint/#alternatives-considered-optional","title":"Alternatives considered (optional)","text":"Other alternatives to your suggested approach, and why they fall short.
"},{"location":"design_doc_blueprint/#issues-addressed-optional","title":"Issues addressed (optional)","text":"A list of issues which are addressed by this design.
"},{"location":"design_doc_blueprint/#related-work-optional","title":"Related Work (optional)","text":"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.
"},{"location":"design_doc_blueprint/#future-possibilities","title":"Future Possibilities","text":"The section for things which could be added to it or deemed out of scope during the discussion.
"},{"location":"design_docs/","title":"Jujutsu Design Docs","text":"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 the native backend or the server component for Jujutsu, you'll need to go through this process.
"},{"location":"design_docs/#process","title":"Process","text":"docs/design
, named after your improvement or project.You can find the base template of a new Design Doc here.
"},{"location":"filesets/","title":"Filesets","text":"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.
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'
The following patterns are supported:
\"path\"
, path
(the quotes are optional), or cwd:\"path\"
: Matches cwd-relative path prefix (file or files under directory recursively.)cwd-file:\"path\"
or file:\"path\"
: Matches cwd-relative file (or exact) path.cwd-glob:\"pattern\"
or glob:\"pattern\"
: Matches file paths with cwd-relative Unix-style shell wildcard pattern
. For example, glob:\"*.c\"
will match all .c
files in the current working directory non-recursively.root:\"path\"
: Matches workspace-relative path prefix (file or files under directory recursively.)root-file:\"path\"
: Matches workspace-relative file (or exact) path.root-glob:\"pattern\"
: Matches file paths with workspace-relative Unix-style shell wildcard pattern
.The following operators are supported. x
and y
below can be any fileset expressions.
~x
: Matches everything but x
.x & y
: Matches both x
and y
.x ~ y
: Matches x
but not y
.x | y
: Matches either x
or y
(or both).(listed in order of binding strengths)
You can use parentheses to control evaluation order, such as (x & y) | z
or x & (y | z)
.
You can also specify patterns by using functions.
all()
: Matches everything.none()
: Matches nothing.Show diff excluding Cargo.lock
.
jj diff '~Cargo.lock'\n
List files in src
excluding Rust sources.
jj file list 'src ~ glob:\"**/*.rs\"'\n
Split a revision in two, putting foo
into the second commit.
jj split '~foo'\n
"},{"location":"git-comparison/","title":"Comparison with Git","text":""},{"location":"git-comparison/#introduction","title":"Introduction","text":"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.
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 further down explaining how to achieve various use cases.
HEAD
and the working copy, so workflows that depend on it can be modeled using proper commits instead. Jujutsu has excellent support for moving changes between commits. Details.No 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.
jj rebase
), all its descendants commits will automatically be rebased on top. Branches pointing to it will also get updated, and so will the working copy if it points to any of the rebased commits.main
branch, you'll get a bookmark by that name in your local repo. If you then move it and push back to the remote, the main
branch on the remote will be updated. Details.git rebase --root
, git checkout --orphan
).Git's \"index\" has multiple roles. One role is as a cache of file system information. Jujutsu has something similar. Unfortunately, Git exposes the index to the user, which makes the CLI unnecessarily complicated (learning what the different flavors of git reset
do, especially when combined with commits and/or paths, usually takes a while). Jujutsu, like Mercurial, doesn't make that mistake.
As a Git power-user, you may think that you need the power of the index to commit only part of the working copy. However, Jujutsu provides commands for more directly achieving most use cases you're used to using Git's index for. For example, to create a commit from part of the changes in the working copy, you might be used to using git add -p; git commit
. With Jujutsu, you'd instead use jj split
to split the working-copy commit into two commits. To add more changes into the parent commit, which you might normally use git add -p; git commit --amend
for, you can instead use jj squash -i
to choose which changes to move into the parent commit, or jj squash <file>
to move a specific file.
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/amend -r <revision>
will move the diff from that revision into its parent.
jj git init [--colocate]
git init
Clone an existing repo jj git clone <source> <destination> [--remote <remote name>]
(there is no support for cloning non-Git repos yet) git clone <source> <destination> [--origin <remote name>]
Update the local repo with all bookmarks/branches from a remote jj git fetch [--remote <remote>]
(there is no support for fetching into non-Git repos yet) git fetch [<remote>]
Update a remote repo with all bookmarks/branches from the local repo jj git push --all [--remote <remote>]
(there is no support for pushing from non-Git repos yet) git push --all [<remote>]
Update a remote repo with a single bookmark from the local repo jj git push --bookmark <bookmark name> [--remote <remote>]
(there is no support for pushing from non-Git repos yet) git push <remote> <bookmark name>
Add a remote target to the repo jj git remote add <remote> <url>
git remote add <remote> <url>
Show summary of current work and repo status jj st
git status
Show diff of the current change jj diff
git diff HEAD
Show diff of another change jj diff -r <revision>
git diff <revision>^ <revision>
Show diff from another change to the current change jj diff --from <revision>
git diff <revision>
Show diff from change A to change B jj diff --from A --to B
git diff A B
Show description and diff of a change jj show <revision>
git show <revision>
Add a file to the current change touch filename
touch filename; git add filename
Remove a file from the current change rm filename
git rm filename
Modify a file in the current change echo stuff >> filename
echo stuff >> filename
Finish work on the current change and start a new change jj commit
git commit -a
See log of ancestors of the current commit jj log -r ::@
git log --oneline --graph --decorate
See log of all reachable commits jj log -r 'all()'
or jj log -r ::
git log --oneline --graph --decorate --branches
Show log of commits not on the main bookmark jj log
(TODO) List versioned files in the working copy jj file list
git ls-files --cached
Search among files versioned in the repository grep foo $(jj file list)
, or rg --no-require-git foo
git grep foo
Abandon the current change and start a new change jj abandon
git reset --hard
(cannot be undone) Make the current change empty jj restore
git reset --hard
(same as abandoning a change since Git has no concept of a \"change\") Abandon the parent of the working copy, but keep its diff in the working copy jj squash --from @-
git reset --soft HEAD~
Discard working copy changes in some files jj restore <paths>...
git restore <paths>...
or git checkout HEAD -- <paths>...
Edit description (commit message) of the current change jj describe
Not supported Edit description (commit message) of the previous change jj describe @-
git commit --amend --only
Temporarily put away the current change jj new @-
(the old working-copy commit remains as a sibling commit) (the old working-copy commit X can be restored with jj edit X
) git stash
Start working on a new change based on the <main> bookmark/branch jj new main
git switch -c topic main
or git checkout -b topic main
(may need to stash or commit first) Move bookmark/branch A onto bookmark/branch B jj rebase -b A -d B
git rebase B A
(may need to rebase other descendant branches separately) Move change A and its descendants onto change B jj rebase -s A -d B
git rebase --onto B A^ <some descendant bookmark>
(may need to rebase other descendant bookmarks separately) Reorder changes from A-B-C-D to A-C-B-D jj rebase -r C --before B
git rebase -i A
Move the diff in the current change into the parent change jj squash/amend
git commit --amend -a
Interactively move part of the diff in the current change into the parent change jj squash/amend -i
git add -p; git commit --amend
Move the diff in the working copy into an ancestor jj squash --into X
git commit --fixup=X; git rebase -i --autosquash X^
Interactively move part of the diff in an arbitrary change to another arbitrary change jj squash -i --from X --into Y
Not supported Interactively split the changes in the working copy in two jj split
git commit -p
Interactively split an arbitrary change in two jj split -r <revision>
Not supported (can be emulated with the \"edit\" action in git rebase -i
) Interactively edit the diff in a given change jj diffedit -r <revision>
Not supported (can be emulated with the \"edit\" action in git rebase -i
) Resolve conflicts and continue interrupted operation echo resolved > filename; jj squash/amend
(operations don't get interrupted, so no need to continue) echo resolved > filename; git add filename; git rebase/merge/cherry-pick --continue
Create a copy of a commit on top of another commit jj duplicate <source>; jj rebase -r <duplicate commit> -d <destination>
(there's no single command for it yet) git co <destination>; git cherry-pick <source>
Find the root of the working copy (or check if in a repo) jj workspace root
git rev-parse --show-toplevel
List bookmarks/branches jj bookmark list
or jj b l
for short git branch
Create a bookmark/branch jj bookmark create <name> -r <revision>
git branch <name> <revision>
Move a bookmark/branch forward jj bookmark move <name> --to <revision>
or jj b m <name> --to <revision>
for short git branch -f <name> <revision>
Move a bookmark/branch backward or sideways jj bookmark move <name> --to <revision> --allow-backwards
git branch -f <name> <revision>
Delete a bookmark/branch jj bookmark delete <name>
git branch --delete <name>
See log of operations performed on the repo jj op log
Not supported Undo an earlier operation jj [op] undo <operation ID>
(jj undo
is an alias for jj op undo
) Not supported Create a commit that cancels out a previous commit jj backout -r <revision>
git revert <revision>
"},{"location":"git-compatibility/","title":"Git compatibility","text":"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).
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.
~/.gitconfig
) that's respected is the following. Feel free to file a bug if you miss any particular configuration options.[remote \"<name>\"]
).core.excludesFile
ssh-agent
, a password-less key ( only ~/.ssh/id_rsa
, ~/.ssh/id_ed25519
or ~/.ssh/id_ed25519_sk
), or a credential.helper
..gitignore
files are supported. So are ignores in .git/info/exclude
or configured via Git's core.excludesfile
config. The .gitignore
support uses a native implementation, so please report a bug if you notice any difference compared to git
.eol
attribute.jj diff
will show a diff from the Git HEAD to the working copy. There are ways of fulfilling your use cases without a staging area.git gc
in the Git repo, but it's not tested, so it's probably a good idea to make a backup of the whole workspace first. There's no garbage collection and repacking of Jujutsu's own data structures yet, however.jj git init --git-repo=<path>
to create a repo backed by a bare Git repo.jj workspace
family of commands.jj sparse
command.To create an empty repo using the Git backend, use jj init --git <name>
. Since the command creates a Jujutsu repo, it will have a .jj/
directory. The underlying Git repo will be inside of that directory (currently in .jj/repo/store/git/
).
To create a Jujutsu repo backed by a Git repo you already have on disk, use jj git init --git-repo=<path to Git repo> <name>
. The repo will work similar to a Git worktree, meaning that the working copies files and the record of the working-copy commit will be separate, but the commits will be accessible in both repos. Use jj git import
to update the Jujutsu repo with changes made in the Git repo. Use jj git export
to update the Git repo with changes made in the Jujutsu repo.
To create a Jujutsu repo from a remote Git URL, use jj git clone <URL> [<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.
A \"co-located\" Jujutsu repo is a hybrid Jujutsu/Git repo. These can be created if you initialize the Jujutsu repo in an existing Git repo by running jj git init --colocate
or with jj git clone --colocate
. The Git repo and the Jujutsu repo 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 repo 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.
There are a few downsides to this mode of operation. Generally, using co-located repos may require you to deal with more involved Jujutsu and Git concepts.
Interleaving jj
and git
commands increases the chance of confusing branch conflicts or conflicted (AKA divergent) change ids. These never lose data, but can be annoying.
Such interleaving can happen unknowingly. For example, some IDEs can cause it because they automatically run git fetch
in the background from time to time.
In co-located repos with a very large number of branches or other refs, jj
commands can get noticeably slower because of the automatic jj git import
executed on each command. This can be mitigated by occasionally running jj util gc
to speed up the import (that command includes packing the Git refs).
Git tools will have trouble with revisions that contain conflicted files. While jj
renders these files with conflict markers in the working copy, they are stored in a non-human-readable fashion inside the repo. Git tools will often see this non-human-readable representation.
When a jj
branch is conflicted, the position of the branch in the Git repo will disagree with one or more of the conflicted positions. The state of that branch in git will be labeled as though it belongs to a remote named \"git\", e.g. branch@git
.
Jujutsu will ignore Git's staging area. It will not understand merge conflicts as Git represents them, unfinished git rebase
states, as well as other less common states a Git repository can be in.
Colocated repositories are less resilient to concurrency issues if you share the repo using an NFS filesystem or Dropbox. In general, such use of Jujutsu is not currently thoroughly tested.
There may still be bugs when interleaving mutating jj
and git
commands, usually having to do with a branch pointer ending up in the wrong place. We are working on the known ones, and are not aware of any major ones. Please report any new ones you find, or if any of the known bugs are less minor than they appear.
A Jujutsu repo backed by a Git repo has a full Git repo inside, so it is technically possible (though not officially supported) to convert it into a co-located repo like so:
# Move the Git repo\nmv .jj/repo/store/git .git\n# Tell jj where to find it\necho -n '../../../.git' > .jj/repo/store/git_target\n# Ignore the .jj directory in Git\necho '/*' > .jj/.gitignore\n# Make the Git repository non-bare and set HEAD\ngit config --unset core.bare\njj new @-\n
We may officially support this in the future. If you try this, we would appreciate feedback and bug reports.
"},{"location":"git-compatibility/#branches","title":"Branches","text":"TODO: Describe how branches are mapped
"},{"location":"git-compatibility/#format-mapping-details","title":"Format mapping details","text":"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.
Commit metadata that cannot be represented in Git commits (such as the Change ID and information about conflicts) is stored outside of the Git repo (currently in .jj/store/extra/
).
Commits with conflicts cannot be represented in Git. They appear in the Git commit as 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 the Git-external storage mentioned in the paragraph above. 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 those directories, making it look like they replaced all the other paths in your repo. You will probably want to run jj abandon
to get back to the state with the unresolved conflicts.
This guide assumes a basic understanding of either Git or Mercurial.
"},{"location":"github/#set-up-an-ssh-key","title":"Set up an SSH key","text":"As of October 2023 it's recommended to set up an SSH key to work with GitHub projects. See GitHub's Tutorial. This restriction may be lifted in the future, see issue #469 for more information and progress on authenticated HTTP.
"},{"location":"github/#basic-workflow","title":"Basic workflow","text":"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.
"},{"location":"github/#using-a-generated-bookmark-name","title":"Using a generated bookmark name","text":"In this example we're letting Jujutsu auto-create a bookmark.
# Start a new commit off of the default bookmark.\n$ jj new main\n# Refactor some files, then add a description and start a new commit\n$ jj commit -m 'refactor(foo): restructure foo()'\n# Add a feature, then add a description and start a new commit\n$ jj commit -m 'feat(bar): add support for bar'\n# Let Jujutsu generate a bookmark name and push that to GitHub. Note that we\n# push the working-copy commit's *parent* because the working-copy commit\n# itself is empty.\n$ jj git push -c @-\n
"},{"location":"github/#using-a-named-bookmark","title":"Using a named bookmark","text":"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.\n$ jj new main\n# Refactor some files, then add a description and start a new commit\n$ jj commit -m 'refactor(foo): restructure foo()'\n# Add a feature, then add a description and start a new commit\n$ jj commit -m 'feat(bar): add support for bar'\n# Create a bookmark so we can push it to GitHub. Note that we created the bookmark\n# on the working-copy commit's *parent* because the working copy itself is empty.\n$ jj bookmark create bar -r @- # `bar` now contains the previous two commits.\n# Push the bookmark to GitHub (pushes only `bar`)\n$ jj git push\n
While it's possible to create a bookmark in advance and commit on top of it in a Git-like manner, you will then need to move the bookmark manually when you create a new commits. Unlike Git, Jujutsu will not do it automatically.
"},{"location":"github/#updating-the-repository","title":"Updating the repository","text":"As of October 2023, Jujutsu has no equivalent to a git pull
command (see issue #1039). Until such a command is added, you need to use jj git fetch
followed by a jj rebase -d $main_bookmark
to update your changes.
After doing jj git init --colocate
, Git will be in a detached HEAD state, which is unusual, as Git mainly works with bookmarks. In a co-located repository, 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\n$ # Do some more work.\n$ jj commit -m \"Update tutorial\"\n# Create a bookmark on the working-copy commit's parent\n$ jj bookmark create doc-update -r @-\n$ jj git push\n
"},{"location":"github/#working-in-a-jujutsu-repository","title":"Working in a Jujutsu repository","text":"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\n$ jj commit\n$ # Push change \"mw\", letting Jujutsu automatically create a bookmark called\n$ # \"push-mwmpwkwknuz\"\n$ jj git push --change mw\n
"},{"location":"github/#addressing-review-comments","title":"Addressing review comments","text":"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 bookmark1. Some projects (such as Jujutsu and LLVM) instead prefer that you keep your commits clean by rewriting them and then force-pushing2.
"},{"location":"github/#adding-new-commits","title":"Adding new commits","text":"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.\n$ jj new your-feature\n$ # Address the comments by updating the code. Then review the changes.\n$ jj diff\n$ # Give the fix a description and create a new working-copy on top.\n$ jj commit -m 'address pr comments'\n$ # Update the bookmark to point to the new commit.\n$ jj bookmark set your-feature -r @-\n$ # Push it to your remote\n$ jj git push\n
Notably, the above workflow creates a new commit for you. The same can be achieved without creating a new commit.
Warning We strongly suggest to jj new
after the example below, as all further edits still get amended to the previous commit.
$ # Create a new commit on top of the `your-feature` bookmark from above.\n$ jj new your-feature\n$ # Address the comments by updating the code. Then review the changes.\n$ jj diff\n$ # Give the fix a description.\n$ jj describe -m 'address pr comments'\n$ # Update the bookmark to point to the current commit.\n$ jj bookmark set your-feature -r @\n$ # Push it to your remote\n$ jj git push\n
"},{"location":"github/#rewriting-commits","title":"Rewriting commits","text":"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`,\n$ # as reviewers requested a fix there.\n$ jj new your-feature- # NOTE: the trailing hyphen is not a typo!\n$ # Address the comments by updating the code. Then review the changes.\n$ jj diff\n$ # Squash the changes into the parent commit\n$ jj squash\n$ # Push the updated bookmark to the remote. Jujutsu automatically makes it a\n$ # force push\n$ jj git push --bookmark your-feature\n
The hyphen after your-feature
comes from the revset syntax.
By default, jj git clone
imports the default remote bookmark (which is usually main
or master
), but jj git fetch
doesn't import new remote bookmarks to local bookmarks. This means that if you want to iterate or test another contributor's bookmark, you'll need to do jj new <bookmark>@<remote>
onto it.
If you want to import all remote bookmarks including inactive ones, set git.auto-local-bookmark = true
in the config file. Then you can specify a contributor's bookmark as jj new <bookmark>
instead of jj new <bookmark>@<remote>
.
You can find more information on that setting here.
"},{"location":"github/#using-github-cli","title":"Using GitHub CLI","text":"GitHub CLI will have trouble finding the proper Git repository path in jj repos that aren't co-located (see issue #1008). You can configure the $GIT_DIR
environment variable to point it to the right path:
$ GIT_DIR=.jj/repo/store/git gh issue list\n
You can make that automatic by installing direnv and defining hooks in a .envrc
file in the repository root to configure $GIT_DIR
. Just add this line into .envrc
:
export GIT_DIR=$PWD/.jj/repo/store/git\n
and run direnv allow
to approve it for direnv to run. Then GitHub CLI will work automatically even in repos that aren't co-located so you can execute commands like gh issue list
normally.
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())'\n
Log all revisions that you authored, across all bookmarks that aren't on any remote:
$ jj log -r 'mine() & bookmarks() & ~remote_bookmarks()'\n
Log all remote bookmarks that you authored or committed to:
$ jj log -r 'remote_bookmarks() & (mine() | committer(your@email.com))'\n
Log all descendants of the current working copy that aren't on any remote:
$ jj log -r '::@ & ~remote_bookmarks()'\n
"},{"location":"github/#merge-conflicts","title":"Merge conflicts","text":"For a detailed overview, how Jujutsu handles conflicts, revisit the tutorial.
"},{"location":"github/#using-several-remotes","title":"Using several remotes","text":"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\n$ cd repo\n$ jj git remote add origin git@github.com:your-org/your-repo-fork\n
This will automatically setup your repository to track the main bookmark from the upstream repository, typically main@upstream
or master@upstream
.
You might want to jj git fetch
from \"upstream\" and to jj git push
to \"origin\". You can configure the default remotes to fetch from and push to in your configuration file (for example, .jj/repo/config.toml
):
[git]\nfetch = \"upstream\"\npush = \"origin\"\n
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]\nfetch = [\"upstream\", \"origin\"]\npush = \"origin\"\n
This is a GitHub-style review, as GitHub currently only is able to compare bookmarks.\u00a0\u21a9
If you're wondering why we prefer clean commits in this project, see e.g. this blog post \u21a9
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.
"},{"location":"glossary/#backend","title":"Backend","text":"A backend is an implementation of the storage layer. There are currently two builtin commit backends: the Git backend and the native backend. The Git backend stores commits in a Git repository. The native backend is used for testing purposes only. Alternative backends could be used, for example, if somebody wanted to use jj with a humongous monorepo (as Google does).
There are also pluggable backends for storing other information than commits, such as the \"operation store backend\" for storing the operation log.
"},{"location":"glossary/#bookmark","title":"Bookmark","text":"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.
"},{"location":"glossary/#branch","title":"Branch","text":"In the context of jj
, the work \"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 co-located repository, each local Git branch corresponds to a jj
bookmark.
A change is a commit as it evolves over time.
"},{"location":"glossary/#change-id","title":"Change ID","text":"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.
For the git backend, Change IDs are currently maintained only locally and not exchanged via push/fetch operations.
"},{"location":"glossary/#commit","title":"Commit","text":"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 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\".
"},{"location":"glossary/#commit-id","title":"Commit ID","text":"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.
When using the Git backend and the backing Git repository's .git/
directory is a sibling of .jj/
, we call the repository \"co-located\". Most tools designed for Git can be easily used on such repositories. jj
and git
commands can be used interchangeably.
See here for details.
"},{"location":"glossary/#conflict","title":"Conflict","text":"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.
"},{"location":"glossary/#divergent-change","title":"Divergent change","text":"A divergent change is a change that has more than one visible commit.
"},{"location":"glossary/#head","title":"Head","text":"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.
See visible commits.
"},{"location":"glossary/#operation","title":"Operation","text":"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.
"},{"location":"glossary/#operation-log","title":"Operation log","text":"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.
"},{"location":"glossary/#repository","title":"Repository","text":"Basically everything under .jj/
, i.e. the full set of operations and commits.
TODO
"},{"location":"glossary/#revision","title":"Revision","text":"A synonym for Commit.
"},{"location":"glossary/#revset","title":"Revset","text":"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.
"},{"location":"glossary/#rewrite","title":"Rewrite","text":"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.
"},{"location":"glossary/#root-commit","title":"Root commit","text":"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.
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.
"},{"location":"glossary/#tracked-bookmarks-and-tracking-bookmarks","title":"Tracked bookmarks and tracking bookmarks","text":"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.
"},{"location":"glossary/#visible-commits","title":"Visible commits","text":"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, visible commits are the \"latest versions\" of a revision with a given change id. A commit that's abandoned or rewritten stops being visible and is labeled as \"hidden\". Such commits are no longer accessible using a change id, but they are still accessible by their commit id.
"},{"location":"glossary/#view","title":"View","text":"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.
"},{"location":"glossary/#workspace","title":"Workspace","text":"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\".
"},{"location":"glossary/#working-copy","title":"Working copy","text":"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\".
"},{"location":"glossary/#working-copy-commit","title":"Working-copy commit","text":"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.
"},{"location":"install-and-setup/","title":"Installation and setup","text":""},{"location":"install-and-setup/#installation","title":"Installation","text":""},{"location":"install-and-setup/#download-pre-built-binaries-for-a-release","title":"Download pre-built binaries for a release","text":"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).
If you'd like to install a prerelease version, you'll need to use one of the options below.
"},{"location":"install-and-setup/#cargo-binstall","title":"Cargo Binstall","text":"If you use cargo-binstall
, you can install the same binaries of the last jj
release from GitHub as follows:
# Will put the jj binary for the latest release in ~/.cargo/bin by default\ncargo binstall --strategies crate-meta-data jj-cli\n
Without the --strategies
option, you may get equivalent binaries that should be compiled from the same source code.
First make sure that you have a Rust version >= 1.76 and that the libssl-dev
, openssl
, pkg-config
, and build-essential
packages are installed by running something like this:
sudo apt-get install libssl-dev openssl pkg-config build-essential\n
Now run either:
# To install the *prerelease* version from the main branch\ncargo install --git https://github.com/martinvonz/jj.git --locked --bin jj jj-cli\n
or:
# To install the latest release\ncargo install --locked --bin jj jj-cli\n
"},{"location":"install-and-setup/#arch-linux","title":"Arch Linux","text":"You can install the jujutsu
package from the official extra repository:
pacman -S jujutsu\n
Or install from the AUR repository with an AUR Helper:
yay -S jujutsu-git\n
"},{"location":"install-and-setup/#nix-os","title":"Nix OS","text":"If you're on Nix OS 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:martinvonz/jj'\n
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\nnix profile install 'github:martinvonz/jj'\n
"},{"location":"install-and-setup/#homebrew","title":"Homebrew","text":"If you use linuxbrew, you can run:
# Installs the latest release\nbrew install jj\n
"},{"location":"install-and-setup/#gentoo-linux","title":"Gentoo Linux","text":"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/jj\n
"},{"location":"install-and-setup/#mac","title":"Mac","text":""},{"location":"install-and-setup/#from-source-vendored-openssl","title":"From Source, Vendored OpenSSL","text":"First make sure that you have a Rust version >= 1.76. You may also need to run:
xcode-select --install\n
Now run either:
# To install the *prerelease* version from the main branch\ncargo install --git https://github.com/martinvonz/jj.git \\\n --features vendored-openssl --locked --bin jj jj-cli\n
or:
# To install the latest release\ncargo install --features vendored-openssl --locked --bin jj jj-cli\n
"},{"location":"install-and-setup/#from-source-homebrew-openssl","title":"From Source, Homebrew OpenSSL","text":"First make sure that you have a Rust version >= 1.76. You will also need Homebrew installed. You may then need to run some or all of these:
xcode-select --install\nbrew install openssl\nbrew install pkg-config\nexport PKG_CONFIG_PATH=\"$(brew --prefix)/opt/openssl@3/lib/pkgconfig\"\n
Now run either:
# To install the *prerelease* version from the main branch\ncargo install --git https://github.com/martinvonz/jj.git --locked --bin jj jj-cli\n
or:
# To install the latest release\ncargo install --locked --bin jj jj-cli\n
"},{"location":"install-and-setup/#homebrew_1","title":"Homebrew","text":"If you use Homebrew, you can run:
# Installs the latest release\nbrew install jj\n
"},{"location":"install-and-setup/#macports","title":"MacPorts","text":"You can also install jj
via the MacPorts jujutsu
port:
# Installs the latest release\nsudo port install jujutsu\n
"},{"location":"install-and-setup/#windows","title":"Windows","text":"First make sure that you have a Rust version >= 1.76. Now run either:
# To install the *prerelease* version from the main branch\ncargo install --git https://github.com/martinvonz/jj.git --locked --bin jj jj-cli --features vendored-openssl\n
or:
# To install the latest release\ncargo install --locked --bin jj jj-cli --features vendored-openssl\n
"},{"location":"install-and-setup/#initial-configuration","title":"Initial configuration","text":"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\"\n$ jj config set --user user.email \"martinvonz@google.com\"\n
"},{"location":"install-and-setup/#command-line-completion","title":"Command-line completion","text":"To set up command-line completion, source the output of jj util completion bash/zsh/fish
. Exactly how to source it depends on your shell.
source <(jj util completion bash)\n
"},{"location":"install-and-setup/#zsh","title":"Zsh","text":"autoload -U compinit\ncompinit\nsource <(jj util completion zsh)\n
"},{"location":"install-and-setup/#fish","title":"Fish","text":"jj util completion fish | source\n
"},{"location":"install-and-setup/#nushell","title":"Nushell","text":"jj util completion nushell | save completions-jj.nu\nuse completions-jj.nu * # Or `source completions-jj.nu`\n
"},{"location":"install-and-setup/#xonsh","title":"Xonsh","text":"source-bash $(jj util completion)\n
"},{"location":"operation-log/","title":"Operation log","text":""},{"location":"operation-log/#introduction","title":"Introduction","text":"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 an operation (jj [op] undo
), which doesn't need to be the most recent one. It also lets you restore the entire repo to the way it looked at an earlier point (jj op restore
).
When referring to operations, you can use @
to represent the current operation.
The following operators are supported:
x-
: Parents of x
(e.g. @-
)x+
: Children of x
One benefit of the operation log (and the reason for its creation) is that it allows lock-free concurrency -- you can run concurrent jj
commands without corrupting the repo, even if you run the commands on different machines that access the repo via a distributed file system (as long as the file system guarantees that a write is only visible once previous writes are visible). When you run a jj
command, it will start by loading the repo at the latest operation. It will not see any changes written by concurrent commands. If there are conflicts, you will be informed of them by subsequent jj st
and/or jj log
commands.
As an example, let's say you had started editing the description of a change and then also update the contents of the change (maybe because you had forgotten the editor). When you eventually close your editor, the command will succeed and e.g. jj log
will indicate that the change has diverged.
The top-level --at-operation/--at-op
option allows you to load the repo at a specific operation. This can be useful for understanding how your repo got into the current state. It can be even more useful for understanding why someone else's repo got into its current state.
When you use --at-op
, the automatic snapshotting of the working copy will not take place. When referring to a revision with the @
symbol (as many commands do by default), that will resolve to the working-copy commit recorded in the operation's view (which is actually how it always works -- it's just the snapshotting that's skipped with --at-op
).
As a top-level option, --at-op
can be passed to any command. However, you will typically only want to run read-only commands. For example, jj log
, jj st
, and jj diff
all make sense. It's still possible to run e.g. jj --at-op=<some operation ID> describe
. That's equivalent to having started jj describe
back when the specified operation was the most recent operation and then let it run until now (which can be done for that particular command by not closing the editor). There's practically no good reason to do that other than to simulate concurrent commands.
Similar tools:
git move
). Under heavy development and quickly gaining new features.Jujutsu supports a functional language for selecting a set of revisions. Expressions in this language are called \"revsets\" (the idea comes from Mercurial). The language consists of symbols, operators, and functions.
Most jj
commands accept a revset (or multiple). Many commands, such as jj diff -r <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.
Most revsets search only the visible commits. Other commits are only included if you explicitly mention them (e.g. by commit ID or a Git ref pointing to them).
"},{"location":"revsets/#symbols","title":"Symbols","text":"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 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 all visible commits with that change ID (there is typically only one visible commit with a given change ID). A unique prefix of the full change ID can also be used. It is an error to use a non-unique prefix.
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-\"'
.
Jujutsu attempts to resolve a symbol in the following order:
The following operators are supported. x
and y
below can be any revset, not only symbols.
x-
: Parents of x
, can be empty.x+
: Children of x
, can be empty.x::
: Descendants of x
, including the commits in x
itself. Shorthand for x::visible_heads()
.x..
: Revisions that are not ancestors of x
. Shorthand for x..visible_heads()
.::x
: Ancestors of x
, including the commits in x
itself. Shorthand for root()::x
...x
: Ancestors of x
, including the commits in x
itself, but excluding the root commit. Shorthand for root()..x
. Equivalent to ::x ~ root()
.x::y
: Descendants of x
that are also ancestors of y
. Equivalent to x:: & ::y
. This is what git log
calls --ancestry-path x..y
.x..y
: Ancestors of y
that are not also ancestors of x
. Equivalent to ::y ~ ::x
. This is what git log
calls x..y
(i.e. the same as we call it).::
: All visible commits in the repo. Shorthand for root()::visible_heads()
. Equivalent to all()
...
: All visible commits in the repo, but excluding the root commit. Shorthand for root()..visible_heads()
. Equivalent to ~root()
.~x
: Revisions that are not in x
.x & y
: Revisions that are in both x
and y
.x ~ y
: Revisions that are in x
but not in y
.x | y
: Revisions that are in either x
or y
(or both).(listed in order of binding strengths)
You can use parentheses to control evaluation order, such as (x & y) | z
or x & (y | z)
.
Given this history:
D\n|\\\n| o C\n| |\no | B\n|/\no A\n|\no root()\n
Operator x-
D-
\u21d2 {C,B}
B-
\u21d2 {A}
A-
\u21d2 {root()}
root()-
\u21d2 {}
(empty set)none()-
\u21d2 {}
(empty set)(D|A)-
\u21d2 {C,B,root()}
(C|B)-
\u21d2 {A}
Operator x+
D+
\u21d2 {}
(empty set)B+
\u21d2 {D}
A+
\u21d2 {B,C}
root()+
\u21d2 {A}
none()+
\u21d2 {}
(empty set)(C|B)+
\u21d2 {D}
(B|root())+
\u21d2 {D,A}
Operator x::
D::
\u21d2 {D}
B::
\u21d2 {D,B}
A::
\u21d2 {D,C,B,A}
root()::
\u21d2 {D,C,B,A,root()}
none()::
\u21d2 {}
(empty set)(C|B)::
\u21d2 {D,C,B}
Operator x..
D..
\u21d2 {}
(empty set)B..
\u21d2 {D,C}
(note that, unlike B::
, this includes C
)A..
\u21d2 {D,C,B}
root()..
\u21d2 {D,C,B,A}
none()..
\u21d2 {D,C,B,A,root()}
(C|B)..
\u21d2 {D}
Operator ::x
::D
\u21d2 {D,C,B,A,root()}
::B
\u21d2 {B,A,root()}
::A
\u21d2 {A,root()}
::root()
\u21d2 {root()}
::none()
\u21d2 {}
(empty set)::(C|B)
\u21d2 {C,B,A,root()}
Operator ..x
..D
\u21d2 {D,C,B,A}
..B
\u21d2 {B,A}
..A
\u21d2 {A}
..root()
\u21d2 {}
(empty set)..none()
\u21d2 {}
(empty set)..(C|B)
\u21d2 {C,B,A}
Operator x::y
D::D
\u21d2 {D}
B::D
\u21d2 {D,B}
(note that, unlike B..D
, this includes B
and excludes C
)A::D
\u21d2 {D,C,B,A}
root()::D
\u21d2 {D,C,B,A,root()}
none()::D
\u21d2 {}
(empty set)D::B
\u21d2 {}
(empty set)(C|B)::(C|B)
\u21d2 {C,B}
Operator x..y
D..D
\u21d2 {}
(empty set)B..D
\u21d2 {D,C}
(note that, unlike B::D
, this includes C
and excludes B
)A..D
\u21d2 {D,C,B}
root()..D
\u21d2 {D,C,B,A}
none()..D
\u21d2 {D,C,B,A,root()}
D..B
\u21d2 {}
(empty set)(C|B)..(C|B)
\u21d2 {}
(empty set)You can also specify revisions by using functions. Some functions take other revsets (expressions) as arguments.
parents(x)
: Same as x-
.
children(x)
: Same as x+
.
ancestors(x[, depth])
: ancestors(x)
is the same as ::x
. ancestors(x, depth)
returns the ancestors of x
limited to the given depth
.
descendants(x[, depth])
: descendants(x)
is the same as x::
. descendants(x, depth)
returns the descendants of x
limited to the given depth
.
reachable(srcs, domain)
: All commits reachable from srcs
within domain
, traversing all parent and child edges.
connected(x)
: Same as x::x
. Useful when x
includes several commits.
all()
: All visible commits in the repo.
none()
: No commits. This function is rarely useful; it is provided for completeness.
bookmarks([pattern])
: All local bookmark targets. If pattern
is specified, this selects the bookmarks whose name match the given string pattern. For example, bookmarks(push)
would match the bookmarks push-123
and repushed
but not the bookmark main
. If a bookmark is in a conflicted state, all its possible targets are included.
remote_bookmarks([bookmark_pattern[, [remote=]remote_pattern]])
: All remote bookmarks targets across all remotes. If just the bookmark_pattern
is specified, the bookmarks whose names match the given string pattern across all remotes are selected. If both bookmark_pattern
and remote_pattern
are specified, the selection is further restricted to just the remotes whose names match remote_pattern
.
For example, remote_bookmarks(push, ri)
would match the bookmarks push-123@origin
and repushed@private
but not push-123@upstream
or main@origin
or main@upstream
. If a bookmark is in a conflicted state, all its possible targets are included.
While Git-tracking bookmarks can be selected by <name>@git
, these bookmarks aren't included in remote_bookmarks()
.
tracked_remote_bookmarks([bookmark_pattern[, [remote=]remote_pattern]])
: All targets of tracked remote bookmarks. Supports the same optional arguments as remote_bookmarks()
.
untracked_remote_bookmarks([bookmark_pattern[, [remote=]remote_pattern]])
: All targets of untracked remote bookmarks. Supports the same optional arguments as remote_bookmarks()
.
tags()
: All tag targets. If a tag is in a conflicted state, all its possible targets are included.
git_refs()
: All Git ref targets as of the last import. If a Git ref is in a conflicted state, all its possible targets are included.
git_head()
: The Git HEAD
target as of the last import.
visible_heads()
: All visible heads (same as heads(all())
).
root()
: The virtual commit that is the oldest ancestor of all other commits.
heads(x)
: Commits in x
that are not ancestors of other commits in x
. Note that this is different from Mercurial's heads(x)
function, which is equivalent to x ~ x-
.
roots(x)
: Commits in x
that are not descendants of other commits in x
. Note that this is different from Mercurial's roots(x)
function, which is equivalent to x ~ x+
.
latest(x[, count])
: Latest count
commits in x
, based on committer timestamp. The default count
is 1.
merges()
: Merge commits.
description(pattern)
: Commits that have a description matching the given string pattern.
author(pattern)
: Commits with the author's name or email matching the given string pattern.
mine()
: Commits where the author's email matches the email of the current user.
committer(pattern)
: Commits with the committer's name or email matching the given string pattern.
author_date(pattern)
: Commits with author dates matching the specified date pattern.
committer_date(pattern)
: Commits with committer dates matching the specified date pattern.
empty()
: Commits modifying no files. This also includes merges()
without user modifications and root()
.
files(expression)
: Commits modifying paths matching the given fileset expression.
Paths are relative to the directory jj
was invoked from. A directory name will match all files in that directory and its subdirectories.
For example, files(foo)
will match files foo
, foo/bar
, foo/bar/baz
. It will not match foobar
or bar/foo
.
Some file patterns might need quoting because the expression
must also be parsable as a revset. For example, .
has to be quoted in files(\".\")
.
diff_contains(text[, files])
: Commits containing diffs matching the given text
pattern line by line.
The search paths can be narrowed by the files
expression. All modified files are scanned by default, but it is likely to change in future version to respect the command line path arguments.
For example, diff_contains(\"TODO\", \"src\")
will search revisions where \"TODO\" is added to or removed from files under \"src\".
conflicts()
: Commits with conflicts.
present(x)
: Same as x
, but evaluated to none()
if any of the commits in x
doesn't exist (e.g. is an unknown bookmark name.)
coalesce(revsets...)
: Commits in the first revset in the list of revsets
which does not evaluate to none()
. If all revsets evaluate to none()
, then the result of coalesce
will also be none()
.
working_copies()
: The working copy commits across all the workspaces.
at_operation(op, x)
: Evaluates x
at the specified operation. For example, at_operation(@-, visible_heads())
will return all heads which were visible at the previous operation.
Given this history:
E\n|\n| D\n|/|\n| o C\n| |\no | B\n|/\no A\n|\no root()\n
function reachable()
reachable(E, A..)
\u21d2 {E,D,C,B}
reachable(D, A..)
\u21d2 {E,D,C,B}
reachable(C, A..)
\u21d2 {E,D,C,B}
reachable(B, A..)
\u21d2 {E,D,C,B}
reachable(A, A..)
\u21d2 {}
(empty set)function connected()
connected(E|A)
\u21d2 {E,B,A}
connected(D|A)
\u21d2 {D,C,B,A}
connected(A)
\u21d2 {A}
function heads()
heads(E|D)
\u21d2 {E,D}
heads(E|C)
\u21d2 {E,C}
heads(E|B)
\u21d2 {E}
heads(E|A)
\u21d2 {E}
heads(A)
\u21d2 {A}
function roots()
roots(E|D)
\u21d2 {E,D}
roots(E|C)
\u21d2 {E,C}
roots(E|B)
\u21d2 {B}
roots(E|A)
\u21d2 {A}
roots(A)
\u21d2 {A}
Functions that perform string matching support the following pattern syntax:
\"string\"
, or string
(the quotes are optional), or substring:\"string\"
: Matches strings that contain string
.exact:\"string\"
: Matches strings exactly equal to string
.glob:\"pattern\"
: Matches strings with Unix-style shell wildcard pattern
.regex:\"pattern\"
: Matches substrings with regular expression pattern
.You can append -i
after the kind to match case\u2010insensitively (e.g. glob-i:\"fix*jpeg*\"
).
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:
New symbols and functions 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]\n'HEAD' = '@-'\n'user()' = 'user(\"me@example.org\")'\n'user(x)' = 'author(x) | committer(x)'\n
"},{"location":"revsets/#built-in-aliases","title":"Built-in Aliases","text":"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 trunk bookmark of the remote named origin
or upstream
. The bookmarks main
, master
, and trunk
are tried. If more than one potential trunk commit exists, the newest one is chosen. If none of the bookmarks exist, the revset evaluates to root()
.
When working with an existing Git repository (via jj git clone
or jj git init
), trunk()
will be overridden at the repository level to the default bookmark of the remote origin
.
You can override this as appropriate. If you do, make sure it always resolves to exactly one commit. For example:
[revset-aliases]\n'trunk()' = 'your-bookmark@your-remote'\n
builtin_immutable_heads()
: Resolves to present(trunk()) | tags() | untracked_remote_bookmarks()
. It is used as the default definition for immutable_heads()
below. it is not recommended to redefined this alias. Prefer to redefine immutable_heads()
instead.
immutable_heads()
: Resolves to present(trunk()) | tags() | untracked_remote_bookmarks()
by default. It is actually defined as builtin_immutable_heads()
, and can be overridden as required. See here for details.
immutable()
: The set of commits that jj
treats as immutable. This is equivalent to ::(immutable_heads() | root())
. It is not recommended to redefine this alias. Note that modifying this will not change whether a commit is immutable. To do that, edit immutable_heads()
.
mutable()
: The set of commits that jj
treats as mutable. This is equivalent to ~immutable()
. It is not recommended to redefined this alias. Note that modifying this will not change whether a commit is immutable. To do that, edit immutable_heads()
.
all:
modifier","text":"Certain commands (such as jj rebase
) can take multiple revset arguments, and each of these may resolve to one-or-many revisions. By default, jj
will not allow revsets that resolve to more than one revision \u2014 a so-called \"large revset\" \u2014 and will ask you to confirm that you want to proceed by prefixing it with the all:
modifier.
If you set the ui.always-allow-large-revsets
option to true
, jj
will behave as though the all:
modifier was used every time it would matter.
An all:
modifier before a revset expression does not otherwise change its meaning. Strictly speaking, it is not part of the revset language. The notation is similar to the modifiers like glob:
allowed before string patterms.
For example, jj rebase -r w -d xyz+
will rebase w
on top of the child of xyz
as long as xyz
has exactly one child.
If xyz
has more than one child, the all:
modifier is not specified, and ui.always-allow-large-revsets
is false
(the default), jj rebase -r w -d xyz+
will return an error.
If ui.always-allow-large-revsets
was true
, the above command would act as if all:
was set (see the next paragraph).
With the all:
modifier, jj rebase -r w -d all:xyz+
will make w
into a merge commit if xyz
has more than one child. The all:
modifier confirms that the user expected xyz
to have more than one child.
A more useful example: if w
is a merge commit, jj rebase -s w -d all:w- -d xyz
will add xyz
to the list of w
's parents.
Show the parent(s) of the working-copy commit (like git log -1 HEAD
):
jj log -r @-\n
Show all ancestors of the working copy (like plain git log
)
jj log -r ::@\n
Show commits not on any remote bookmark:
jj log -r 'remote_bookmarks()..'\n
Show commits not on origin
(if you have other remotes like fork
):
jj log -r 'remote_bookmarks(remote=origin)..'\n
Show the initial commits in the repo (the ones Git calls \"root commits\"):
jj log -r 'root()+'\n
Show some important commits (like git --simplify-by-decoration
):
jj log -r 'tags() | bookmarks()'\n
Show local commits leading up to the working copy, as well as descendants of those commits:
jj log -r '(remote_bookmarks()..@)::'\n
Show commits authored by \"martinvonz\" and containing the word \"reset\" in the description:
jj log -r 'author(martinvonz) & description(reset)'\n
"},{"location":"roadmap/","title":"Roadmap","text":"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.
"},{"location":"roadmap/#support-for-copies-and-renames","title":"Support for copies and renames","text":"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.
"},{"location":"roadmap/#forge-integrations","title":"Forge integrations","text":"We would like to make it easier to work with various popular forges by providing something like jj github submit
, jj gitlab submit
, and jj gerrit send
. For popular forges, we might include that support by default in the standard jj
binary.
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.
"},{"location":"roadmap/#better-rust-api-for-uis","title":"Better Rust API for UIs","text":"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
.
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.
"},{"location":"roadmap/#open-source-cloud-based-repos-server-and-daemon-process","title":"Open-source cloud-based repos (server and daemon process)","text":"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 of objects it thinks the client might ask for next. In 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.
"},{"location":"roadmap/#virtual-file-system-vfs","title":"Virtual file system (VFS)","text":"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.
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.
"},{"location":"sapling-comparison/","title":"Comparison with Sapling","text":""},{"location":"sapling-comparison/#introduction","title":"Introduction","text":"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:
split
commands, and automatically rebasing descendant commits when you amend a commit.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:
sl shelve
.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:
Undo: jj's undo is powered by the operation log, which records how the repo has changed over time. Sapling has a similar feature with its MetaLog. They seem to provide similar functionality, but jj also exposes the log to the user via jj op log
, so you can tell how far back you want to go back. Sapling has sl debugmetalog
, but that seems to show the history of a single commit, not the whole repo's history. Thanks to jj snapshotting the working copy, it's possible to undo changes to the working copy. For example, if you jj undo
a jj commit
, jj diff
will show the same changes as before jj commit
, but if you sl undo
a sl commit
, the working copy will be clean.
jj
and git
interchangeably in the same repo.blame/annotate
or bisect
commands, and also no copy/rename support. Sapling also has very nice web UI called Interactive Smartlog, which lets you drag and drop commits to rebase them, among other things.sl pr submit --stack
, which lets you push a stack of commits as separate GitHub PRs, including setting the base branch. It only supports GitHub. jj doesn't have any direct integration with GitHub or any other forge. However, it has jj git push --change
for automatically creating branches for specified commits. You have to specify each commit you want to create a branch for by using jj git push --change X --change Y ...
, and you have to manually set up any base branches in GitHub's UI (or GitLab's or ...). On subsequent pushes, you can update all at once by specifying something like jj git push -r main..@
(to push all branches on the current stack of commits from where it forked from main
).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 represent objects of different types; the types are described in a follow-up section. In addition to context-specific keywords, the top-level object can be referenced as self
.
In jj log
/jj evolog
templates, all 0-argument methods of the Commit
type are available as keywords. For example, commit_id
is equivalent to self.commit_id()
.
In jj op log
templates, all 0-argument methods of the Operation
type are available as keywords. For example, current_operation
is equivalent to self.current_operation()
.
The following operators are supported.
x.f()
: Method call.-x
: Negate integer value.!x
: Logical not.x == y
: Logical equal. Operands must be either Boolean
, Integer
, or String
.x && y
: Logical and, short-circuiting.x || y
: Logical or, short-circuiting.x ++ y
: Concatenate x
and y
templates.(listed in order of binding strengths)
"},{"location":"templates/#global-functions","title":"Global functions","text":"The following functions are defined.
fill(width: Integer, content: Template) -> Template
: Fill lines at the given width
.indent(prefix: Template, content: Template) -> Template
: Indent non-empty lines by the given prefix
.pad_start(width: Integer, content: Template[, fill_char: Template])
: Pad (or right-justify) content by adding leading fill characters. The content
shouldn't have newline character.pad_end(width: Integer, content: Template[, fill_char: Template])
: Pad (or left-justify) content by adding trailing fill characters. The content
shouldn't have newline character.truncate_start(width: Integer, content: Template)
: Truncate content
by removing leading characters. The content
shouldn't have newline character.truncate_end(width: Integer, content: Template)
: Truncate content
by removing trailing characters. The content
shouldn't have newline character.label(label: Template, content: Template) -> Template
: Apply label to the content. The label
is evaluated as a space-separated string.raw_escape_sequence(content: Template) -> Template
: Preserves any escape sequences in content
(i.e., bypasses sanitization) and strips labels. Note: This function is intended for escape sequences and as such, its output is expected to be invisible / of no display width. Outputting content with nonzero display width may break wrapping, indentation etc.if(condition: Boolean, then: Template[, else: Template]) -> Template
: Conditionally evaluate then
/else
template content.coalesce(content: Template...) -> Template
: Returns the first non-empty content.concat(content: Template...) -> Template
: Same as content_1 ++ ... ++ content_n
.separate(separator: Template, content: Template...) -> Template
: Insert separator between non-empty contents.surround(prefix: Template, suffix: Template, content: Template) -> Template
: Surround non-empty content with texts such as parentheses.No methods are defined. Can be constructed with false
or true
literal.
This type cannot be printed. The following methods are defined.
description() -> String
change_id() -> ChangeId
commit_id() -> CommitId
parents() -> List<Commit>
author() -> Signature
committer() -> Signature
mine() -> Boolean
: Commits where the author's email matches the email of the current user.working_copies() -> String
: For multi-workspace repository, indicate working-copy commit as <workspace name>@
.current_working_copy() -> Boolean
: True for the working-copy commit of the current workspace.bookmarks() -> List<RefName>
: Local and remote bookmarks pointing to the commit. A tracking remote bookmark will be included only if its target is different from the local one.local_bookmarks() -> List<RefName>
: All local bookmarks pointing to the commit.remote_bookmarks() -> List<RefName>
: All remote bookmarks pointing to the commit.tags() -> List<RefName>
git_refs() -> List<RefName>
git_head() -> Boolean
: True for the Git HEAD
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).immutable() -> Boolean
: True if the commit is included in the set of immutable commits.contained_in(revset: String) -> 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: String]) -> TreeDiff
: Changes from the parents within the files
expression. All files are compared by default, but it is likely to change in future version to respect the command line path arguments.root() -> Boolean
: True if the commit is the root commit.The following methods are defined.
.normal_hex() -> String
: Normal hex representation (0-9a-f), useful for ChangeId, whose canonical hex representation is \"reversed\" (z-k)..short([len: Integer]) -> String
.shortest([min_len: Integer]) -> ShortestIdPrefix
: Shortest unique prefix.No methods are defined.
"},{"location":"templates/#list-type","title":"List type","text":"A list can be implicitly converted to Boolean
. The following methods are defined.
.len() -> Integer
: Number of elements in the list..join(separator: Template) -> Template
: Concatenate elements with the given separator
..map(|item| expression) -> ListTemplate
: Apply template expression
to each element. Example: parents.map(|c| c.commit_id().short())
The following methods are defined. See also the List
type.
.join(separator: Template) -> Template
This type cannot be printed. The following methods are defined.
current_operation() -> Boolean
description() -> String
id() -> OperationId
tags() -> String
time() -> TimestampRange
user() -> String
snapshot() -> Boolean
: True if the operation is a snapshot operation.root() -> Boolean
: True if the operation is the root operation.The following methods are defined.
.short([len: Integer]) -> String
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.
The following methods are defined.
.name() -> String
: Local bookmark or tag name..remote() -> String
: Remote name or empty if this is a local 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.The following methods are defined.
.prefix() -> String
.rest() -> String
.upper() -> ShortestIdPrefix
.lower() -> ShortestIdPrefix
The following methods are defined.
.name() -> String
.email() -> String
.username() -> String
.timestamp() -> Timestamp
This type cannot be printed. The following methods are defined.
.lower() -> Integer
: Lower bound..upper() -> Option<Integer>
: Upper bound if known..exact() -> Option<Integer>
: Exact value if upper bound is known and it equals to the lower bound..zero() -> Boolean
: True if upper bound is known and is 0
.A string can be implicitly converted to Boolean
. The following methods are defined.
.len() -> Integer
: Length in UTF-8 bytes..contains(needle: Template) -> Boolean
.first_line() -> String
.lines() -> List<String>
: Split into lines excluding newline characters..upper() -> String
.lower() -> String
.starts_with(needle: Template) -> Boolean
.ends_with(needle: Template) -> Boolean
.remove_prefix(needle: Template) -> String
: Removes the passed prefix, if present.remove_suffix(needle: Template) -> String
: Removes the passed suffix, if present.substr(start: Integer, end: Integer) -> String
: Extract substring. The start
/end
indices should be specified in UTF-8 bytes. Negative values count from the end of the string.String literals must be surrounded by single or double quotes ('
or \"
). A double-quoted string literal supports the following escape sequences:
\\\"
: double quote\\\\
: backslash\\t
: horizontal tab\\r
: carriage return\\n
: new line\\0
: null\\e
: escape (i.e., \\x1b
)\\xHH
: byte with hex value HH
Other escape sequences are not supported. Any UTF-8 characters are allowed inside a string literal, with two exceptions: unescaped \"
-s and uses of \\
that don't form a valid escape sequence.
A single-quoted string literal has no escape syntax. '
can't be expressed inside a single-quoted string literal.
Most types can be implicitly converted to Template
. No methods are defined.
The following methods are defined.
.ago() -> String
: Format as relative timestamp..format(format: String) -> 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: String) -> Boolean
: True if the timestamp is exactly at or after the given date..before(date: String) -> Boolean
: True if the timestamp is before, but not including, the given date.The following methods are defined.
.start() -> Timestamp
.end() -> Timestamp
.duration() -> String
This type cannot be printed. The following methods are defined.
.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) -> Template
: Format as a histogram of the changes..summary() -> Template
: Format as a list of status code and path pairs.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 cli/src/config/templates.toml
file in jj's source tree.
New keywords and functions 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 function will be shadowed by name, and can't co-exist with aliases.
For example:
[template-aliases]\n'commit_change_ids' = '''\nconcat(\n format_field(\"Commit ID\", commit_id),\n format_field(\"Change ID\", commit_id),\n)\n'''\n'format_field(key, value)' = 'key ++ \": \" ++ value ++ \"\\n\"'\n
"},{"location":"templates/#examples","title":"Examples","text":"Get short commit IDs of the working-copy parents:
jj log --no-graph -r @ -T 'parents.map(|c| c.commit_id().short()).join(\",\")'\n
Show machine-readable list of full commit and change IDs:
jj log --no-graph -T 'commit_id ++ \" \" ++ change_id ++ \"\\n\"'\n
"},{"location":"testimonials/","title":"Testimonials","text":"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 \u2014 from our loving users, our silly developers \u2014 all to tip the scales and get you on our side!
"},{"location":"testimonials/#what-the-users-have-to-say","title":"What the users have to say","text":"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 \u2014 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.
\u2014 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
\u2014 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.
\u2014 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\".
\u2014 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
\u2014 jyn514, Rust contributor
Jujutsu is pretty cool, you can even keep most of your existing workflows
\u2014 Ben, who doesn't want you keeping your existing workflow
Wait, it's not called Jujitsu?
\u2014 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.
\u2014 gul banana, computer programmer
"},{"location":"testimonials/#what-the-developers-have-to-say","title":"What the developers have to say","text":"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 \u2014 developed over years of experience \u2014 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.
\u2014 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.
\u2014 Anton Bulakh, contributor and dopamine enthusiast
I'm sometimes still surprised that navigating with jj next
and jj prev
works.
\u2014 Philip Metzger, author of jj next
and jj prev
I'm surprised when it works.
\u2014 Martin von Zweigbergk, project creator and leader
"},{"location":"testimonials/#spread-the-word-yourself","title":"Spread the word yourself","text":"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.
"},{"location":"tutorial/","title":"Tutorial","text":"Hint: This tutorial has become somewhat out of date. Many people find the alternative (not quite finished) tutorial by Steve Klabnik helpful.
This text assumes that the reader is familiar with Git.
"},{"location":"tutorial/#preparation","title":"Preparation","text":"If you haven't already, make sure you install and configure Jujutsu.
"},{"location":"tutorial/#cloning-a-git-repo","title":"Cloning a Git repo","text":"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\n# repos yet)\n$ jj git clone https://github.com/octocat/Hello-World\nFetching into new repo in \"/tmp/tmp.O1DWMiaKd4/Hello-World\"\nWorking copy now at: kntqzsqt d7439b06 (empty) (no description set)\nParent commit : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1\nAdded 1 files, modified 0 files, removed 0 files\n$ cd Hello-World\n
Running jj st
(short for jj status
) now yields something like this:
$ jj st\nThe working copy is clean\nWorking copy : kntqzsqt d7439b06 (empty) (no description set)\nParent commit: orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1\n
We can see from the output above that our working copy is a real commit with a commit ID (d7439b06
in the example). When you make a change in the working copy, the working-copy commit gets automatically amended by the next jj
command.
Now let's say we want to edit the README
file in the repo to say \"Goodbye\" instead of \"Hello\". Let's start by describing the change (adding a commit message) so we don't forget what we're working on:
# This will bring up $EDITOR (or `pico` or `Notepad` by default). Enter\n# something like \"Say goodbye\" in the editor and then save the file and close\n# the editor.\n$ jj describe\nWorking copy now at: kntqzsqt e427edcf (empty) Say goodbye\nParent commit : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1\n
Now make the change in the README:
# Adjust as necessary for compatibility with your flavor of `sed`\n$ sed -i 's/Hello/Goodbye/' README\n$ jj st\nWorking copy changes:\nM README\nWorking copy : kntqzsqt 5d39e19d Say goodbye\nParent commit: orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1\n
Note that you didn't have to tell Jujutsu to add the change like you would with git add
. You actually don't even need to tell it when you add new files or remove existing files. To untrack a path, add it to your .gitignore
and run jj file untrack <path>
.
To see the diff, run jj diff
:
$ jj diff --git # Feel free to skip the `--git` flag\ndiff --git a/README b/README\nindex 980a0d5f19...1ce3f81130 100644\n--- a/README\n+++ b/README\n@@ -1,1 +1,1 @@\n-Hello World!\n+Goodbye World!\n
Jujutsu's diff format currently defaults to inline coloring of the diff (like git diff --color-words
), so we used --git
above to make the diff readable in this tutorial.
As you may have noticed, the working-copy commit's ID changed both when we edited the description and when we edited the README. However, the parent commit stayed the same. Each change to the working-copy commit amends the previous version. So how do we tell Jujutsu that we are done amending the current change and want to start working on a new one? That is what jj new
is for. That will create a new commit on top of your current working-copy commit. The new commit is for the working-copy changes.
So, let's say we're now done with this change, so we create a new change:
$ jj new\nWorking copy now at: mpqrykyp aef4df99 (empty) (no description set)\nParent commit : kntqzsqt 5d39e19d Say goodbye\n$ jj st\nThe working copy is clean\nWorking copy : mpqrykyp aef4df99 (empty) (no description set)\nParent commit: kntqzsqt 5d39e19d Say goodbye\n
If we later realize that we want to make further changes, we can make them in the working copy and then run jj squash
. That command squashes 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
, and jj amend
is in fact an alias for jj squash
.
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.
You're probably familiar with git log
. Jujutsu has very similar functionality in its jj log
command:
$ jj log\n@ mpqrykyp martinvonz@google.com 2023-02-12 15:00:22.000 -08:00 aef4df99\n\u2502 (empty) (no description set)\n\u25c9 kntqzsqt martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19d\n\u2502 Say goodbye\n\u2502 \u25c9 tpstlust support+octocat@github.com 2018-05-10 12:55:19.000 -05:00 octocat-patch-1@origin b1b3f972\n\u251c\u2500\u256f sentence case\n\u2502 \u25c9 kowxouwz octocat@nowhere.com 2014-06-10 15:22:26.000 -07:00 test@origin b3cbd5bb\n\u251c\u2500\u256f Create CONTRIBUTING.md\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2502 (empty) Merge pull request #6 from Spaceghost/patch-1\n~\n
The @
indicates the working-copy commit. The first ID on a line (e.g. \"mpqrykyp\" above) is the \"change ID\", which is an ID that follows the commit as it's rewritten (similar to Gerrit's Change-Id). The second ID is the commit ID, which changes when you rewrite the commit. 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()'\n@ mpqrykyp martinvonz@google.com 2023-02-12 15:00:22.000 -08:00 aef4df99\n\u2577 (empty) (no description set)\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2577 (empty) Merge pull request #6 from Spaceghost/patch-1\n\u25c9 zzzzzzzz root() 00000000\n
The 00000000
commit (change ID zzzzzzzz
) is a virtual commit that's called the \"root commit\". It's the root commit of every repo. The root()
function in the revset matches it.
There are also operators for getting the parents (foo-
), children (foo+
), ancestors (::foo
), descendants (foo::
), DAG range (foo::bar
, like git log --ancestry-path
), range (foo..bar
, same as Git's). See the revset documentation for all revset operators and functions.
Hint: If the default jj log
omits some commits you expect to see, you can always run jj log -r ::
(or, equivalently, jj log -r 'all()'
) to see all the commits.
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\n$ jj new master -m A; echo a > file1\nWorking copy now at: nuvyytnq 00a2aeed (empty) A\nParent commit : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1\nAdded 0 files, modified 1 files, removed 0 files\n$ jj new -m B1; echo b1 > file1\nWorking copy now at: ovknlmro 967d9f9f (empty) B1\nParent commit : nuvyytnq 5dda2f09 A\n$ jj new -m B2; echo b2 > file1\nWorking copy now at: puqltutt 8ebeaffa (empty) B2\nParent commit : ovknlmro 7d7c6e6b B1\n$ jj new -m C; echo c > file2\nWorking copy now at: qzvqqupx 62a3c6d3 (empty) C\nParent commit : puqltutt daa6ffd5 B2\n$ jj log\n@ qzvqqupx martinvonz@google.com 2023-02-12 15:07:41.946 -08:00 2370ddf3\n\u2502 C\n\u25c9 puqltutt martinvonz@google.com 2023-02-12 15:07:33.000 -08:00 daa6ffd5\n\u2502 B2\n\u25c9 ovknlmro martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6b\n\u2502 B1\n\u25c9 nuvyytnq martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f09\n\u2502 A\n\u2502 \u25c9 kntqzsqt martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19d\n\u251c\u2500\u256f Say goodbye\n\u2502 \u25c9 tpstlust support+octocat@github.com 2018-05-10 12:55:19.000 -05:00 octocat-patch-1@origin b1b3f972\n\u251c\u2500\u256f sentence case\n\u2502 \u25c9 kowxouwz octocat@nowhere.com 2014-06-10 15:22:26.000 -07:00 test@origin b3cbd5bb\n\u251c\u2500\u256f Create CONTRIBUTING.md\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2502 (empty) Merge pull request #6 from Spaceghost/patch-1\n~\n
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 --destination
/-d
option on A.
$ jj rebase -s puqltutt -d nuvyytnq # Replace the IDs by what you have for B2 and A\nRebased 2 commits\nNew conflicts appeared in these commits:\n qzvqqupx 1978b534 (conflict) C\n puqltutt f7fb5943 (conflict) B2\nTo resolve the conflicts, start by updating to the first one:\n jj new puqltuttzvly\nThen use `jj resolve`, or edit the conflict markers in the file directly.\nOnce the conflicts are resolved, you may want to inspect the result with `jj diff`.\nThen run `jj squash` to move the resolution into the conflicted commit.\nWorking copy now at: qzvqqupx 1978b534 (conflict) C\nParent commit : puqltutt f7fb5943 (conflict) B2\nAdded 0 files, modified 1 files, removed 0 files\n$ jj log\n@ qzvqqupx martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 1978b534 conflict\n\u2502 C\n\u25c9 puqltutt martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 f7fb5943 conflict\n\u2502 B2\n\u2502 \u25c9 ovknlmro martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6b\n\u251c\u2500\u256f B1\n\u25c9 nuvyytnq martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f09\n\u2502 A\n\u2502 \u25c9 kntqzsqt martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19d\n\u251c\u2500\u256f Say goodbye\n\u2502 \u25c9 tpstlust support+octocat@github.com 2018-05-10 12:55:19.000 -05:00 octocat-patch-1@origin b1b3f972\n\u251c\u2500\u256f sentence case\n\u2502 \u25c9 kowxouwz octocat@nowhere.com 2014-06-10 15:22:26.000 -07:00 test@origin b3cbd5bb\n\u251c\u2500\u256f Create CONTRIBUTING.md\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2502 (empty) Merge pull request #6 from Spaceghost/patch-1\n~\n
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:
$ jj new puqltutt # Replace the ID by what you have for B2\nWorking copy now at: zxoosnnp c7068d1c (conflict) (empty) (no description set)\nParent commit : puqltutt f7fb5943 (conflict) B2\nAdded 0 files, modified 0 files, removed 1 files\n$ jj st\nThe working copy is clean\nThere are unresolved conflicts at these paths:\nfile1 2-sided conflict\nWorking copy : zxoosnnp c7068d1c (conflict) (empty) (no description set)\nParent commit: puqltutt f7fb5943 (conflict) B2\n$ cat file1\n<<<<<<<\n%%%%%%%\n-b1\n+a\n+++++++\nb2\n>>>>>>>\n$ echo resolved > file1\n$ jj squash\nRebased 1 descendant commits\nExisting conflicts were resolved or abandoned from these commits:\n qzvqqupx hidden 1978b534 (conflict) C\n puqltutt hidden f7fb5943 (conflict) B2\nWorking copy now at: ntxxqymr e3c279cc (empty) (no description set)\nParent commit : puqltutt 2c7a658e B2\n$ jj log\n@ ntxxqymr martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 e3c279cc\n\u2502 (empty) (no description set)\n\u2502 \u25c9 qzvqqupx martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 b9da9d28\n\u251c\u2500\u256f C\n\u25c9 puqltutt martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 2c7a658e\n\u2502 B2\n\u2502 \u25c9 ovknlmro martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6b\n\u251c\u2500\u256f B1\n\u25c9 nuvyytnq martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f09\n\u2502 A\n\u2502 \u25c9 kntqzsqt martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19d\n\u251c\u2500\u256f Say goodbye\n\u2502 \u25c9 tpstlust support+octocat@github.com 2018-05-10 12:55:19.000 -05:00 octocat-patch-1@origin b1b3f972\n\u251c\u2500\u256f sentence case\n\u2502 \u25c9 kowxouwz octocat@nowhere.com 2014-06-10 15:22:26.000 -07:00 test@origin b3cbd5bb\n\u251c\u2500\u256f Create CONTRIBUTING.md\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2502 (empty) Merge pull request #6 from Spaceghost/patch-1\n~\n
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.
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\n@ d3b77addea49 martinvonz@vonz.svl.corp.google.com 3 minutes ago, lasted 3 milliseconds\n\u2502 squash commit 63874fe6c4fba405ffc38b0dd926f03b715cf7ef\n\u2502 args: jj squash\n\u25c9 6fc1873c1180 martinvonz@vonz.svl.corp.google.com 3 minutes ago, lasted 1 milliseconds\n\u2502 snapshot working copy\n\u2502 args: jj squash\n\u25c9 ed91f7bcc1fb martinvonz@vonz.svl.corp.google.com 6 minutes ago, lasted 1 milliseconds\n\u2502 new empty commit\n\u2502 args: jj new puqltutt\n\u25c9 367400773f87 martinvonz@vonz.svl.corp.google.com 12 minutes ago, lasted 3 milliseconds\n\u2502 rebase commit daa6ffd5a09a8a7d09a65796194e69b7ed0a566d and descendants\n\u2502 args: jj rebase -s puqltutt -d nuvyytnq\n[many more lines]\n
The most useful command is jj undo
(alias for jj op undo
), which will undo an operation. By default, it will undo the most recent operation. Let's try it:
$ jj undo\nNew conflicts appeared in these commits:\n qzvqqupx 1978b534 (conflict) C\n puqltutt f7fb5943 (conflict) B2\nTo resolve the conflicts, start by updating to the first one:\n jj new puqltuttzvly\nThen use `jj resolve`, or edit the conflict markers in the file directly.\nOnce the conflicts are resolved, you may want to inspect the result with `jj diff`.\nThen run `jj squash` to move the resolution into the conflicted commit.\nWorking copy now at: zxoosnnp 63874fe6 (no description set)\nParent commit : puqltutt f7fb5943 (conflict) B2\n$ jj log\n@ zxoosnnp martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 63874fe6\n\u2502 (no description set)\n\u2502 \u25c9 qzvqqupx martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 1978b534 conflict\n\u251c\u2500\u256f C\n\u25c9 puqltutt martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 f7fb5943 conflict\n\u2502 B2\n\u2502 \u25c9 ovknlmro martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6b\n\u251c\u2500\u256f B1\n\u25c9 nuvyytnq martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f09\n\u2502 A\n\u2502 \u25c9 kntqzsqt martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19d\n\u251c\u2500\u256f Say goodbye\n\u2502 \u25c9 tpstlust support+octocat@github.com 2018-05-10 12:55:19.000 -05:00 octocat-patch-1@origin b1b3f972\n\u251c\u2500\u256f sentence case\n\u2502 \u25c9 kowxouwz octocat@nowhere.com 2014-06-10 15:22:26.000 -07:00 test@origin b3cbd5bb\n\u251c\u2500\u256f Create CONTRIBUTING.md\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2502 (empty) Merge pull request #6 from Spaceghost/patch-1\n~\n
As you can perhaps see, that undid the jj squash
invocation we used for squashing the conflict resolution into commit B2 earlier. Notice that it also updated the working copy.
You can also view the repo the way it looked after some earlier operation. For example, if you want to see jj log
output right after the jj rebase
operation, try jj log --at-op=367400773f87
but use the hash from your own jj op log
.
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\nWorking copy now at: ztqrpvnw f94e49cf (empty) abc\nParent commit : orrkosyo 7fd1a60b master | (empty) Merge pull request #6 from Spaceghost/patch-1\nAdded 0 files, modified 0 files, removed 1 files\n$ jj new -m ABC; printf 'A\\nB\\nc\\n' > file\nWorking copy now at: kwtuwqnm 6f30cd1f (empty) ABC\nParent commit : ztqrpvnw 51002261 ab\n$ jj new -m ABCD; printf 'A\\nB\\nC\\nD\\n' > file\nWorking copy now at: mrxqplyk a6749154 (empty) ABCD\nParent commit : kwtuwqnm 30aecc08 ABC\n$ jj log -r master::@\n@ mrxqplyk martinvonz@google.com 2023-02-12 19:38:21.000 -08:00 b98c607b\n\u2502 ABCD\n\u25c9 kwtuwqnm martinvonz@google.com 2023-02-12 19:38:12.000 -08:00 30aecc08\n\u2502 ABC\n\u25c9 ztqrpvnw martinvonz@google.com 2023-02-12 19:38:03.000 -08:00 51002261\n\u2502 abc\n\u25c9 orrkosyo octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b\n\u2502 (empty) Merge pull request #6 from Spaceghost/patch-1\n~\n
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\nUsing default editor ':builtin'; you can change this by setting ui.diff-editor\nWorking copy now at: mrxqplyk 52a6c7fd ABCD\nParent commit : kwtuwqnm 643061ac ABC\n
That will bring up the built-in diff editor1 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).
If we look at the diff of the second commit, we now see that all three lines got capitalized:
$ jj diff -r @-\nModified regular file file:\n 1 1: aA\n 2 2: bB\n 3 3: cC\n
The child change (\"ABCD\" in our case) will have the same content state after the jj squash
command. That means that you can move any changes you want into the parent change, even if they touch the same word, and it won't cause any conflicts.
Let's try one final command for changing the contents of an exiting commit. That command is jj diffedit
, which lets you edit the contents of a commit without checking it out.
$ jj diffedit -r @-\nUsing default editor ':builtin'; you can change this by setting ui.diff-editor\nCreated kwtuwqnm 70985eaa (empty) ABC\nRebased 1 descendant commits\nNew conflicts appeared in these commits:\n mrxqplyk 1c72cd50 (conflict) ABCD\nTo resolve the conflicts, start by updating to it:\n jj new mrxqplykmyqv\nThen use `jj resolve`, or edit the conflict markers in the file directly.\nOnce the conflicts are resolved, you may want to inspect the result with `jj diff`.\nThen run `jj squash` to move the resolution into the conflicted commit.\nWorking copy now at: mrxqplyk 1c72cd50 (conflict) ABCD\nParent commit : kwtuwqnm 70985eaa (empty) ABC\nAdded 0 files, modified 1 files, removed 0 files\n
In the diff editor, edit the right side by e.g. adding something to the first line. 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 addition to the first 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).
There are many other diff editors you could use. For example, if you have Meld installed and in the PATH, you can use it via jj squash -i --tool meld
or a fancier config with jj squash -i --tool meld-3
. You can configure the default with the ui.diff-editor
option; those docs also explain how to specify a path to an executable if it is not in the PATH.\u00a0\u21a9
Jujutsu works the same on all platforms, but there are some caveats that Windows users should be aware of.
"},{"location":"windows/#line-endings-are-not-converted","title":"Line endings are not converted","text":"Jujutsu does not currently honor .gitattributes
and does not have a setting like Git's core.autocrlf
. This means that line endings will be checked out exactly as they are committed and committed exactly as authored. This is true on all platforms, but Windows users are most likely to miss CRLF conversion.
If your Git repository expects Windows users to have core.autocrlf
set to true
, then the files are committed with LF line endings but are checked out with CRLF line endings. Jujutsu doesn't understand this and will convert the committed line endings to CRLF.
After creating a colocated repository on Windows, you most likely want to set core.autocrlf
to input
, then jj abandon
to convert all files on disk to LF line endings:
PS> git config core.autocrlf input\n\n# Abandoning the working copy will cause Jujutsu to overwrite all files with\n# CRLF line endings with the line endings they are committed with, probably LF\nPS> jj abandon\n
This setting ensures 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.
"},{"location":"windows/#pagination","title":"Pagination","text":"On Windows, jj
will use its integrated pager called minus
by default, unless the environment variable %PAGER%
or 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\"]'\nPS> jj config set --user ui.paginate auto\n
"},{"location":"windows/#typing-in-powershell","title":"Typing @
in PowerShell","text":"PowerShell uses @
as part the array sub-expression operator, so it often needs to be escaped or quoted in commands:
PS> jj log -r `@\nPS> jj log -r '@'\n
One solution is to create a revset alias. For example, to make HEAD
an alias for @
:
PS> jj config set --user revset-aliases.HEAD '@'\nPS> jj log -r HEAD\n
"},{"location":"windows/#wsl-sets-the-execute-bit-on-all-files","title":"WSL sets the execute bit on all files","text":"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-repo\n
Then only use the ~/my-repo
workspace from Linux.
jj
supports symlinks on Windows only when they are enabled by the operating system. This requires Windows 10 version 14972 or higher, as well as Developer Mode. If those conditions are not satisfied, jj
will materialize symlinks as ordinary files.
For colocated repositories, Git support must also be enabled using the git config
option core.symlinks=true
.
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.
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.
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/martinvonz/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.
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 $HOME/.gitignore
and $GIT_DIR/info/exclude
.
Ignored files are never tracked automatically (regardless of the value of snapshot.auto-track
), but they can still end up being tracked for a few reasons:
jj file track
commandYou can untrack such files with the jj file untrack command.
"},{"location":"working-copy/#workspaces","title":"Workspaces","text":"You can have multiple working copies backed by a single repo. Use jj workspace add
to create a new working copy. The working copy will have a .jj/
directory linked to the main repo. The working copy and the .jj/
directory together is called a \"workspace\". Each workspace can have a different commit checked out.
Having multiple workspaces can be useful for running long-running tests in a one while you continue developing in another, for example. If needed, jj workspace root
prints the root path of the current workspace.
When you're done using a workspace, use jj workspace forget
to make the repo forget about it. The files can be deleted from disk separately (either before or after).
Almost all commands go through three main steps:
@
should point toIf 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.
Authors: Daniel Ploch
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.
"},{"location":"design/copy-tracking/#objective","title":"Objective","text":"Implement extensible APIs for recording and retrieving copy info for the purposes of diffing and rebasing across renames and copies more accurately. This should be performant both for Git, which synthesizes copy info on the fly between arbitrary trees, and for custom extensions 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.
"},{"location":"design/copy-tracking/#interface-design","title":"Interface Design","text":""},{"location":"design/copy-tracking/#read-api","title":"Read API","text":"Copy information will be served both by a new Backend trait method described below, as well as a new field on Commit objects for backends that support copy tracking:
/// An individual copy source.\npub struct CopySource {\n /// The source path a target was copied from.\n ///\n /// It is not required that the source path is different than the target\n /// path. A custom backend may choose to represent 'rollbacks' as copies\n /// from a file unto itself, from a specific prior commit.\n path: RepoPathBuf,\n file: FileId,\n /// The source commit the target was copied from. If not specified, then the\n /// parent of the target commit is the source commit. Backends may use this\n /// field to implement 'integration' logic, where a source may be\n /// periodically merged into a target, similar to a branch, but the\n /// branching occurs at the file level rather than the repository level. It\n /// also follows naturally that any copy source targeted to a specific\n /// commit should avoid copy propagation on rebasing, which is desirable\n /// for 'fork' style copies.\n ///\n /// If specified, it is required that the commit id is an ancestor of the\n /// commit with which this copy source is associated.\n commit: Option<CommitId>,\n}\n\npub enum CopySources {\n Resolved(CopySource),\n Conflict(HashSet<CopySource>),\n}\n\n/// An individual copy event, from file A -> B.\npub struct CopyRecord {\n /// The destination of the copy, B.\n target: RepoPathBuf,\n /// The CommitId where the copy took place.\n id: CommitId,\n /// The source of the copy, A.\n sources: CopySources,\n}\n\n/// Backend options for fetching copy records.\npub struct CopyRecordOpts {\n // TODO: Probably something for git similarity detection\n}\n\npub type CopyRecordStream = BoxStream<BackendResult<CopyRecord>>;\n\npub trait Backend {\n /// Get all copy records for `paths` in the dag range `roots..heads`.\n ///\n /// The exact order these are returned is unspecified, but it is guaranteed\n /// to be reverse-topological. That is, for any two copy records with\n /// different commit ids A and B, if A is an ancestor of B, A is streamed\n /// after B.\n ///\n /// Streaming by design to better support large backends which may have very\n /// large single-file histories. This also allows more iterative algorithms\n /// like blame/annotate to short-circuit after a point without wasting\n /// unnecessary resources.\n async fn get_copy_records(&self, paths: &[RepoPathBuf], roots: &[CommitId], heads: &[CommitId]) -> CopyRecordStream;\n}\n
Obtaining copy records for a single commit requires first computing the files list for that commit, then calling get_copy_records with heads = [id]
and roots = parents()
. This enables commands like jj diff
to produce better diffs that take copy sources into account.
Backends that support tracking copy records at the commit level will do so through a new field on backend::Commit
objects:
pub struct Commit {\n ...\n copies: Option<HashMap<RepoPathBuf, CopySources>>,\n}\n\npub trait Backend {\n /// Whether this backend supports storing explicit copy records on write.\n fn supports_copy_tracking(&self) -> bool;\n}\n
This field will be ignored by backends that do not support copy tracking, and always set to None
when read from such backends. Backends that do support copy tracking are required to preserve the field value always.
This API will enable the creation of new jj
commands for recording copies:
jj cp $SRC $DEST [OPTIONS]\njj mv $SRC $DEST [OPTIONS]\n
These commands will rewrite the target commit to reflect the given move/copy instructions in its tree, as well as recording the rewrites on the Commit object itself for backends that support it (for backends that do not, these copy records will be silently discarded).
Flags for the first two commands will include:
-r/--revision\n perform the copy or move at the specified revision\n defaults to the working copy commit if unspecified\n-f\n force overwrite the destination path\n--after\n record the copy retroactively, without modifying the targeted commit tree\n--resolve\n overwrite all previous copy intents for this $DEST\n--allow-ignore-copy\n don't error if the backend doesn't support copy tracking\n--from REV\n specify a commit id for the copy source that isn't the parent commit\n
For backends which do not support copy tracking, it will be an error to use --after
, since this has no effect on anything and the user should know that. The supports_copy_tracking()
trait method is used to determine this.
An additional command is provided to deliberately discard copy info for a destination path, possibly as a means of resolving a conflict.
jj forget-cp $DEST [-r REV]\n
"},{"location":"design/copy-tracking/#behavioral-changes","title":"Behavioral Changes","text":""},{"location":"design/copy-tracking/#rebase-changes","title":"Rebase Changes","text":"In general, we want to support the following use cases:
Using the aforementioned copy tracing API, both of these should be feasible. A detailed approach to a specific edge case is detailed in the next section.
"},{"location":"design/copy-tracking/#rename-of-an-added-file","title":"Rename of an added file","text":"A well known and thorny problem in Mercurial occurs in the following scenario:
In jj, we have an opportunity to fix this because all rebasing occurs atomically and transactionally within memory. The exact implementation of this is yet to be determined, but conceptually the following should produce desirable results:
With copy-tracking, a whole new class of conflicts become possible. These need to be well-defined and have well documented resolution paths. Because copy info in a commit is keyed by destination, conflicts can only occur at the destination of a copy, not at a source (that's called forking).
"},{"location":"design/copy-tracking/#split-conflicts","title":"Split conflicts","text":"Suppose we create commit A by renaming file F1 -> F2, then we split A. What happens to the copy info? I argue that this is straightforward:
Things get a little messier if A also modifies F1, and this modification is separated from the copy, but I think this is messy only in an academic sense and the user gets a sane result either way. If they want to separate the modification from the copy while still putting it in an earlier commit, they can express this intent after with jj cp --after --from
.
Suppose we create commit A by renaming file F1 -> F, then we create a sibling commit B by renaming file F2 -> F. What happens when we create a merge commit with parents A and B?
In terms of copy info there is no conflict here, because C does not have copy info and needs none, but resolving the contents of F becomes more complicated. We need to (1) identify the greatest common ancestor of A and B (D) (which we do anyway), and (2) invoke get_copy_records()
on F for each of D::A
and D::B
to identify the 'real' source file id for each parent. If these are the same, then we can use that as the base for a better 3-way merge. Otherwise, we must treat it as an add+add conflict where the base is the empty file id.
It is possible that F1 and F2 both came from a common source file G, but that these copies precede D. In such case, we will not produce as good of a merge resolution as we theoretically could, but (1) this seems extremely niche and unlikely, and (2) we cannot reasonably achieve this without implementing some analogue of Mercurial's linknodes concept, and it would be nice to avoid that additional complexity.
"},{"location":"design/copy-tracking/#squash-conflicts","title":"Squash conflicts","text":"Suppose we create commit A by renaming file F1 -> F, then we create child commit B in which we replace F by renaming F2 -> F. This touches on two issues.
Firstly, if these two commits are squashed together, then we have a destination F with two copy sources, F1 and F2. In this case, we can store a CopySources::Conflict([F1, F2])
as the copy source for F, and treat this commit as 'conflicted' in jj log
. jj status
will need modification to show this conflicted state, and jj resolve
will need some way of handling the conflicted copy sources (possibly printing them in some structured text form, and using the user's merge tool to resolve them). Alternatively, the user can 'resolve directly' by running jj cp --after --resolve
with the desired copy info.
Secondly, who is to say that commit B is 'replacing' F at all? In some version control systems, it is possible to 'integrate' a file X into an existing file Y, by e.g. propagating changes in X since its previous 'integrate' into Y, without erasing Y's prior history in that moment for the purpose of archaeology. With the commit metadata currently defined, it is not possible to distinguish between a 'replacement' operation and an 'integrate' operation.
"},{"location":"design/copy-tracking/#track-replacements-explicitly","title":"Track replacements explicitly","text":"One solution is to add a destructive: bool
field or similar to the CopySource
struct, to explicitly distinguish between these two types of copy records. It then becomes possible to record a non-destructive copy using --after
to recognize that a file F was 'merged into' its destination, which can be useful in handling parallel edits of F that later sync this information.
Alternatively, we can keep copy-tracking simple in jj by taking a stronger stance here and treating all copies-onto-existing-files as 'replacement' operations. This makes integrations with more complex VCSs that do support 'integrate'-style operations trickier, but it is possible that a more generic commit extension system is better suited to such backends.
"},{"location":"design/copy-tracking/#future-changes","title":"Future Changes","text":"An implementation of jj blame
or jj annotate
does not currently exist, but when it does we'll definitely want it to be copy-tracing aware to provide better annotations for users doing archaeology. The Read APIs provided are expected to be sufficient for these use cases.
Git uses rename detection rather than copy tracking, generating copy info on the fly between two arbitrary trees. It does not have any place for explicit copy info that exchanges with other users of the same git repo, so any enhancements jj adds here would be local only and could potentially introduce confusion when collaborating with other users.
"},{"location":"design/copy-tracking/#directory-copiesmoves","title":"Directory copies/moves","text":"All copy/move information will be read and written at the file level. While jj cp|mv
may accept directory paths as a convenience and perform the appropriate tree modification operations, the renames will be recorded at the file level, one for each copied/moved file.
Decide what approach(es) to Git submodule storage we should pursue. The decision will be recorded in ./git-submodules.md.
"},{"location":"design/git-submodule-storage/#use-cases-to-consider","title":"Use cases to consider","text":"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.
"},{"location":"design/git-submodule-storage/#fetching-submodule-commits","title":"Fetching submodule commits","text":"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.
"},{"location":"design/git-submodule-storage/#jj-op-restore-and-operation-log-format","title":"\"jj op restore\" and operation log format","text":"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.
"},{"location":"design/git-submodule-storage/#nested-submodules","title":"Nested submodules","text":"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.
"},{"location":"design/git-submodule-storage/#supporting-future-extensions","title":"Supporting future extensions","text":"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:
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:
"},{"location":"design/git-submodule-storage/#fetching-submodule-commits_1","title":"Fetching submodule commits","text":"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.
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.
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.
"},{"location":"design/git-submodule-storage/#extending-to-colocated-git-repos","title":"Extending to colocated Git repos","text":"Git expects submodules to be in .git/modules
, so it will not understand this storage format. To support colocated Git repos, 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.
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.
"},{"location":"design/git-submodule-storage/#store-git-submodules-as-alternate-git-backends","title":"Store Git submodules as alternate Git backends","text":"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.
"},{"location":"design/git-submodules/","title":"Git submodules","text":"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.
"},{"location":"design/git-submodules/#objective","title":"Objective","text":"This proposal aims to replicate the workflows users are used to with Git submodules, e.g.:
When it is convenient, this proposal will also aim to make submodules easier to use than Git's implementation.
"},{"location":"design/git-submodules/#non-goals","title":"Non-goals","text":"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.
"},{"location":"design/git-submodules/#intro-to-git-submodules","title":"Intro to Git Submodules","text":"Git submodules are a feature of Git that allow a repository (submodule) to be embedded inside another repository (the superproject). Notably, a submodule is a full repository, complete with its own index, object store and ref store. It can be interacted with like any other repository, regardless of the superproject.
In a superproject commit, submodule information is captured in two places:
A gitlink
entry in the commit's tree, where the value of the gitlink
entry is the submodule commit id. This tells Git what to populate in the working tree.
A top level .gitmodules
file. This file is in Git's config syntax and entries take the form submodule.<submodule-name>.*
. These include many settings about the submodules, but most importantly:
submodule<submodule-name>.path
contains the path from the root of the tree to the gitlink
being described.
submodule<submodule-name>.url
contains the url to clone the submodule from.
In the working tree, Git notices the presence of a submodule by the .git
entry (signifying the root of a Git repository working tree). This is either the submodule's actual Git directory (an \"old-form\" submodule), or a .git
file pointing to <superproject-git-directory>/modules/<submodule-name>
. The latter is sometimes called the \"absorbed form\", and is Git's preferred mode of operation.
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 repositories, while colocated repositories will be supported when convenient.
This section should be treated as a set of guidelines, not a strict order of work.
"},{"location":"design/git-submodules/#phase-1-readonly-submodules","title":"Phase 1: Readonly submodules","text":"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.
"},{"location":"design/git-submodules/#outcomes","title":"Outcomes","text":"This allows a user to write new contents to a submodule and its remote.
"},{"location":"design/git-submodules/#outcomes_1","title":"Outcomes","text":"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.
"},{"location":"design/git-submodules/#outcomes_2","title":"Outcomes","text":"I.e. outcomes we would like to see if there were no constraints whatsoever.
TODO
"},{"location":"design/git-submodules/#storing-submodules","title":"Storing submodules","text":"Possible approaches under discussion. See ./git-submodule-storage.md.
"},{"location":"design/git-submodules/#snapshotting-new-submodule-changes","title":"Snapshotting new submodule changes","text":"TODO
"},{"location":"design/git-submodules/#mergingrebasing-with-submodules","title":"Merging/rebasing with submodules","text":"TODO
"},{"location":"design/run/","title":"Introducing JJ run","text":"Authors: Philip Metzger, Martin von Zweigberk, Danny Hooper, Waleed Khan
Initial Version, 10.12.2022 (view full history here)
Summary: This Document documents the design of a new run
command for Jujutsu which will be used to seamlessly integrate with build systems, linters and formatters. This is achieved by running a user-provided command or script across multiple revisions. For more details, read the Use-Cases of jj run.
The goal of this Design Document is to specify the correct behavior of jj run
. The points we decide on here I (Philip Metzger) will try to implement. There exists some prior work in other DVCS:
git test
: part of git-branchless. Similar to this proposal for jj run
.hg run
: Google's internal Mercurial extension. Similar to this proposal for jj run
. Details not available.hg fix
: Google's open source Mercurial extension: source code. A more specialized approach to rewriting file content without full context of the working directory.git rebase -x
: runs commands opportunistically as part of rebase.git bisect run
: run a command to determine which commit introduced a bug.The initial need for some kind of command runner integrated in the VCS, surfaced in a github discussion. In a discussion on discord about the git-hook model, there was consensus about not repeating their mistakes.
For jj run
there is prior art in Mercurial, git branchless and Google's internal Mercurial. Currently git-branchless git test
and hg fix
implement some kind of command runner. The Google internal hg run
works in conjunction with CitC (Clients in the Cloud) which allows it to lazily apply the current command to any affected file. Currently no Jujutsu backend (Git, Native) has a fancy virtual filesystem supporting it, so we can't apply this optimization. We could do the same once we have an implementation of the working copy based on a virtual file system. Until then, we have to run the commands in regular local-disk working copies.
jj test
, jj fix
and jj format
.jj test
, jj format
and jj fix
, we shouldn't mash their use-cases into jj run
.fix
subcommand as it cuts too much design space.Linting and Formatting:
jj run 'pre-commit run' -r $revset
jj run 'cargo clippy' -r $revset
jj run 'cargo +nightly fmt'
Large scale changes across repositories, local and remote:
jj run 'sed /some/test/' -r 'mine() & ~remote_bookmarks(exact:\"origin\")'
jj run '$rewrite-tool' -r '$revset'
Build systems:
jj run 'bazel build //some/target:somewhere'
jj run 'ninja check-lld'
Some of these use-cases should get a specialized command, as this allows further optimization. A command could be jj format
, which runs a list of formatters over a subset of a file in a revision. Another command could be jj fix
, which runs a command like rustfmt --fix
or cargo clippy --fix
over a subset of a file in a revision.
All the work will be done in the .jj/
directory. This allows us to hide all complexity from the users, while preserving the user's current workspace.
We will copy the approach from git-branchless's git test
of creating a temporary working copy for each parallel command. The working copies will be reused between jj run
invocations. They will also be reused within jj run
invocation if there are more commits to run on than there are parallel jobs.
We will leave ignored files in the temporary directory between runs. That enables incremental builds (e.g. by letting cargo reuse its target/
directory). However, it also means that runs potentially become less reproducible. We will provide a flag for removing ignored files from the temporary working copies to address that.
Another problem with leaving ignored files in the temporary directories is that they take up space. That is especially problematic in the case of cargo (the target/
directory often takes up tens of GBs). The same flag for cleaning up ignored files can be used to address that. We may want to also have a flag for cleaning up temporary working copies after running the command.
An early version of the command will directly use Treestate to to manage the temporary working copies. That means that running jj
inside the temporary working copies will not work . We can later extend that to use a full Workspace. To prevent operations in the working copies from impacting the repo, we can use a separate OpHeadsStore for it.
Since the subprocesses will run in temporary working copies, they won't interfere with the user's working copy. The user can therefore continue to work in it while jj run
is running.
We want subprocesses to be able to make changes to the repo by updating their assigned working copy. Let's say the user runs jj run
on just commits A and B, where B's parent is A. Any changes made on top of A would be squashed into A, forming A'. Similarly B' would be formed by squasing it into B. We can then either do a normal rebase of B' onto A', or we can simply update its parent to A'. The former is useful, e.g. when the subprocess only makes a partial update of the tree based on the parent commit. In addition to these two modes, we may want to have an option to ignore any changes made in the subprocess's working copy.
Once we give the subprocess access to a fork of the repo via separate OpHeadsStore, it will be able to create new operations in its fork. If the user runs jj run -r foo
and the subprocess checks out another commit, it's not clear what that should do. We should probably just verify that the working-copy commit's parents are unchanged after the subprocess returns. Any operations created by the subprocess will be ignored.
Like all commands, jj run
will refuse to rewrite public/immutable commits. For private/unpublished revisions, we either amend or reparent the changes, which are available as command options.
It may be useful to execute commands in topological order. For example, commands with costs proportional to incremental changes, like build systems. There may also be other relevant heuristics, but topological order is an easy and effective way to start.
Parallel execution of commands on different commits may choose to schedule commits to still reduce incremental changes in the working copy used by each execution slot/\"thread\". However, running the command on all commits concurrently should be possible if desired.
Executing commands in topological order allows for more meaningful use of any potential features that stop execution \"at the first failure\". For example, when running tests on a chain of commits, it might be useful to proceed in topological/chronological order, and stop on the first failure, because it might imply that the remaining executions will be undesirable because they will also fail.
"},{"location":"design/run/#dealing-with-failure","title":"Dealing with failure","text":"It will be useful to have multiple strategies to deal with failures on a single or multiple revisions. The reason for these strategies is to allow customized conflict handling. These strategies then can be exposed in the ui with a matching option.
Continue: If any subprocess fails, we will continue the work on child revisions. Notify the user on exit about the failed revisions.
Stop: Signal a fatal failure and cancel any scheduled work that has not yet started running, but let any already started subprocess finish. Notify the user about the failed command and display the generated error from the subprocess.
Fatal: Signal a fatal failure and immediately stop processing and kill any running processes. Notify the user that we failed to apply the command to the specific revision.
We will leave any affected commit in its current state, if any subprocess fails. This allows us to provide a better user experience, as leaving revisions in an undesirable state, e.g partially formatted, may confuse users.
"},{"location":"design/run/#resource-constraints","title":"Resource constraints","text":"It will be useful to constrain the execution to prevent resource exhaustion. Relevant resources could include:
jj run
can provide some simple mitigations like limiting parallelism to \"number of CPUs\" by default, and limiting parallelism by dividing \"available memory\" by some estimate or measurement of per-invocation memory use of the commands.The base command of any jj command should be usable. By default jj run
works on the @
the current working copy.
continue|stop|fatal
, see Dealing with failurejj log
: No special handling needed jj diff
: No special handling needed jj st
: For now reprint the final output of jj run
jj op log
: No special handling needed, but awaits further discussion in #963 jj undo/jj op undo
: No special handling needed
Should the command be working copy backend specific? How do we manage the Processes which the command will spawn? Configuration options, User and Repository Wide?
"},{"location":"design/run/#future-possibilities","title":"Future possibilities","text":"select(..., message = \"arch not supported for $project\")
.jj run
asynchronous by spawning a main
process, directly return to the user and incrementally updating the output of jj st
.Authors: Daniel Ploch
Summary: This Document documents a redesign of the sparse command and it's internal storage format in jj, in order to facilitate several desirable improvements for large repos. It covers both the migration path and the planned end state.
"},{"location":"design/sparse-v2/#objective","title":"Objective","text":"Redesign Sparse Patterns to accommodate more advanced features for native and custom implementations. This includes three main goals:
Sparse patterns are an effectively unordered list of prefix strings:
path/one\npath/to/dir/two\n
The set of files identified by the Sparse Patterns is all paths which match any provided prefix. This governs what gets materialized in the working copy on checkout, and what is updated on snapshot. The set is stored in working copy state files which are not versioned in the Op Store.
Because all paths are bare strings with no escaping or higher-level formatting, the current design makes it difficult to add new features like exclusions or path remappings.
"},{"location":"design/sparse-v2/#proposed-state-sparse-patterns-v2","title":"Proposed State (Sparse Patterns v2)","text":"Sparse Patterns v2 will be stored as objects in the Op Store, referenced by a WorkingCopyPatternsId
from the active View
. They will have a new, ordered structure which can fully represent previous patterns.
/// Analogues of RepoPath, specifically describing paths in the working copy.\nstruct WorkingCopyPathBuf {\n String\n}\nstruct WorkingCopyPath {\n str\n}\n\npub enum SparsePatternsPathType {\n Dir, // Everything under <path>/...\n Files, // Files under <path>/*\n Exact, // <path> exactly\n}\n\npub struct SparsePatternsPath {\n path_type: SparsePatternsPathType,\n include: bool, // True if included, false if excluded.\n path: RepoPathBuf,\n}\n\npub struct WorkingCopyMapping {\n src_path: RepoPathBuf,\n dst_path: WorkingCopyPathBuf,\n recursive: bool, // If false, only immediate children of src_path (files) are renamed.\n}\n\npub struct WorkingCopyPatterns {\n sparse_paths: Vec<SparsePatternsPath>,\n mappings: Vec<WorkingCopyMapping>,\n}\n\npub trait OpStore {\n ...\n pub fn read_working_copy_patterns(&self, id: &WorkingCopyPatternsId) -> OpStoreResult<WorkingCopyPatterns> { ... }\n pub fn write_working_copy_patterns(&self, sparse_patterns: &WorkingCopyPatterns) -> OpStoreResult<WorkingCopyPatternsId> { .. }\n}\n
To support these more complex behaviors, a new WorkingCopyPatterns
trait will be introduced, initially only as a thin wrapper around the existing prefix format, but soon to be expanded with richer types and functionality.
impl WorkingCopyPatterns {\n pub fn to_matcher(&self) -> Box<dyn Matcher> {\n ...\n }\n\n ...\n}\n
"},{"location":"design/sparse-v2/#command-syntax","title":"Command Syntax","text":"SparsePatternsPath
rules can be specified on the CLI and in an editor via a compact syntax:
(include|exclude):(dir|files|exact):<path>\n
If both prefix terms are omitted, then include:dir:
is assumed. If any prefix is specified, both must be specified. The editor and CLI will both accept path rules in either format going forward.
jj sparse set --add foo/bar
is equal to jj sparse set --add include:dir:foo/bar
jj sparse set --add exclude:dir:foo/bar
adds a new Dir
type rule with include = false
jj sparse set --exclude foo/bar
as a possible shorthand for the abovejj sparse list
will print the explicit rulesPaths will be stored in an ordered, canonical form which unambiguously describes the set of files to be included. Every --add
command will append to the end of this list before the patterns are canonicalized. Whether a file is included is determined by the first matching rule in reverse order.
For example:
include:dir:foo\nexclude:dir:foo/bar\ninclude:dir:foo/bar/baz\nexclude:dir:foo/bar/baz/qux\n
Produces rule set which includes \"foo/file.txt\", excludes \"foo/bar/file.txt\", includes \"foo/bar/baz/file.txt\", and excludes \"foo/bar/baz/qux/file.txt\".
If the rules are subtly re-ordered, they become canonicalized to a smaller, but functionally equivalent form:
# Before\ninclude:dir:foo\nexclude:dir:foo/bar/baz/qux\ninclude:dir:foo/bar/baz\nexclude:dir:foo/bar\n\n# Canonicalized\ninclude:dir:foo\nexclude:dir:foo/bar\n
"},{"location":"design/sparse-v2/#canonicalization","title":"Canonicalization","text":"There are many ways to represent functionally equivalent WorkingCopyPatterns
. For instance, the following 4 rule sets are all functionally equivalent:
# Set 1\ninclude:dir:bar\ninclude:dir:foo\n\n# Set 2\ninclude:dir:foo\ninclude:dir:bar\n\n# Set 3\ninclude:dir:bar\ninclude:dir:bar/baz/qux\ninclude:dir:foo\n\n# Set 4\ninclude:dir:foo\nexclude:dir:foo/baz\ninclude:dir:bar\ninclude:dir:foo/baz\n
Because these patterns are stored in the Op Store now, it is useful for all of these representations to be rewritten into a minimal, canonical form before serialization. In this case, Set 1
will be the canonical set. The canonical form of a WorkingCopyPatterns
is defined as the form such that:
WARNING: This section is intentionally lacking, more research is needed.
All WorkingCopyPatterns
will come equipped with a default no-op mapping. These mappings are inspired by and similar to Perforce client views.
vec![WorkingCopyMapping {\n src_path: RepoPathBuf::root(),\n dst_path: WorkingCopyPathBuf::root(),\n recursive: true,\n}]\n
WorkingCopyPatterns
will provide an interface to map working copy paths into repo paths and vice versa. The `WorkingCopy`` trait will apply this mapping to all snapshot and checkout operations, and jj commands which accept relative paths will need to be updated to perform working copy path -> repo path translations as needed. It's not clear at this time which commands will need changing, as some are more likely to refer to repo paths rather than working copy paths.
TODO: Expand this section.
In particular, the path rules for sparse patterns will always be repo paths, not working copy paths. Thus, if the working copy wants to track \"foo\" and rename it to \"subdir/bar\", they must jj sparse set --add foo
and jj map set --from foo --to bar
. In other words, the mapping operation can be thought of as always after the sparse operation.
New commands will enable editing of the WorkingCopyMapping
s:
TODO: Maybe this should be jj workspace map ...
?
jj map list
will print all mapping pairs.jj map add --from foo --to bar
will add a new mapping to the end of the list.jj map remove --from foo
will remove a specific mapping rule.jj map edit
will pull up a text editor for manual editing.Like sparse paths, mappings will have a compact text syntax for editing in file form, or for adding a rule textually on the CLI:
\"<from>\" -> \"<to>\" [nonrecursive]\n
Like sparse paths, mapping rules are defined to apply in order and on any save operation will be modified to a minimal canonical form. Thus, jj map set --from \"\" --to \"\"
will always completely wipe the map. The first matching rule in reverse list order determines how a particular repo path should be mapped into the working copy, and likewise how a particular working copy path should be mapped into the repo. For simplicity, the 'last rule wins' applies both for repo->WC conversions, as well as WC->repo conversions, using the same ordering.
If a working copy mapping places the same repo file at two distinct working copy paths, snapshotting will fail unless these files are identical. Some specialized filesystems may even treat these as the 'same' file, allowing this to work in some cases.
If a working copy mapping places two distinct repo files at the same working copy path, checkout will fail with an error regardless of equivalence.
"},{"location":"design/sparse-v2/#versioning-and-storage","title":"Versioning and Storage","text":"Updating the active WorkingCopyPatterns
for a particular working copy will now take place in two separate steps: one transaction which updates the op store, and a separate LockedWorkingCopy
operation which actually updates the working copy. The working copy proto will no longer store WorkingCopyPatterns
directly, instead storing only a WorkingCopyPatternsId
. On mismatch with the current op head, the user will be prompted to run jj workspace update-stale
.
This gives the user the ability to update the active WorkingCopyPatterns
whilst not interacting with the local working copy, which is useful for custom integrations which may not be able to check out particular working copy patterns due to problems with the backend (encoding, permission errors, etc.). A bad jj sparse set --add oops
command can thus be undone, even via jj op undo
if desired.
The View object will be migrated to store working copy patterns via id. The indirection will save on storage since working copy patterns are not expected to change very frequently.
// Before:\npub wc_commit_ids: HashMap<WorkspaceId, CommitId>,\n\n// After:\npub struct WorkingCopyInfo {\n pub commit_id: CommitId,\n pub wc_patterns_id: WorkingCopyPatternsId,\n}\n...\npub wc_info: HashMap<WorkspaceId, WorkingCopyInfo>,\n
A View object with no stored working copy patterns will be modified at read time to include the current working copy patterns, thus all read_view
operations will need to pass in the current working copy patterns for a migration period of at least 6 months. After that, we may choose to auto-fill missing working copy infos with a default WorkingCopyPatterns
as needed.
Perforce client maps are very similar in concept to the entirety of WorkingCopyPatterns
, and this design aims to achieve similar functionality.
The Josh Project implements partial git clones in a way similar to how sparse patterns try to work.
"},{"location":"design/sparse-v2/#patterns-via-configuration","title":"Patterns via configuration","text":"There may be some scenarios where it is valuable to configure working copy patterns via a configuration file, rather than through explicit commands. Generally this only makes sense for automated repos, with the configuration coming from outside the repo - there are too many caveats and edge cases if the configuration comes from inside the repo and/or is fought with by a human.
No configuration syntax is planned at this time but if we add any, we should probably reuse the compact line syntaxes as much as possible for consistency.
"},{"location":"design/tracking-branches/","title":"Remote/@git
tracking branches","text":"This is a plan to implement more Git-like remote tracking branch UX.
"},{"location":"design/tracking-branches/#objective","title":"Objective","text":"jj
imports all remote branches to local branches by default. As described in #1136, this doesn't interact nicely with Git if we have multiple Git remotes with a number of branches. The git.auto-local-bookmark
config can mitigate this problem, but we'll get locally-deleted branches instead.
The goal of this plan is to implement
Under the current model, all remote branches are \"tracking\" branches, and remote changes are merged into the local counterparts.
branches\n [name]:\n local_target?\n remote_targets[remote]: target\ntags\n [name]: target\ngit_refs\n [\"refs/heads/{name}\"]: target # last-known local branches\n [\"refs/remotes/{remote}/{name}\"]: target # last-known remote branches\n # (copied to remote_targets)\n [\"refs/tags/{name}\"]: target # last-known tags\ngit_head: target?\n
branches[name].remote_targets
and git_refs[\"refs/remotes\"]
. These two are mostly kept in sync, but there are two scenarios where remote-tracking branches and git refs can diverge:jj branch forget
jj op undo
/restore
in colocated repo@git
tracking branches are stored in git_refs[\"refs/heads\"]
. We need special case to resolve @git
branches, and their behavior is slightly different from the other remote-tracking branches.We'll add a per-remote-branch state
to distinguish non-tracking branches from tracking ones.
state = new # not merged in the local branch or tag\n | tracking # merged in the local branch or tag\n# `ignored` state could be added if we want to manage it by view, not by\n# config file. target of ignored remote branch would be absent.\n
We'll add a per-remote view-like object to record the last known remote branches. It will replace branches[name].remote_targets
in the current model. @git
branches will be stored in remotes[\"git\"]
.
branches\n [name]: target\ntags\n [name]: target\nremotes\n [\"git\"]:\n branches\n [name]: target, state # refs/heads/{name}\n tags\n [name]: target, state = tracking # refs/tags/{name}\n head: target?, state = TBD # refs/HEAD\n [remote]:\n branches\n [name]: target, state # refs/remotes/{remote}/{name}\n tags: (empty)\n head: (empty)\ngit_refs # last imported/exported refs\n [\"refs/heads/{name}\"]: target\n [\"refs/remotes/{remote}/{name}\"]: target\n [\"refs/tags/{name}\"]: target\n
With the proposed data model, we can
branches[name].remote_targets
and git_refs[\"refs/remotes\"]
export flow import flow\n ----------- -----------\n +----------------+ --.\n +------------------->|backing Git repo|---+ :\n | +----------------+ | : unchanged\n |[update] |[copy] : on \"op restore\"\n | +----------+ | :\n | +-------------->| git_refs |<------+ :\n | | +----------+ | --'\n +--[compare] [diff]--+\n | .-- +---------------+ | | --.\n | : +--->|remotes[\"git\"] | | | :\n +---: | | |<---+ | :\n : | |remotes[remote]| | : restored\n '-- | +---------------+ |[merge] : on \"op restore\"\n | | : by default\n [copy]| +---------------+ | :\n +----| (local) |<---------+ :\n | branches/tags | :\n +---------------+ --'\n
jj git import
applies diff between git_refs
and remotes[]
. git_refs
is always copied from the backing Git repo.jj git export
copies jj's remotes
view back to the Git repo. If a ref in the Git repo has been updated since the last import, the ref isn't exported.jj op restore
never rolls back git_refs
.The git.auto-local-bookmark
config knob is applied when importing new remote branch. jj branch
sub commands will be added to change the tracking state.
fn default_state_for_newly_imported_branch(config, remote) {\n if remote == \"git\" {\n State::Tracking\n } else if config[\"git.auto-local-bookmark\"] {\n State::Tracking\n } else {\n State::New\n }\n}\n
A branch target to be merged is calculated based on the state
.
fn target_in_merge_context(known_target, state) {\n match state {\n State::New => RefTarget::absent(),\n State::Tracking => known_target,\n }\n}\n
"},{"location":"design/tracking-branches/#mapping-to-the-current-data-model","title":"Mapping to the current data model","text":"remotes[\"git\"].branches
corresponds to git_refs[\"refs/heads\"]
, but forgotten branches are removed from remotes[\"git\"].branches
.remotes[\"git\"].tags
corresponds to git_refs[\"refs/tags\"]
.remotes[\"git\"].head
corresponds to git_head
.remotes[remote].branches
corresponds to branches[].remote_targets[remote]
.state = new|tracking
doesn't exist in the current model. It's determined by git.auto-local-bookmark
config.In the following sections, a merge is expressed as adds - removes
. In particular, a merge of local and remote targets is [local, remote] - [known_remote]
.
jj git fetch
remotes[remote].branches[glob]
(see below).tags
?jj git import
git_refs
from the backing Git repo.remotes
to the new git_refs
.git_refs[\"refs/heads\"] - remotes[\"git\"].branches
git_refs[\"refs/tags\"] - remotes[\"git\"].tags
\"HEAD\" - remotes[\"git\"].head
(unused)git_refs[\"refs/remotes/{remote}\"] - remotes[remote]
branches
and tags
if state
is tracking
.target
is absent
, the default state
should be calculated. This also applies to previously-forgotten branches.remotes
reflecting the import.jj git push
remotes[remote]
to the local changes.branches - remotes[remote].branches
state
is new
(i.e. untracked), the known remote branch target
is considered absent
.state
is new
, and if the local branch target
is absent
, the diff [absent, remote] - absent
is noop. So it's not allowed to push deleted branch to untracked remote.--force-with-lease
behavior?tags
~ (not implemented, but should be the same as branches
)remotes[remote]
and git_refs
reflecting the push.jj git export
branches
/tags
back to remotes[\"git\"]
.remotes[\"git\"].branches[name].state
can be set to untracked. Untracked local branches won't be exported to Git.remotes[\"git\"].branches[name]
is absent
, the default state = tracking
applies. This also applies to forgotten branches.tags
~ (not implemented, but should be the same as branches
)git_refs
to the new remotes[remote]
.git_refs
reflecting the export.If a ref failed to export at the step 3, the preceding steps should also be rolled back for that ref.
jj init
git.auto_local_branch
config.!git.auto_local_branch
, no tracking
state will be set.jj git clone
git.auto_local_branch
config.git.auto_local_branch
config. This isn't technically needed, but will help users coming from Git.jj branch set {name}
branches[name]
entry.jj branch delete {name}
branches[name]
entry.jj branch forget {name}
branches[name]
entry if exists.remotes[remote].branches[name]
entries if exist. TODO: maybe better to not remove non-tracking remote branches?jj branch track {name}@{remote}
(new command)[local, remote] - [absent]
in local branch.remotes[remote].branches[name].state = tracking
.jj branch untrack {name}@{remote}
(new command)remotes[remote].branches[name].state = new
.jj branch list
Note: desired behavior of jj branch forget
is to
state = new|tracking
based on git.auto_local_branch
state
is tracking
, merges [absent, new_remote] - [absent]
(i.e. creates local branch with new_remote
target)remotes[remote].branches[name].state
[local, new_remote] - [known_remote]
state = new|tracking
based on git.auto_local_branch
state
is tracking
, merges [local, new_remote] - [absent]
remotes[remote].branches[name].state
[local, absent] - [known_remote]
remotes[remote].branches[name]
(target
becomes absent
) (i.e. the remote branch is no longer tracked)state = new|tracking
based on git.auto_local_branch
[local, absent] - [absent]
-> local
state = new|tracking
based on git.auto_local_branch
state
is tracking
, merges [absent, new_remote] - [absent]
-> new_remote
remotes[remote].branches[name].state
state = new
[local, absent] - [absent]
-> local
remotes[remote].branches[name].target = local
, .state = tracking
[local, remote] - [absent]
local
moved backwards or sidewaysremotes[remote].branches[name].target = local
, .state = tracking
[local, remote] - [remote]
-> local
local
moved backwards or sideways, and if remote
is out of syncremotes[remote].branches[name].target = local
[absent, remote] - [remote]
-> absent
remote
is out of sync?remotes[remote].branches[name]
(target
becomes absent
)[absent, remote] - [absent]
-> remote
target
of forgotten remote branch is absent
remotes[\"git\"].branches[name].target = local
, .state = tracking
[local, absent] - [absent]
-> local
[local, git] - [absent]
-> failremotes[\"git\"].branches[name].target = local
[local, git] - [git]
-> local
remotes[\"git\"].branches[name]
[absent, git] - [git]
-> absent
[absent, git] - [git]
-> absent
for forgotten local/remote branches[old, git] - [git]
-> old
for undone local/remote branchesgit_refs
isn't diffed against the refs in the backing Git repo.@git
remote","text":"jj branch untrack {name}@git
jj git fetch --remote git
git::import_refs()
only for local branches.jj git push --remote git
jj branch track
and git::export_refs()
only for local branches.tracking
remotes?We're introducing a temporary process to describe how we'll gain approval to adopt permanent governance policies - basically, how we make social and technical decisions as a community. This temporary process describes how the governance working group can propose these policies and how community members can influence them and vote on them. Once permanent governance policies are in place, the temporary process will stop being used, and the permanent governance policies will be used instead.
"},{"location":"governance/temporary-voting/#context","title":"Context","text":"The governance working group was appointed by recommendation from Martin (jj's original author and current sole maintainer), without recommendation or approval from the broader jj community. This isn't a problem in itself - but it does mean that the governance working group (Austin Seipp/aseipp, Waleed Khan/arxanas, Martin von Zweigbergk/martinvonz, and Emily Shaffer/nasamuffin) needs to get some community approval before setting policy for the entire jj project. If we skip this step, we risk being perceived as exercising excessive control over the project.
"},{"location":"governance/temporary-voting/#goals-and-non-goals","title":"Goals and Non-Goals","text":"governance.md
(describing the formal structure of governance used for this project), technical design approval process, and code review process.The working group lets the community know about upcoming policy drafts they're intending to share for approval. This must happen at least a week before entering stage 3, and ideally should happen even earlier.
At this time, the working group should:
At this time, the community is invited to:
The working group will consider these recommendations in good faith, but may choose not to adopt them.
"},{"location":"governance/temporary-voting/#stage-2-proposal-review-period","title":"Stage 2: Proposal Review Period","text":"This stage lasts until the working group feels major concerns have been addressed and the proposal is ready for a vote. However, at least 72 hours must elapse between the proposal being published and the vote starting, to allow community members around the globe to read and comment. Typically, this stage should last at least one week.
At this time, the working group should:
At this time, the community is invited to:
Think of this like a code review; the goal of this stage is to build a proposal that is representative of the community's will. Keep recommendations actionable and constructive: \"This clause discourages X; if we phrase it like \"foo bar baz\" it could be less exclusive\" is much more productive than \"It's obvious that the governance working group doesn't want X!\"
At the discretion of the working group, but based on the outcome of the discussion, the proposal will go to a vote or the proposal will be dropped.
"},{"location":"governance/temporary-voting/#stage-3-proposal-voting-period","title":"Stage 3: Proposal Voting Period","text":"When the working group feels that major concerns have been addressed and is happy with the text of the proposal, the working group will open voting on the proposal.
Proposals with 2/3 or more votes in favor at the end of the voting period will be approved.
After voting has concluded, either:
Deciding whether to revise or abandon is up to the discretion of the governance working group. The working group is expected to double-check their assumption that the goals the proposal is attempting to meet are desirable after the proposal fails to be accepted.
"},{"location":"governance/temporary-voting/#stage-4-implementation","title":"Stage 4: Implementation","text":"Typically, implementation will look like merging the document with the policy into the jj codebase and remembering to use that policy in conversations moving forward.
In some cases, implementation may also involve nomination of individuals to a group or committee. When this is necessary, expect the policy being proposed to describe how these individuals will be nominated, both initially and moving into the future.
It's possible (but unlikely) that during implementation, some obstacle will arise that means the policy doesn't actually work. If this does happen, expect the working group to be transparent with the community about the situation. We may reuse some of all of this process to figure out how to move forward.
"},{"location":"technical/architecture/","title":"Architecture","text":""},{"location":"technical/architecture/#data-model","title":"Data model","text":"The commit data model is similar to Git's object model , but with some differences.
"},{"location":"technical/architecture/#separation-of-library-from-ui","title":"Separation of library from UI","text":"The jj
binary consists of two Rust crates: the library crate (jj-lib
) and the CLI crate (jj-cli
). The library crate is currently only used by the CLI crate, but it is meant to also be usable from a GUI or TUI, or in a server serving requests from multiple users. As a result, the library should avoid interacting directly with the user via the terminal or by other means; all input/output is handled by the CLI crate 1. Since the library crate is meant to usable in a server, it also cannot read configuration from the user's home directory, or from user-specific environment variables.
A lot of thought has gone into making the library crate's API easy to use, but not much has gone into \"details\" such as which collection types are used, or which symbols are exposed in the API.
"},{"location":"technical/architecture/#storage-independent-apis","title":"Storage-independent APIs","text":"One overarching principle in the design is that it should be easy to change where data is stored. The goal was to be able to put storage on local-disk by default but also be able to move storage to the cloud at Google (and for anyone). To that end, commits (and trees, files, etc.) are stored by the commit backend, operations (and views) are stored by the operation backend, the heads of the operation log are stored by the \"op heads\" backend, the commit index is stored by the index backend, and the working copy is stored by the working copy backend. The interfaces are defined in terms of plain Rust data types, not tied to a specific format. The working copy doesn't have its own trait defined yet, but its interface is small and easy to create traits for when needed.
The commit backend to use when loading a repo is specified in the .jj/repo/store/type
file. There are similar files for the other backends (.jj/repo/index/type
, .jj/repo/op_store/type
, .jj/repo/op_heads/type
).
Here's a diagram showing some important types in the library crate. The following sections describe each component.
graph TD;\n ReadonlyRepo-->Store;\n ReadonlyRepo-->OpStore;\n ReadonlyRepo-->OpHeadsStore;\n ReadonlyRepo-->ReadonlyIndex\n MutableIndex-->ReadonlyIndex;\n Store-->Backend;\n GitBackend-->Backend;\n LocalBackend-->Backend;\n LocalBackend-->StackedTable;\n MutableRepo-->ReadonlyRepo;\n MutableRepo-->MutableIndex;\n Transaction-->MutableRepo;\n WorkingCopy-->TreeState;\n Workspace-->WorkingCopy;\n Workspace-->RepoLoader;\n RepoLoader-->Store;\n RepoLoader-->OpStore;\n RepoLoader-->OpHeadsStore;\n RepoLoader-->ReadonlyRepo;\n Git-->GitBackend;\n GitBackend-->StackedTable;
"},{"location":"technical/architecture/#backend","title":"Backend","text":"The Backend
trait defines the interface each commit backend needs to implement. The current in-tree commit backends are GitBackend
and LocalBackend
.
Since there are non-commit backends, the Backend
trait should probably be renamed to CommitBackend
.
The GitBackend
stores commits in a Git repository. It uses libgit2
to read and write commits and refs.
To prevent GC from deleting commits that are still reachable from the operation log, the GitBackend
stores a ref for each commit in the operation log in the refs/jj/keep/
namespace.
Commit data that is available in Jujutsu's model but not in Git's model is stored in a StackedTable
in .jj/repo/store/extra/
. That is currently the change ID and the list of predecessors. For commits that don't have any data in that table, which is any commit created by git
, we use an empty list as predecessors, and the bit-reversed commit ID as change ID.
Because we use the Git Object ID as commit ID, two commits that differ only in their change ID, for example, will get the same commit ID, so we error out when trying to write the second one of them.
"},{"location":"technical/architecture/#localbackend","title":"LocalBackend","text":"The LocalBackend
is just a proof of concept. It stores objects addressed by their hash, with one file per object.
The Store
type wraps the Backend
and returns wrapped types for commits and trees to make them easier to use. The wrapped objects have a reference to the Store
itself, so you can do e.g. commit.parents()
without having to provide the Store
as an argument.
The Store
type also provides caching of commits and trees.
A ReadonlyRepo
represents the state of a repo at a specific operation. It keeps the view object associated with that operation.
The repository doesn't know where on disk any working copies live. It knows, via the view object, which commit is supposed to be the current working-copy commit in each workspace.
"},{"location":"technical/architecture/#mutablerepo","title":"MutableRepo","text":"A MutableRepo
is a mutable version of ReadonlyRepo
. It has a reference to its base ReadonlyRepo
, but it has its own copy of the view object and lets the caller modify it.
The Transaction
object has a MutableRepo
and metadata that will go into the operation log. When the transaction commits, the MutableRepo
becomes a view object in the operation log on disk, and the Transaction
object becomes an operation object. In memory, Transaction::commit()
returns a new ReadonlyRepo
.
The RepoLoader
represents a repository at an unspecified operation. You can think of as a pointer to the .jj/repo/
directory. It can create a ReadonlyRepo
given an operation ID.
The TreeState
type represents the state of the files in a working copy. It keep track of the mtime and size for each tracked file. It knows the TreeId
that the working copy represents. It has a snapshot()
method that will use the recorded mtimes and sizes and detect changes in the working copy. If anything changed, it will return a new TreeId
. It also has checkout()
for updating the files on disk to match a requested TreeId
.
The TreeState
type supports sparse checkouts. In fact, all working copies are sparse; they simply track the full repo in most cases.
The WorkingCopy
type has a TreeState
but also knows which WorkspaceId
it has and at which operation it was most recently updated.
The Workspace
type represents the combination of a repo and a working copy ( like Git's 'worktree' concept).
The repo view at the current operation determines the desired working-copy commit in each workspace. The WorkingCopy
determines what is actually in the working copy. The working copy can become stale if the working-copy commit was changed from another workspace (or if the process updating the working copy crashed, for example).
The git
module contains functionality for interoperating with a Git repo, at a higher level than the GitBackend
. The GitBackend
is restricted by the Backend
trait; the git
module is specifically for Git-backed repos. It has functionality for importing refs from the Git repo and for exporting to refs in the Git repo. It also has functionality for pushing and pulling to/from Git remotes.
A user-provided revset expression string goes through a few different stages to be evaluated:
RevsetExpression
, which is close to an ASTtags()
into specific commits. After this stage, the expression is still a RevsetExpression
, but it won't have any CommitRef
variants in it.visible_heads()
and all()
and produces a ResolvedExpression
.ResolvedExpression
into a Revset
.This evaluation step is performed by Index::evaluate_revset()
, allowing the Revset
implementation to leverage the specifics of a custom index implementation. The first three steps are independent of the index implementation.
StackedTable
(actually ReadonlyTable
and MutableTable
) is a simple disk format for storing key-value pairs sorted by key. The keys have to have the same size but the values can have different sizes. We use our own format because we want lock-free concurrency and there doesn't seem to be an existing key-value store we could use.
The file format contains a lookup table followed by concatenated values. The lookup table is a sorted list of keys, where each key is followed by the associated value's offset in the concatenated values.
A table can have a parent table. When looking up a key, if it's not found in the current table, the parent table is searched. We never update a table in place. If the number of new entries to write is less than half the number of entries in the parent table, we create a new table with the new entries and a pointer to the parent. Otherwise, we copy the entries from the parent table and the new entries into a new table with the grandparent as the parent. We do that recursively so parent tables are at least 2 times as large as child tables. This results in O(log N) amortized insertion time and lookup time.
There's no garbage collection of unreachable tables yet.
The tables are named by their hash. We keep a separate directory of pointers to the current leaf tables, in the same way as we do for the operation log.
"},{"location":"technical/architecture/#design-of-the-cli-crate","title":"Design of the CLI crate","text":""},{"location":"technical/architecture/#templates","title":"Templates","text":"The concept is copied from Mercurial, but the syntax is different. The main difference is that the top-level expression is a template expression, not a string like in Mercurial. There is also no string interpolation (e.g. \"Commit ID: {node}\"
in Mercurial).
Diff-editing works by creating two very sparse working copies, containing only the files we want the user to edit. We then let the user edit the right-hand side of the diff. Then we simply snapshot that working copy to create the new tree.
There are a few exceptions, such as for messages printed during automatic upgrades of the repo format\u00a0\u21a9
Concurrent editing is a key feature of DVCSs -- that's why they're called Distributed Version Control Systems. A DVCS that didn't let users edit files and create commits on separate machines at the same time wouldn't be much of a distributed VCS.
When conflicting changes are made in different clones, a DVCS will have to deal with that when you push or pull. For example, when using Mercurial, if the remote has updated a bookmark called main
(Mercurial's bookmarks are similar to a Git's branches) and you had updated the same bookmark locally but made it point to a different target, Mercurial would add a bookmark called main@origin
to indicate the conflict. Git instead prevents the conflict by renaming pulled branches to origin/main
whether or not there was a conflict. However, most DVCSs treat local concurrency quite differently, typically by using lock files to prevent concurrent edits. Unlike those DVCSs, Jujutsu treats concurrent edits the same whether they're made locally or remotely.
One problem with using lock files is that they don't work when the clone is in a distributed file system. Most clones are of course not stored in distributed file systems, but it is a big problem when they are (Mercurial repos frequently get corrupted, for example).
Another problem with using lock files is related to complexity of implementation. The simplest way of using lock files is to take coarse-grained locks early: every command that may modify the repo takes a lock at the very beginning. However, that means that operations that wouldn't actually conflict would still have to wait for each other. The user experience can be improved by using finer-grained locks and/or taking the locks later. The drawback of that is complexity. For example, you need to verify that any assumptions you made before locking are still valid after you take the lock.
To avoid depending on lock files, Jujutsu takes a different approach by accepting that concurrent changes can always happen. It instead exposes any conflicting changes to the user, much like other DVCSs do for conflicting changes made remotely.
"},{"location":"technical/concurrency/#syncing-with-rsync-nfs-dropbox-etc","title":"Syncing withrsync
, NFS, Dropbox, etc","text":"Jujutsu's lock-free concurrency means that it's possible to update copies of the clone on different machines and then let rsync
(or Dropbox, or NFS, etc.) merge them. The working copy may mismatch what's supposed to be checked out, but no changes to the repo will be lost (added commits, moved bookmarks, etc.). If conflicting changes were made, they will appear as conflicts. For example, if a bookmark was moved to two different locations, they will appear in jj log
in both locations but with a \"?\" after the name, and jj status
will also inform the user about the conflict.
Note that, for now, there are known bugs in this area. Most notably, with the Git backend, repository corruption is possible because the backend is not entirely lock-free. If you know about the bug, it is relatively easy to recover from.
Moreover, such use of Jujutsu is not currently thoroughly tested, especially in the context of co-located repositories. While the contents of commits should be safe, concurrent modification of a repository from different computers might conceivably lose some bookmark pointers. Note that, unlike in pure Git, losing a bookmark pointer does not lead to losing commits.
"},{"location":"technical/concurrency/#operation-log","title":"Operation log","text":"The most important piece in the lock-free design is the \"operation log\". That is what allows us to detect and merge divergent operations.
The operation log is similar to a commit DAG (such as in Git's object model), but each commit object is instead an \"operation\" and each tree object is instead a \"view\". The view object contains the set of visible head commits, bookmarks, tags, and the working-copy commit in each workspace. The operation object contains a pointer to the view object (like how commit objects point to tree objects), pointers to parent operation(s) (like how commit objects point to parent commit(s)), and metadata about the operation. These types are defined in op_store.proto
The operation log is normally linear. It becomes non-linear if there are divergent operations.
When a command starts, it loads the repo at the latest operation. Because the associated view object completely defines the repo state, the running command will not see any changes made by other processes thereafter. When the operation completes, it is written with the start operation as parent. The operation cannot fail to commit (except for disk failures and such). It is left for the next command to notice if there were divergent operations. It will have to be able to do that anyway since the concurrent operation could have arrived via a distributed file system. This model -- where each operation sees a consistent view of the repo and is guaranteed to be able to commit their changes -- greatly simplifies the implementation of commands.
It is possible to load the repo at a particular operation with jj --at-operation=<operation ID> <command>
. If the command is mutational, that will result in a fork in the operation log. That works exactly the same as if any later operations had not existed when the command started. In other words, running commands on a repo loaded at an earlier operation works the same way as if the operations had been concurrent. This can be useful for simulating divergent operations.
If Jujutsu tries to load the repo and finds multiple heads in the operation log, it will do a 3-way merge of the view objects based on their common ancestor (possibly several 3-way merges if there were more than two heads). Conflicts are recorded in the resulting view object. For example, if bookmark main
was moved from commit A to commit B in one operation and moved to commit C in a concurrent operation, then main
will be recorded as \"moved from A to B or C\". See the RefTarget
definition in op_store.proto
.
Because we allow bookmarks (etc.) to be in a conflicted state rather than just erroring out when there are multiple heads, the user can continue to use the repo, including performing further operations on the repo. Of course, some commands will fail when using a conflicted bookmark. For example, jj checkout main
when main
is in a conflicted state will result in an error telling you that main
resolved to multiple revisions.
The operation objects and view objects are stored in content-addressed storage just like Git commits are. That makes them safe to write without locking.
We also need a way of finding the current head of the operation log. We do that by keeping the ID of the current head(s) as a file in a directory. The ID is the name of the file; it has no contents. When an operation completes, we add a file pointing to the new operation and then remove the file pointing to the old operation. Writing the new file is what makes the operation visible (if the old file didn't get properly deleted, then future readers will take care of that). This scheme ensures that transactions are atomic.
"},{"location":"technical/conflicts/","title":"First-class conflicts","text":""},{"location":"technical/conflicts/#introduction","title":"Introduction","text":"Conflicts can happen when two changes are applied to some state. This document is about conflicts between changes to files (not about conflicts between changes to bookmark targets, for example).
For example, if you merge two branches in a repo, there may be conflicting changes between the two branches. Most DVCSs require you to resolve those conflicts before you can finish the merge operation. Jujutsu instead records the conflicts in the commit and lets you resolve the conflict when you feel like it.
"},{"location":"technical/conflicts/#data-model","title":"Data model","text":"When a merge conflict happens, it is recorded as an ordered list of tree objects linked from the commit (instead of the usual single tree per commit). There will always be an odd number of trees linked from the commit. You can think of the first tree as a start tree, and the subsequent pairs of trees to apply the diff between onto the start. Examples:
The resulting tree contents is calculated on demand. Note that we often don't need to merge the entire tree. For example, when checking out a commit in the working copy, we only need to merge parts of the tree that differs from the tree that was previously checked out in the working copy. As another example, when listing paths with conflicts, we only need to traverse parts of the tree that cannot be trivially resolved; if only one side modified lib/
, then we don't need to look for conflicts in that sub-tree.
When merging trees, if we can't resolve a sub-tree conflict trivially by looking at just the tree id, we recurse into the sub-tree. Similarly, if we can't resolve a file conflict trivially by looking at just the id, we recursive into the hunks within the file.
See here for how conflicts are stored when using the Git commit backend.
"},{"location":"technical/conflicts/#conflict-simplification","title":"Conflict simplification","text":"Remember that a 3-way merge can be written A+C-B
. If one of those states is itself a conflict, then we simply insert the conflict expression there. Then we simplify by removing canceling terms. These two steps are implemented in Merge::flatten()
and Merge::simplify()
in merge.rs
.
For example, let's say commit B is based on A and is rebased to C, where it results in conflicts (B+C-A
), which the user leaves unresolved. If the commit is then rebased to D, the result will be (B+C-A)+(D-C)
(D-C
comes from changing the base from C to D). That expression can be simplified to B+D-A
, which is a regular 3-way merge between B and D with A as base (no trace of C). This is what lets the user keep old commits rebased to head without resolving conflicts and still not get messy recursive conflicts.
As another example, let's go through what happens when you back out a conflicted commit. Let's say we have the usual B+C-A
conflict on top of non-conflict state C. We then back out that change. Backing out (\"reverting\" in Git-speak) a change means applying its reverse diff, so the result is (B+C-A)+(A-(B+C-A))
, which we can simplify to just A
(i.e. no conflict).