diff --git a/prerelease/FAQ/index.html b/prerelease/FAQ/index.html index d075fe7ca0..06ef8e8fb6 100644 --- a/prerelease/FAQ/index.html +++ b/prerelease/FAQ/index.html @@ -390,8 +390,8 @@
git add -p
or hg commit -i
?¶At the moment the best options to partially add a file are: jj split
,
-jj amend -i
and jj move -i
.
git add -p && git commit
or hg commit -i
?¶Since the changes are already in the working-copy commit, the equivalent to
+git add -p && git commit
/git commit -p
/hg commit -i
is to split the
+working-copy commit with jj split -i
(or the practically identical
+jj commit -i
).
For the equivalent of git commit --amend -p
/hg amend -i
, use jj squash -i
.
git rebase --interactive
or hg histedit
?¶Not yet, you can check this issue for updates.
To reorder commits, it is for now recommended to rebase commits individually,
diff --git a/prerelease/search/search_index.json b/prerelease/search/search_index.json
index 4e3eb0a38e..967f236098 100644
--- a/prerelease/search/search_index.json
+++ b/prerelease/search/search_index.json
@@ -1 +1 @@
-{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Jujutsu\u2014a version control system","text":""},{"location":"#welcome-to-jjs-documentation-website","title":"Welcome to 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 branch to move forward when you commit. However, Jujutsu does not have a concept of a \"current branch\".
To move branches, use jj branch set
.
jj git push --all
says \"Nothing changed\" instead of pushing it. What do I do?","text":"jj git push --all
pushes all branches, not all revisions. You have two options:
jj git push --change
will automatically create a branch and push it.jj branch
commands to create or move a branch 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/#can-i-prevent-jujutsu-from-recording-my-unfinished-work-im-not-ready-to-commit-it","title":"Can I prevent Jujutsu from recording my unfinished work? I'm not ready to commit it.","text":"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.
For more options see the next question.
"},{"location":"FAQ/#can-i-add-a-portion-of-the-edits-i-made-to-a-file-similarly-to-git-add-p-or-hg-commit-i","title":"Can I add a portion of the edits I made to a file, similarly togit add -p
or hg commit -i
?","text":"At the moment the best options to partially add a file are: jj split
, jj amend -i
and jj move -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 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 $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).
You can find more details on gitignore
files here.
In general, you should separate out the changes to their own commit (using e.g. jj split
). After that, one possible workflow is to rebase your pending PRs on top of the commit with the local changes. Then, just before pushing to a remote, use jj rebase -s child_of_commit_with_local_changes -d main
to move the PRs back on top of main
.
If you have several PRs, you can try jj rebase -s all:commit_with_local_changes+ -d main
(note the +
) to move them all at once.
An alternative workflow would be to rebase the commit with local changes on top of the PR you're working on and then do jj new commit_with_local_changes
. You'll then need to use jj new --before
to create new commits and jj move --to
to move new changes into the correct commits.
Use jj obslog -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.
Branches are named pointers to revisions (just like they are in Git). You can move them without affecting the target revision's identity. Branches automatically move when revisions are rewritten (e.g. by jj rebase
). You can pass a branch's name to commands that want a revision as argument. For example, jj co main
will check out the revision pointed to by the \"main\" branch. Use jj branch list
to list branches and jj branch
to create, move, or delete branches. There is currently no concept of an active/current/checked-out branch.
Jujutsu identifies a branch by its name across remotes (this is unlike Git and more like Mercurial's \"bookmarks\"). For example, a branch called \"main\" in your local repo is considered the same branch as a branch by the same name on a remote. When you pull from a remote (currently only via jj git fetch
), any branches from the remote will be imported as branches in your local repo.
Jujutsu also records the last seen position on each remote (just like Git's remote-tracking branches). You can refer to these with <branch name>@<remote name>
, such as jj new main@origin
. Most commands don't show the remote branch if it has the same target as the local branch. The local branch (without @<remote name>
) is considered the branch's desired target. Consequently, if you want to update a branch on a remote, you first update the branch locally and then push the update to the remote. If a local branch also exists on some remote but points to a different target there, jj log
will show the branch name with an asterisk suffix (e.g. main*
). That is meant to remind you that you may want to push the branch to some remote.
When you pull from a remote, any changes compared to the current record of the remote's state will be propagated to the local branch. Let's say you run jj git fetch --remote origin
and the remote's \"main\" branch has moved so its target is now ahead of the local record in main@origin
. That will update main@origin
to the new target. It will also apply the change to the local branch main
. If the local target had also moved compared to main@origin
(probably because you had run jj branch set main
), then the two updates will be merged. If one is ahead of the other, then that target will be the new target. Otherwise, the local branch will be conflicted (see next section for details).
Branches can end up in a conflicted state. When that happens, jj status
will include information about the conflicted branches (and instructions for how to mitigate it). jj branch list
will have details. jj log
will show the branch name with a question mark suffix (e.g. main?
) on each of the conflicted branch's potential target revisions. Using the branch name to look up a revision will resolve to all potential targets. That means that jj co main
will error out, complaining that the revset resolved to multiple revisions.
Both local branches (e.g. main
) and the remote branch (e.g. main@origin
) can have conflicts. Both can end up in that state if concurrent operations were run in the repo. The local branch more typically becomes conflicted because it was updated both locally and on a remote.
To resolve a conflicted state in a local branch (e.g. main
), you can move the branch to the desired target with jj branch
. You may want to first either merge the conflicted targets with jj merge
, or you may want to rebase one side on top of the other with jj rebase
.
To resolve a conflicted state in a remote branch (e.g. main@origin
), simply pull from the remote (e.g. jj git fetch
). The conflict resolution will also propagate to the local branch (which was presumably also conflicted).
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
"},{"location":"code-of-conduct/#our-standards","title":"Our Standards","text":"Examples of behavior that contributes to creating a positive environment include:
Examples of unacceptable behavior by participants include:
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers 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, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
"},{"location":"code-of-conduct/#scope","title":"Scope","text":"This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
This Code of Conduct also applies outside the project spaces when the Project Steward has a reasonable belief that an individual's behavior may have a negative impact on the project or its community.
"},{"location":"code-of-conduct/#conflict-resolution","title":"Conflict Resolution","text":"We do not believe that all conflict is bad; healthy debate and disagreement often yield positive results. However, it is never okay to be disrespectful or to engage in behavior that violates the project\u2019s code of conduct.
If you see someone violating the code of conduct, you are encouraged to address the behavior directly with those involved. Many issues can be resolved quickly and easily, and this gives people more control over the outcome of their dispute. If you are unable to resolve the matter for any reason, or if the behavior is threatening or harassing, report it. We are dedicated to providing an environment where participants feel welcome and safe.
Reports should be directed to [PROJECT STEWARD NAME(s) AND EMAIL(s)], the Project Steward(s) for [PROJECT NAME]. It is the Project Steward\u2019s duty to receive and address reported violations of the code of conduct. They will then work with a committee consisting of representatives from the Open Source Programs Office and the Google Open Source Strategy team. If for any reason you are uncomfortable reaching out to the Project Steward, please email opensource@google.com.
We will investigate every complaint, but you may not receive a direct response. We will use our discretion in determining when and how to follow up on reported incidents, which may range from not taking action to permanent expulsion from the project and project-sponsored spaces. We will notify the accused of the report and provide them an opportunity to discuss it before any action is taken. The identity of the reporter will be omitted from the details of the report supplied to the accused. In potentially harmful situations, such as ongoing harassment or threats to anyone's safety, we may take action without notice.
"},{"location":"code-of-conduct/#attribution","title":"Attribution","text":"This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
"},{"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":"The config settings are loaded from the following locations. Less common ways to specify jj
config settings are discussed in a later section.
.jj/repo/config.toml
(per-repository)See the TOML site and the syntax guide for a description of the syntax.
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
and auto
(default: auto
). auto
will use color only when writing to a terminal.
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).
If you use a string value for a color, as in the example 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 = \"red\", 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\"\n
"},{"location":"config/#default-description","title":"Default description","text":"The value of the ui.default-description
setting will be used to prepopulate the editor when describing changes with an empty description. This could be a useful reminder to fill in things like BUG=, TESTED= etc.
ui.default-description = \"\\n\\nTESTED=TODO\"\n
"},{"location":"config/#diff-format","title":"Diff format","text":"# Possible values: \"color-words\" (default), \"git\", \"summary\"\nui.diff.format = \"git\"\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.
# Use Difftastic by default\nui.diff.tool = [\"difft\", \"--color=always\", \"$left\", \"$right\"]\n# Use tool named \"<name>\" (see below)\nui.diff.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.You can configure the set of immutable commits via revset-aliases.\"immutable_heads()\"
. The default set of immutable heads is trunk() | tags()
. For example, to prevent rewriting commits on main@origin
and commits authored by other users:
# The `main.. &` bit is an optimization to scan for non-`mine()` commits only\n# among commits that are not in `main`.\nrevset-aliases.\"immutable_heads()\" = \"main@origin | (main@origin.. & ~mine())\"\n
Ancestors of the configured set are also immutable. The root commit is always immutable even if the set is empty.
"},{"location":"config/#default-revisions-to-log","title":"Default revisions to log","text":"You can configure the revisions jj log
without -r
should show.
# Show commits that are not in `main@origin`\nrevsets.log = \"main@origin..\"\n
"},{"location":"config/#graph-style","title":"Graph style","text":"# Possible values: \"curved\" (default), \"square\", \"ascii\", \"ascii-large\",\n# \"legacy\"\nui.graph.style = \"square\"\n
"},{"location":"config/#wrap-log-content","title":"Wrap log content","text":"If enabled, log
/obslog
/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 branch\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 (default)\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/#pager","title":"Pager","text":"Windows users: Note that pagination is disabled by default on Windows for now (#2040).
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.
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
"},{"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) branch\n# compared to the `main` branch\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 = \"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 tool used for editing diffs (e.g. jj split
, jj amend -i
). The default is the special value :builtin
, which launches a TUI tool to edit the diff in your terminal.
jj
makes the following substitutions:
$left
and $right
are replaced with the paths to the left and right directories to diff respectively.If no arguments are specified, [\"$left\", \"$right\"]
are set by default.
For example:
# Use merge-tools.kdiff3.edit-args\nui.diff-editor = \"kdiff3\"\n# Specify edit-args inline\nui.diff-editor = [\"kdiff3\", \"--merge\", \"$left\", \"$right\"]\n
If ui.diff-editor
consists of a single word, e.g. \"kdiff3\"
, the arguments will be read from the following config keys.
# merge-tools.kdiff3.program = \"kdiff3\" # Defaults to the name of the tool if not specified\nmerge-tools.kdiff3.edit-args = [\n\"--merge\", \"--cs\", \"CreateBakFiles=0\", \"$left\", \"$right\"]\n
"},{"location":"config/#experimental-3-pane-diff-editing","title":"Experimental 3-pane diff editing","text":"The special \"meld-3\"
diff editor sets up Meld to show 3 panes: the sides of 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
.
To configure other diff editors, 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 these instructions 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 \"kdiff3\" or \"vimdiff\"\n# Specify merge-args inline\nui.merge-editor = [\"meld\", \"$left\", \"$base\", \"$right\", \"-o\", \"$output\"]\n
The \"meld\", \"kdiff3\", and \"vimdiff\" tools can be used out of the box, as long as they are installed.
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.
By default, when jj
imports a new remote-tracking branch from Git, it also creates a local branch with the same name. In some repositories, this may be undesirable, e.g.:
You can disable this behavior by setting git.auto-local-branch
like so,
git.auto-local-branch = false\n
This setting is applied only to new remote branches. Existing remote branches can be tracked individually by using jj branch track
/untrack
commands.
# import feature1 branch and start tracking it\njj branch track feature1@origin\n# delete local gh-pages branch and stop tracking it\njj branch delete gh-pages\njj branch untrack gh-pages@upstream\n
"},{"location":"config/#prefix-for-generated-branches-on-push","title":"Prefix for generated branches on push","text":"jj git push --change
generates branch names with a prefix of \"push-\" by default. You can pick a different prefix by setting git.push-branch-prefix
. For example:
git.push-branch-prefix = \"martinvonz/push-\"\n
"},{"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.
To configure the Watchman filesystem monitor, set core.fsmonitor = \"watchman\"
. Ensure that you have installed the Watchman executable on your system.
Debugging commands are available under jj debug watchman
.
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
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":"Like Pijul and Darcs but 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 ||||||| right\n APPLE\n GRAPE\n ORANGE\n >>>>>>>\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 the 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.
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 checking out the commit (jj checkout/new <commit>
) and then squash in the changes when you're done (jj squash
). jj git push
will automatically force-push the branch.
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.
"},{"location":"contributing/#community-guidelines","title":"Community Guidelines","text":"This project follows Google's Open Source Community Guidelines.
"},{"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.71 # 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.71 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.71.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
.
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
installed. If your distribution packages poetry
, something like apt install python3-poetry
is likely the best way to install it. Otherwise, you can download Python from https://python.org or follow the Python installation instructions. Finally, follow the Poetry 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
If you get requests to \"unlock a keyring\" or error messages about failing to do so, this is a known poetry
bug. The workaround is 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
branch 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
branch (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/'\\\nprerelease 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
branch, and push the gh-pages
branch 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/'\\\nv1.33.1 latest --push\n
(Optional) When you are done, you may want to reset the gh-branches
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 branch set gh-pages -r gh-pages@upstream\njj git push --remote origin --branch gh-pages\n
If you want to preserve some of the changes you made, you can do jj branch 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
branch 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
branch of https://martinvonz.github.com/jj (instead of the usual main
branch).
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":"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. 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 branch by that name in your local repo as well. 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 init --git
(without --git
, you get a native Jujutsu repo, which is slow and whose format will change) git init
Clone an existing repo jj git clone <source> <destination>
(there is no support for cloning non-Git repos yet) git clone <source> <destination>
Update the local repo with all 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 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 branch from the local repo jj git push --branch <branch name> [--remote <remote>]
(there is no support for pushing from non-Git repos yet) git push <remote> <branch name>
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 commits jj log
git log --oneline --graph --decorate
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\") 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
(first make sure that nothing is staged) Temporarily put away the current change Not needed git stash
Start working on a new change based on the <main> branch jj co main
git switch -c topic main
or git checkout -b topic main
(may need to stash or commit first) Move branch A onto 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 branch>
(may need to rebase other descendant branches separately) Reorder changes from A-B-C-D to A-C-B-D jj rebase -r C -d A; rebase -s B -d C
(pass change IDs, not commit IDs, to not have to look up commit ID of rewritten C) 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 move --to 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 move -i --from X --to 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>
List branches jj branch list
git branch
Create a branch jj branch create <name> -r <revision>
git branch <name> <revision>
Move a branch forward jj branch set <name> -r <revision>
git branch -f <name> <revision>
Move a branch backward or sideways jj branch set <name> -r <revision> --allow-backwards
git branch -f <name> <revision>
Delete a branch jj branch 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"},{"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 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 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.
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 init --git-repo=.
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 git pack-refs --all
to speed up the import.
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 st\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) is stored outside of the Git repo (currently in .jj/store/extra/
).
Paths with conflicts cannot be represented in Git. They appear as files with a .jjconflict
suffix in the Git repo. They contain a JSON representation with information about the conflict. They are not meant to be human-readable.
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 December 2022 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 branch when you need to push the stack to a remote. There are two primary workflows, using a generated branch name or naming a branch.
"},{"location":"github/#using-a-generated-branch-name","title":"Using a generated branch name","text":"In this example we're letting Jujutsu auto-create a branch.
# Start a new commit off of the default branch.\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 branch name and push that to GitHub\n$ jj git push -c @-\n
"},{"location":"github/#using-a-named-branch","title":"Using a named branch","text":"In this example, we create a branch named bar
and then push it to the remote.
# Start a new commit off of the default branch.\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 branch so we can push it to GitHub\n$ jj branch create bar -r @- # create a branch `bar` that now contains the previous two commits.\n# Push the branch to GitHub (pushes only `bar`)\n$ jj git push\n
While it's possible to create a branch and commit on top of it in a Git-like manner, you will then need to move the branch 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 December 2022, Jujutsu has no equivalent to a git pull
command. Until such a command is added, you need to use jj git fetch
followed by a jj rebase -d $main_branch
to update your changes.
After doing jj init --git-repo=.
, git will be in a detached HEAD state, which is unusual, as git mainly works with branches. In a co-located repository, jj
isn't the source of truth. But Jujutsu allows an incremental migration, as jj commit
updates the HEAD of the git repository.
$ nvim docs/tutorial.md\n$ # Do some more work.\n$ jj commit -m \"Update tutorial\"\n$ jj branch create doc-update\n$ # Move the previous revision to doc-update.\n$ jj branch set 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 branches, you just can generate one for a change. As Jujutsu is able to create a branch for a revision.
$ # Do your work\n$ jj commit\n$ # Push change \"mw\", letting Jujutsu automatically create a branch called \"push-mwmpwkwknuz\"\n$ jj git push --change mw
"},{"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 branch1. 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` branch from above.\n$ jj new your-feature\n$ # Address the comments, by updating the code\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 branch to point to the new commit.\n$ jj branch 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` branch from above.\n$ jj new your-feature\n$ # Address the comments, by updating the code\n$ jj diff\n$ # Give the fix a description.\n$ jj describe -m 'address pr comments'\n$ # Update the branch to point to the current commit.\n$ jj branch 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 reviews 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\n$ # Review the changes\n$ jj diff\n$ # Squash the changes into the parent commit\n$ jj squash\n$ # Push the updated branch to the remote. Jujutsu automatically makes it a force push\n$ jj git push --branch your-feature\n
The hyphen after your-feature
comes from revset syntax.
By default jj git clone
and jj git fetch
clone all active branches from the remote. This means that if you want to iterate or test another contributor's branch you can jj new <branchname>
onto it.
If your remote has a large amount of old, inactive branches or this feature is undesirable, set git.auto-local-branch = false
in the config file.
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 branches, which aren't on the main branch nor on any remote jj log -r 'branches() & ~(main | remote_branches())'
Log all revisions which you authored, across all branches which aren't on any remote jj log -r 'mine() & branches() & ~remote_branches()'
Log all remote branches, which you authored or committed to jj log -r 'remote_branches() & (mine() | committer(your@email.com))'
Log all descendants of the current working copy, which aren't on a remote jj log -r '::@ & ~remote_branches()'
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. In this case, 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\".
This is a GitHub Style review, as GitHub currently only is able to compare branches.\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 have any named branches 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/#branch","title":"Branch","text":"A branch is a named pointer to a commit. They automatically follow the commit if it gets rewritten. Branches are sometimes called \"named branches\" to distinguish them from anonymous branches, but note that they are more similar to Git's branches than to Mercurial's named branches. See here for details.
"},{"location":"glossary/#change","title":"Change","text":"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 branches. For example, if you moved a branch locally, and it was also moved on the remote, then the branch will be in a conflicted state after you pull from the remote. See here for details.
Similar to a branch 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 branch pointing to them) are visible at a given operation. Note that this is quite different from Git's HEAD.
A snapshot of the visible commits and branches 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/#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.
A view is a snapshot of branches 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/#linux","title":"Linux","text":""},{"location":"install-and-setup/#build-using-cargo","title":"Build usingcargo
","text":"First make sure that you have the libssl-dev
, openssl
, pkg-config
, and build-essential
packages 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/#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/#mac","title":"Mac","text":""},{"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/#from-source","title":"From Source","text":"You may 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/#windows","title":"Windows","text":"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 is the default\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/#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 branch, 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 as well as the -
operator (e.g. @-
) to get the parent of an operation.
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.
The commits listed by jj log
without arguments are called \"visible commits\". Other commits are only included if you explicitly mention them (e.g. by commit ID or a Git ref pointing to them).
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 branch.
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 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 & y
: Revisions that are in both x
and y
.x | y
: Revisions that are in either x
or y
(or both).x ~ y
: Revisions that are in x
but not in y
.~x
: Revisions that are not in x
.x-
: Parents of x
.x+
: Children of x
.::x
: Ancestors of x
, including the commits in x
itself.x::
: Descendants of x
, including the commits in x
itself.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
.::
: All visible commits in the repo. Equivalent to all()
.:x
, x:
, and x:y
: Deprecated versions of ::x
, x::
, and x::y
We plan to delete them in jj 0.15+.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)...x
: Ancestors of x
, including the commits in x
itself, but excluding the root commit. Equivalent to ::x ~ root()
.x..
: Revisions that are not ancestors of x
...
: All visible commits in the repo, but excluding the root commit. Equivalent to ~root()
.You can use parentheses to control evaluation order, such as (x & y) | z
or x & (y | z)
.
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)
: Same as x::
.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.branches([pattern])
: All local branch targets. If pattern
is specified, branches whose name contains the given string are selected. For example, branches(push)
would match the branches push-123
and repushed
but not the branch main
. If a branch is in a conflicted state, all its possible targets are included.remote_branches([branch_pattern[, [remote=]remote_pattern]])
: All remote branch targets across all remotes. If just the branch_pattern
is specified, branches whose name contains the given string across all remotes are selected. If both branch_pattern
and remote_pattern
are specified, the selection is further restricted to just the remotes whose name contains remote_pattern
. For example, remote_branches(push, ri)
would match the branches push-123@origin
and repushed@private
but not push-123@upstream
or main@origin
or main@upstream
. If a branch is in a conflicted state, all its possible targets are included.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. Equivalent to present(HEAD@git)
.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 with the given string in their description.author(pattern)
: Commits with the given string in the author's name or email.mine()
: Commits where the author's email matches the email of the current user.committer(pattern)
: Commits with the given string in the committer's name or email.empty()
: Commits modifying no files. This also includes merges()
without user modifications and root()
.file(pattern..)
: Commits modifying the paths specified by the pattern..
. 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, file(foo)
will match files foo
, foo/bar
, foo/bar/baz
, but not file foobar
.conflict()
: 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 branch name.)Functions that perform string matching support the following pattern syntax.
\"string\"
, 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
.New symbols and functions can be defined in the config file, by using any combination of the predefined symbols/functions and other aliases.
For example:
[revset-aliases]\n'mine' = 'author(martinvonz)'\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 branch of the remote named origin
or upstream
. The branches main
, master
, and trunk
are tried. If more than one potential trunk commit exists, the newest one is chosen. If none of the branches exist, the revset evaluates to root()
.You can override this as appropriate. If you do, make sure it always resolves to exactly one commit. For example:
[revset-aliases]\n'trunk()' = 'your-branch@your-remote'\n
"},{"location":"revsets/#examples","title":"Examples","text":"Show the parent(s) of the working-copy commit (like git log -1 HEAD
):
jj log -r @-\n
Show commits not on any remote branch:
jj log -r 'remote_branches()..'\n
Show commits not on origin
(if you have other remotes like fork
):
jj log -r 'remote_branches(remote=origin)..'\n
Show all ancestors of the working copy (almost like plain git log
)
jj log -r ::@\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() | branches()'\n
Show local commits leading up to the working copy, as well as descendants of those commits:
jj log -r '(remote_branches()..@)::'\n
Show commits authored by \"martinvonz\" and containing the word \"reset\" in the description:
jj log -r 'author(martinvonz) & description(reset)'\n
"},{"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 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.
sl shelve
.<<<<<<<
etc.). This also has several advantages: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.
"},{"location":"templates/#commit-keywords","title":"Commit keywords","text":"The following keywords can be used in jj log
/jj obslog
templates.
description: String
change_id: ChangeId
commit_id: CommitId
parents: List<Commit>
author: Signature
committer: Signature
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.branches: String
tags: String
git_refs: String
git_head: String
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).conflict: Boolean
: True if the commit contains merge conflicts.empty: Boolean
: True if the commit modifies no files.root: Boolean
: True if the commit is the root commit.The following keywords can be used in jj op log
templates.
current_operation: Boolean
description: String
id: OperationId
tags: String
time: TimestampRange
user: String
The following operators are supported.
x.f()
: Method call.x ++ y
: Concatenate x
and y
templates.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
.label(label: Template, content: Template) -> Template
: Apply label to the content. The label
is evaluated as a space-separated string.if(condition: Boolean, then: Template[, else: Template]) -> Template
: Conditionally evaluate then
/else
template content.concat(content: Template...) -> Template
: Same as content_1 ++ ... ++ content_n
.separate(separator: Template, content: Template...) -> Template
: Insert separator between non-empty contents.No methods are defined. Can be constructed with false
or true
literal.
This type cannot be printed. All commit keywords are accessible as 0-argument methods.
"},{"location":"templates/#commitid-changeid-type","title":"CommitId / ChangeId type","text":"The following methods are defined.
.short([len: Integer]) -> String
.shortest([min_len: Integer]) -> ShortestIdPrefix
: Shortest unique prefix.No methods are defined.
"},{"location":"templates/#list-type","title":"List type","text":"The following methods are defined.
.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
The following methods are defined.
.short([len: Integer]) -> String
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
A string can be implicitly converted to Boolean
. The following methods are defined.
.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. Negative values count from the end.String literals must be surrounded by double quotes (\"
). The following escape sequences starting with a backslash have their usual meaning: \\\"
, \\\\
, \\n
, \\r
, \\t
, \\0
. 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.
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.The following methods are defined.
.start() -> Timestamp
.end() -> Timestamp
.duration() -> String
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.
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":"tutorial/","title":"Tutorial","text":"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: d7439b06fbef (no description set)\nAdded 1 files, modified 0 files, removed 0 files\n$ cd Hello-World\n
Running jj st
(short forjj status
) now yields something like this:
$ jj st\nParent commit: 7fd1a60b01f9 Merge pull request #6 from Spaceghost/patch-1\nWorking copy : d7439b06fbef (no description set)\nThe working copy is clean\n
We can see from the output above that our working copy is a real commit with a commit ID (d7439b06fbef
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: e427edcfd0ba Say goodbye\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\nParent commit: 7fd1a60b01f9 Merge pull request #6 from Spaceghost/patch-1\nWorking copy : 5d39e19dac36 Say goodbye\nWorking copy changes:\nM README\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 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. For familiarity for user coming from other VCSs, there is also a jj checkout/co
command, which is practically a synonym for jj new
(you can specify a destination for jj new
as well).
So, let's say we're now done with this change, so we create a new change:
$ jj new\nWorking copy now at: aef4df99ea11 (no description set)\n$ jj st\nParent commit: 5d39e19dac36 Say goodbye\nWorking copy : aef4df99ea11 (no description set)\nThe working copy is clean\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 checkout-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 checkout
so you can easily review your adjustments with jj diff
before running jj squash
.
You're probably familiar with git log
. Jujutsu has very similar functionality in its jj log
command:
$ jj log\n@ mpqrykypylvy martinvonz@google.com 2023-02-12 15:00:22.000 -08:00 aef4df99ea11\n\u2502 (empty) (no description set)\n\u25c9 kntqzsqtnspv martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19dac36\n\u2502 Say goodbye\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\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. \"mpqrykypylvy\" 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 -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, branches()
refers to all commits pointed to by branches. We can combine expressions with |
for union, &
for intersection and ~
for difference. For example:
$ jj log -r '@ | root() | branches()'\n@ mpqrykypylvy martinvonz@google.com 2023-02-12 15:00:22.000 -08:00 aef4df99ea11\n\u2577 (empty) (no description set)\n\u2577 \u25c9 kowxouwzwxmv octocat@nowhere.com 2014-06-10 15:22:26.000 -07:00 test b3cbd5bbd7e8\n\u256d\u2500\u256f Create CONTRIBUTING.md\n\u2502 \u25c9 tpstlustrvsn support+octocat@github.com 2018-05-10 12:55:19.000 -05:00 octocat-patch-1 b1b3f9723831\n\u251c\u2500\u256f sentence case\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\n\u2577 (empty) Merge pull request #6 from Spaceghost/patch-1\n\u25c9 zzzzzzzzzzzz 1970-01-01 00:00:00.000 +00:00 000000000000\n(empty) (no description set)\n
The 000000000000
commit (change ID zzzzzzzzzzzz
) 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). There are also a few more functions, such as heads(<set>)
, which filters out revisions in the input set if they're ancestors of other revisions in the set.
Now let's see how Jujutsu deals with merge conflicts. We'll start by making some commits:
# Start creating a chain of commits off of the `master` branch\n$ jj new master -m A; echo a > file1\nWorking copy now at: 00a2aeed556a A\nAdded 0 files, modified 1 files, removed 0 files\n$ jj new -m B1; echo b1 > file1\nWorking copy now at: 967d9f9fd288 B1\n$ jj new -m B2; echo b2 > file1\nWorking copy now at: 8ebeaffa332b B2\n$ jj new -m C; echo c > file2\nWorking copy now at: 62a3c6d315cd C\n$ jj log\n@ qzvqqupxlkot martinvonz@google.com 2023-02-12 15:07:41.946 -08:00 2370ddf3fa39\n\u2502 C\n\u25c9 puqltuttrvzp martinvonz@google.com 2023-02-12 15:07:33.000 -08:00 daa6ffd5a09a\n\u2502 B2\n\u25c9 ovknlmrokpkl martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6bd0b4\n\u2502 B1\n\u25c9 nuvyytnqlquo martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f097aa9\n\u2502 A\n\u2502 \u25c9 kntqzsqtnspv martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19dac36\n\u251c\u2500\u256f Say goodbye\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\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:
$ jj rebase -s puqltuttrvzp -d nuvyytnqlquo\nRebased 2 commits\nWorking copy now at: 1978b53430cd C\nAdded 0 files, modified 1 files, removed 0 files\n$ jj log\n@ qzvqqupxlkot martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 1978b53430cd conflict\n\u2502 C\n\u25c9 puqltuttrvzp martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 f7fb5943ee41 conflict\n\u2502 B2\n\u2502 \u25c9 ovknlmrokpkl martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6bd0b4\n\u251c\u2500\u256f B1\n\u25c9 nuvyytnqlquo martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f097aa9\n\u2502 A\n\u2502 \u25c9 kntqzsqtnspv martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19dac36\n\u251c\u2500\u256f Say goodbye\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\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 jj log
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 puqltuttrvzp # Replace the ID by what you have for B2\nWorking copy now at: c7068d1c23fd (no description set)\nAdded 0 files, modified 0 files, removed 1 files\n$ jj st\nParent commit: f7fb5943ee41 B2\nWorking copy : c7068d1c23fd (no description set)\nThe working copy is clean\nThere are unresolved conflicts at these paths:\nfile1 2-sided conflict\n$ cat file1\n<<<<<<<\n%%%%%%%\n-b1\n+a\n+++++++\nb2\n>>>>>>>\n$ echo resolved > file1\n$ jj squash\nRebased 1 descendant commits\nWorking copy now at: e3c279cc2043 (no description set)\n$ jj log\n@ ntxxqymrlvxu martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 e3c279cc2043\n\u2502 (empty) (no description set)\n\u2502 \u25c9 qzvqqupxlkot martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 b9da9d28b26b\n\u251c\u2500\u256f C\n\u25c9 puqltuttrvzp martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 2c7a658e2586\n\u2502 B2\n\u2502 \u25c9 ovknlmrokpkl martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6bd0b4\n\u251c\u2500\u256f B1\n\u25c9 nuvyytnqlquo martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f097aa9\n\u2502 A\n\u2502 \u25c9 kntqzsqtnspv martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19dac36\n\u251c\u2500\u256f Say goodbye\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\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 ovknlmrokpkl
. 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 2023-02-12 19:34:09.549 -08:00 - 2023-02-12 19:34:09.552 -08:00\n\u2502 squash commit 63874fe6c4fba405ffc38b0dd926f03b715cf7ef\n\u2502 args: jj squash\n\u25c9 6fc1873c1180 martinvonz@vonz.svl.corp.google.com 2023-02-12 19:34:09.548 -08:00 - 2023-02-12 19:34:09.549 -08:00\n\u2502 snapshot working copy\n\u25c9 ed91f7bcc1fb martinvonz@vonz.svl.corp.google.com 2023-02-12 19:32:46.007 -08:00 - 2023-02-12 19:32:46.008 -08:00\n\u2502 new empty commit\n\u2502 args: jj new puqltuttrvzp\n\u25c9 367400773f87 martinvonz@vonz.svl.corp.google.com 2023-02-12 15:08:33.917 -08:00 - 2023-02-12 15:08:33.920 -08:00\n\u2502 rebase commit daa6ffd5a09a8a7d09a65796194e69b7ed0a566d and descendants\n\u2502 args: jj rebase -s puqltuttrvzp -d nuvyytnqlquo\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\nWorking copy now at: 63874fe6c4fb (no description set)\n$ jj log\n@ zxoosnnpvvpn martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 63874fe6c4fb\n\u2502 (no description set)\n\u2502 \u25c9 qzvqqupxlkot martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 1978b53430cd conflict\n\u251c\u2500\u256f C\n\u25c9 puqltuttrvzp martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 f7fb5943ee41 conflict\n\u2502 B2\n\u2502 \u25c9 ovknlmrokpkl martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6bd0b4\n\u251c\u2500\u256f B1\n\u25c9 nuvyytnqlquo martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f097aa9\n\u2502 A\n\u2502 \u25c9 kntqzsqtnspv martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19dac36\n\u251c\u2500\u256f Say goodbye\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\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. These commands assume that you have meld
installed. If you prefer a terminal-based diff editor, you can configure scm-diff-editor
instead.
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: f94e49cf2547 abc\nAdded 0 files, modified 0 files, removed 1 files\n$ jj new -m ABC; printf 'A\\nB\\nc\\n' > file\nWorking copy now at: 6f30cd1fb351 ABC\n$ jj new -m ABCD; printf 'A\\nB\\nC\\nD\\n' > file\nWorking copy now at: a67491542e10 ABCD\n$ jj log -r master::@\n@ mrxqplykzpkw martinvonz@google.com 2023-02-12 19:38:21.000 -08:00 b98c607bf87f\n\u2502 ABCD\n\u25c9 kwtuwqnmqyqp martinvonz@google.com 2023-02-12 19:38:12.000 -08:00 30aecc0871ea\n\u2502 ABC\n\u25c9 ztqrpvnwqqnq martinvonz@google.com 2023-02-12 19:38:03.000 -08:00 510022615871\n\u2502 abc\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\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 -i
(short for jj squash --interactive
) 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 'meld'; you can change this by setting ui.diff-editor\nWorking copy now at: 52a6c7fda1e3 ABCD\n
That will bring up Meld with a diff of the changes in the \"ABCD\" commit. Modify the right side of the diff to have the desired end state in \"ABC\" by removing the \"D\" line. Then save the changes and close Meld. 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 'meld'; you can change this by setting ui.diff-editor\nCreated 70985eaa924f ABC\nRebased 1 descendant commits\nWorking copy now at: 1c72cd50525d ABCD\nAdded 0 files, modified 1 files, removed 0 files\n
When Meld starts, edit the right side by e.g. adding something to the first line. Then save the changes and close Meld. 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. Other commands for rewriting contents of existing commits are jj split
, jj unsquash -i
and jj move -i
. Now that you've seen how jj squash -i
and jj diffedit
work, you can hopefully figure out how those work (with the help of the instructions in the diff).
The working copy is where the current working-copy commit's files are written so you can interact with them. It 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. 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. To untrack a file while keeping it in the working copy, first make sure it's ignored and then run jj untrack <path>
.
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
. However, $GIT_DIR/info/exclude
or equivalent way (maybe .jj/gitignore
) of specifying per-clone ignores is not yet supported.
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).
When you modify workspace A's working-copy commit from workspace B, workspace A's working copy will become stale. By \"stale\", we mean that the files in the working copy don't match the desired commit indicated by the @
symbol in jj log
. When that happens, use jj workspace update-stale
to update the files in the working copy.
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_branches(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 revelant 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 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: - CPU and memory available on the machine running the commands. 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. - External resources that are not immediately known to jj. For example, commands run in parallel may wish to limit the total number of connections to a server. We might choose to defer any handling of this to the implementation of the command being invoked, instead of trying to communicate that information to jj.
The base command of any jj command should be usable. By default jj run
works on the @
the current working copy. * --command, explicit name of the first argument * -x, for git compatibility (may alias another command) * -j, --jobs, the amount of parallelism to use * -k, --keep-going, continue on failure (may alias another command) * --show, display the diff for an affected revision * --dry-run, do the command execution without doing any work, logging all intended files and arguments * --rebase, rebase all parents on the consulitng diff (may alias another command) * --reparent, change the parent of an effected revision to the new change (may alias another command) * --clean, remove existing workspaces and remove the ignored files * --readonly, ignore changes across multiple run invocations * --error-strategy=continue|stop|fatal
, see Dealing with failure
jj 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
. @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-branch
config can mitigate this problem, but we'll get locally-deleted branches instead.
The goal of this plan is to implement * proper support for tracking/non-tracking remote branches * logically consistent data model for importing/exporting Git refs
"},{"location":"design/tracking-branches/#current-data-model-as-of-jj-080","title":"Current data model (as of jj 0.8.0)","text":"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: 1. jj branch forget
2. 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 * naturally support remote branches which have no local counterparts * deduplicate 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-branch
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) {\nif remote == \"git\" {\nState::Tracking\n} else if config[\"git.auto-local-branch\"] {\nState::Tracking\n} else {\nState::New\n}\n}\n
A branch target to be merged is calculated based on the state
.
fn target_in_merge_context(known_target, state) {\nmatch state {\nState::New => RefTarget::absent(),\nState::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-branch
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
1. Fetches remote changes to the backing Git repo. 2. Import changes only for remotes[remote].branches[glob]
(see below)
.tags
?jj git import
1. Copies git_refs
from the backing Git repo. 2. Calculates diff from the known 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]
3. Merges diff in local branches
and tags
if state
is tracking
.target
is absent
, the default state
should be calculated. This also applies to previously-forgotten branches. 4. Updates remotes
reflecting the import. 5. Abandons commits that are no longer referenced.jj git push
1. Calculates diff from the known 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
) 2. Pushes diff to the remote Git repo (as well as remote tracking branches in the backing Git repo.) 3. Updates remotes[remote]
and git_refs
reflecting the push.jj git export
1. Copies local 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
) 2. Calculates diff from the known git_refs
to the new remotes[remote]
. 3. Applies diff to the backing Git repo. 4. Updates 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.
"},{"location":"design/tracking-branches/#initclone","title":"init/clone","text":"jj init
git.auto_local_branch
config.If !git.auto_local_branch
, no tracking
state will be set.
jj git clone
git.auto_local_branch
config.git.auto_local_branch
config. (Because local branch is created for the default remote branch, it makes sense to track.)jj branch set {name}
1. Sets local branches[name]
entry.jj branch delete {name}
1. Removes local branches[name]
entry.jj branch forget {name}
1. Removes local branches[name]
entry if exists. 2. Removes remotes[remote].branches[name]
entries if exist. TODO: maybe better to not remove non-tracking remote branches?jj branch track {name}@{remote}
(new command) 1. Merges [local, remote] - [absent]
in local branch.remotes[remote].branches[name].state = tracking
.jj branch untrack {name}@{remote}
(new command) 1. Sets remotes[remote].branches[name].state = new
.jj branch list
Note: desired behavior of jj branch forget
is to * discard both local and remote branches (without actually removing branches at remotes) * not abandon commits which belongs to those branches (even if the branch is removed at a remote)
state = new|tracking
based on git.auto_local_branch
2. If new state
is tracking
, merges [absent, new_remote] - [absent]
(i.e. creates local branch with new_remote
target) 3. Sets remotes[remote].branches[name].state
[local, new_remote] - [known_remote]
state = new|tracking
based on git.auto_local_branch
2. If new state
is tracking
, merges [local, new_remote] - [absent]
3. Sets remotes[remote].branches[name].state
[local, absent] - [known_remote]
2. Removes remotes[remote].branches[name]
(target
becomes absent
) (i.e. the remote branch is no longer tracked) 3. Abandons commits in the deleted branchstate = new|tracking
based on git.auto_local_branch
2. Noop anyway since [local, absent] - [absent]
-> local
state = new|tracking
based on git.auto_local_branch
2. If new state
is tracking
, merges [absent, new_remote] - [absent]
-> new_remote
3. Sets remotes[remote].branches[name].state
state = new
[local, absent] - [absent]
-> local
2. Sets remotes[remote].branches[name].target = local
, .state = tracking
[local, remote] - [absent]
local
moved backwards or sideways 2. Sets remotes[remote].branches[name].target = local
, .state = tracking
[local, remote] - [remote]
-> local
local
moved backwards or sideways, and if remote
is out of sync 2. Sets remotes[remote].branches[name].target = local
[absent, remote] - [remote]
-> absent
remote
is out of sync? 2. Removes 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
2. Exports [local, absent] - [absent]
-> local
[local, git] - [absent]
-> failremotes[\"git\"].branches[name].target = local
2. Exports [local, git] - [git]
-> local
remotes[\"git\"].branches[name]
2. Exports [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.git.auto_local_branch = false
by default to help Git interop?tracking
remotes?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;\n
"},{"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 branches, etc.). If conflicting changes were made, they will appear as conflicts. For example, if a branch 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 branch pointers. Note that, unlike in pure Git, losing a branch 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 concurrent 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, branches, 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 concurrent 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 concurrent 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 concurrent 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 branch 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 branches (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 branch. 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 branch 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 within the tree object as a special conflict object (not a file object with conflict markers). Conflicts are stored as a lists of states to add and another list of states to remove. A \"state\" here can be a normal file, a symlink, or a tree. These two lists together can be a viewed as a simple algebraic expression of positive and negative terms. The order of terms is undefined.
For example, a regular 3-way merge between B and C, with A as base, is B+C-A
({ removes=[A], adds=[B,C] }
). A modify/remove conflict is B-A
. An add/add conflict is B+C
. An octopus merge of N commits usually has N positive terms and N-1 negative terms. A non-conflict state A is equivalent to a conflict state containing just the term A
. An empty expression indicates absence of any content at that path. A conflict can thus encode a superset of what can be encoded in a regular path state.
Remember that a 3-way merge can be written B+C-A
. If one of those states is itself a conflict, then we simply insert the conflict expression there. Then we simplify by removing canceling terms.
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 branch to move forward when you commit. However, Jujutsu does not have a concept of a \"current branch\".
To move branches, use jj branch set
.
jj git push --all
says \"Nothing changed\" instead of pushing it. What do I do?","text":"jj git push --all
pushes all branches, not all revisions. You have two options:
jj git push --change
will automatically create a branch and push it.jj branch
commands to create or move a branch 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/#can-i-prevent-jujutsu-from-recording-my-unfinished-work-im-not-ready-to-commit-it","title":"Can I prevent Jujutsu from recording my unfinished work? I'm not ready to commit it.","text":"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.
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 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 $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).
You can find more details on gitignore
files here.
In general, you should separate out the changes to their own commit (using e.g. jj split
). After that, one possible workflow is to rebase your pending PRs on top of the commit with the local changes. Then, just before pushing to a remote, use jj rebase -s child_of_commit_with_local_changes -d main
to move the PRs back on top of main
.
If you have several PRs, you can try jj rebase -s all:commit_with_local_changes+ -d main
(note the +
) to move them all at once.
An alternative workflow would be to rebase the commit with local changes on top of the PR you're working on and then do jj new commit_with_local_changes
. You'll then need to use jj new --before
to create new commits and jj move --to
to move new changes into the correct commits.
Use jj obslog -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.
Branches are named pointers to revisions (just like they are in Git). You can move them without affecting the target revision's identity. Branches automatically move when revisions are rewritten (e.g. by jj rebase
). You can pass a branch's name to commands that want a revision as argument. For example, jj co main
will check out the revision pointed to by the \"main\" branch. Use jj branch list
to list branches and jj branch
to create, move, or delete branches. There is currently no concept of an active/current/checked-out branch.
Jujutsu identifies a branch by its name across remotes (this is unlike Git and more like Mercurial's \"bookmarks\"). For example, a branch called \"main\" in your local repo is considered the same branch as a branch by the same name on a remote. When you pull from a remote (currently only via jj git fetch
), any branches from the remote will be imported as branches in your local repo.
Jujutsu also records the last seen position on each remote (just like Git's remote-tracking branches). You can refer to these with <branch name>@<remote name>
, such as jj new main@origin
. Most commands don't show the remote branch if it has the same target as the local branch. The local branch (without @<remote name>
) is considered the branch's desired target. Consequently, if you want to update a branch on a remote, you first update the branch locally and then push the update to the remote. If a local branch also exists on some remote but points to a different target there, jj log
will show the branch name with an asterisk suffix (e.g. main*
). That is meant to remind you that you may want to push the branch to some remote.
When you pull from a remote, any changes compared to the current record of the remote's state will be propagated to the local branch. Let's say you run jj git fetch --remote origin
and the remote's \"main\" branch has moved so its target is now ahead of the local record in main@origin
. That will update main@origin
to the new target. It will also apply the change to the local branch main
. If the local target had also moved compared to main@origin
(probably because you had run jj branch set main
), then the two updates will be merged. If one is ahead of the other, then that target will be the new target. Otherwise, the local branch will be conflicted (see next section for details).
Branches can end up in a conflicted state. When that happens, jj status
will include information about the conflicted branches (and instructions for how to mitigate it). jj branch list
will have details. jj log
will show the branch name with a question mark suffix (e.g. main?
) on each of the conflicted branch's potential target revisions. Using the branch name to look up a revision will resolve to all potential targets. That means that jj co main
will error out, complaining that the revset resolved to multiple revisions.
Both local branches (e.g. main
) and the remote branch (e.g. main@origin
) can have conflicts. Both can end up in that state if concurrent operations were run in the repo. The local branch more typically becomes conflicted because it was updated both locally and on a remote.
To resolve a conflicted state in a local branch (e.g. main
), you can move the branch to the desired target with jj branch
. You may want to first either merge the conflicted targets with jj merge
, or you may want to rebase one side on top of the other with jj rebase
.
To resolve a conflicted state in a remote branch (e.g. main@origin
), simply pull from the remote (e.g. jj git fetch
). The conflict resolution will also propagate to the local branch (which was presumably also conflicted).
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
"},{"location":"code-of-conduct/#our-standards","title":"Our Standards","text":"Examples of behavior that contributes to creating a positive environment include:
Examples of unacceptable behavior by participants include:
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers 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, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
"},{"location":"code-of-conduct/#scope","title":"Scope","text":"This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
This Code of Conduct also applies outside the project spaces when the Project Steward has a reasonable belief that an individual's behavior may have a negative impact on the project or its community.
"},{"location":"code-of-conduct/#conflict-resolution","title":"Conflict Resolution","text":"We do not believe that all conflict is bad; healthy debate and disagreement often yield positive results. However, it is never okay to be disrespectful or to engage in behavior that violates the project\u2019s code of conduct.
If you see someone violating the code of conduct, you are encouraged to address the behavior directly with those involved. Many issues can be resolved quickly and easily, and this gives people more control over the outcome of their dispute. If you are unable to resolve the matter for any reason, or if the behavior is threatening or harassing, report it. We are dedicated to providing an environment where participants feel welcome and safe.
Reports should be directed to [PROJECT STEWARD NAME(s) AND EMAIL(s)], the Project Steward(s) for [PROJECT NAME]. It is the Project Steward\u2019s duty to receive and address reported violations of the code of conduct. They will then work with a committee consisting of representatives from the Open Source Programs Office and the Google Open Source Strategy team. If for any reason you are uncomfortable reaching out to the Project Steward, please email opensource@google.com.
We will investigate every complaint, but you may not receive a direct response. We will use our discretion in determining when and how to follow up on reported incidents, which may range from not taking action to permanent expulsion from the project and project-sponsored spaces. We will notify the accused of the report and provide them an opportunity to discuss it before any action is taken. The identity of the reporter will be omitted from the details of the report supplied to the accused. In potentially harmful situations, such as ongoing harassment or threats to anyone's safety, we may take action without notice.
"},{"location":"code-of-conduct/#attribution","title":"Attribution","text":"This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
"},{"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":"The config settings are loaded from the following locations. Less common ways to specify jj
config settings are discussed in a later section.
.jj/repo/config.toml
(per-repository)See the TOML site and the syntax guide for a description of the syntax.
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
and auto
(default: auto
). auto
will use color only when writing to a terminal.
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).
If you use a string value for a color, as in the example 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 = \"red\", 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\"\n
"},{"location":"config/#default-description","title":"Default description","text":"The value of the ui.default-description
setting will be used to prepopulate the editor when describing changes with an empty description. This could be a useful reminder to fill in things like BUG=, TESTED= etc.
ui.default-description = \"\\n\\nTESTED=TODO\"\n
"},{"location":"config/#diff-format","title":"Diff format","text":"# Possible values: \"color-words\" (default), \"git\", \"summary\"\nui.diff.format = \"git\"\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.
# Use Difftastic by default\nui.diff.tool = [\"difft\", \"--color=always\", \"$left\", \"$right\"]\n# Use tool named \"<name>\" (see below)\nui.diff.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.You can configure the set of immutable commits via revset-aliases.\"immutable_heads()\"
. The default set of immutable heads is trunk() | tags()
. For example, to prevent rewriting commits on main@origin
and commits authored by other users:
# The `main.. &` bit is an optimization to scan for non-`mine()` commits only\n# among commits that are not in `main`.\nrevset-aliases.\"immutable_heads()\" = \"main@origin | (main@origin.. & ~mine())\"\n
Ancestors of the configured set are also immutable. The root commit is always immutable even if the set is empty.
"},{"location":"config/#default-revisions-to-log","title":"Default revisions to log","text":"You can configure the revisions jj log
without -r
should show.
# Show commits that are not in `main@origin`\nrevsets.log = \"main@origin..\"\n
"},{"location":"config/#graph-style","title":"Graph style","text":"# Possible values: \"curved\" (default), \"square\", \"ascii\", \"ascii-large\",\n# \"legacy\"\nui.graph.style = \"square\"\n
"},{"location":"config/#wrap-log-content","title":"Wrap log content","text":"If enabled, log
/obslog
/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 branch\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 (default)\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/#pager","title":"Pager","text":"Windows users: Note that pagination is disabled by default on Windows for now (#2040).
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.
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
"},{"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) branch\n# compared to the `main` branch\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 = \"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 tool used for editing diffs (e.g. jj split
, jj amend -i
). The default is the special value :builtin
, which launches a TUI tool to edit the diff in your terminal.
jj
makes the following substitutions:
$left
and $right
are replaced with the paths to the left and right directories to diff respectively.If no arguments are specified, [\"$left\", \"$right\"]
are set by default.
For example:
# Use merge-tools.kdiff3.edit-args\nui.diff-editor = \"kdiff3\"\n# Specify edit-args inline\nui.diff-editor = [\"kdiff3\", \"--merge\", \"$left\", \"$right\"]\n
If ui.diff-editor
consists of a single word, e.g. \"kdiff3\"
, the arguments will be read from the following config keys.
# merge-tools.kdiff3.program = \"kdiff3\" # Defaults to the name of the tool if not specified\nmerge-tools.kdiff3.edit-args = [\n\"--merge\", \"--cs\", \"CreateBakFiles=0\", \"$left\", \"$right\"]\n
"},{"location":"config/#experimental-3-pane-diff-editing","title":"Experimental 3-pane diff editing","text":"The special \"meld-3\"
diff editor sets up Meld to show 3 panes: the sides of 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
.
To configure other diff editors, 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 these instructions 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 \"kdiff3\" or \"vimdiff\"\n# Specify merge-args inline\nui.merge-editor = [\"meld\", \"$left\", \"$base\", \"$right\", \"-o\", \"$output\"]\n
The \"meld\", \"kdiff3\", and \"vimdiff\" tools can be used out of the box, as long as they are installed.
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.
By default, when jj
imports a new remote-tracking branch from Git, it also creates a local branch with the same name. In some repositories, this may be undesirable, e.g.:
You can disable this behavior by setting git.auto-local-branch
like so,
git.auto-local-branch = false\n
This setting is applied only to new remote branches. Existing remote branches can be tracked individually by using jj branch track
/untrack
commands.
# import feature1 branch and start tracking it\njj branch track feature1@origin\n# delete local gh-pages branch and stop tracking it\njj branch delete gh-pages\njj branch untrack gh-pages@upstream\n
"},{"location":"config/#prefix-for-generated-branches-on-push","title":"Prefix for generated branches on push","text":"jj git push --change
generates branch names with a prefix of \"push-\" by default. You can pick a different prefix by setting git.push-branch-prefix
. For example:
git.push-branch-prefix = \"martinvonz/push-\"\n
"},{"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.
To configure the Watchman filesystem monitor, set core.fsmonitor = \"watchman\"
. Ensure that you have installed the Watchman executable on your system.
Debugging commands are available under jj debug watchman
.
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
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":"Like Pijul and Darcs but 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 ||||||| right\n APPLE\n GRAPE\n ORANGE\n >>>>>>>\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 the 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.
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 checking out the commit (jj checkout/new <commit>
) and then squash in the changes when you're done (jj squash
). jj git push
will automatically force-push the branch.
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.
"},{"location":"contributing/#community-guidelines","title":"Community Guidelines","text":"This project follows Google's Open Source Community Guidelines.
"},{"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.71 # 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.71 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.71.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
.
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
installed. If your distribution packages poetry
, something like apt install python3-poetry
is likely the best way to install it. Otherwise, you can download Python from https://python.org or follow the Python installation instructions. Finally, follow the Poetry 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
If you get requests to \"unlock a keyring\" or error messages about failing to do so, this is a known poetry
bug. The workaround is 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
branch 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
branch (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/'\\\nprerelease 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
branch, and push the gh-pages
branch 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/'\\\nv1.33.1 latest --push\n
(Optional) When you are done, you may want to reset the gh-branches
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 branch set gh-pages -r gh-pages@upstream\njj git push --remote origin --branch gh-pages\n
If you want to preserve some of the changes you made, you can do jj branch 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
branch 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
branch of https://martinvonz.github.com/jj (instead of the usual main
branch).
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":"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. 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 branch by that name in your local repo as well. 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 init --git
(without --git
, you get a native Jujutsu repo, which is slow and whose format will change) git init
Clone an existing repo jj git clone <source> <destination>
(there is no support for cloning non-Git repos yet) git clone <source> <destination>
Update the local repo with all 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 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 branch from the local repo jj git push --branch <branch name> [--remote <remote>]
(there is no support for pushing from non-Git repos yet) git push <remote> <branch name>
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 commits jj log
git log --oneline --graph --decorate
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\") 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
(first make sure that nothing is staged) Temporarily put away the current change Not needed git stash
Start working on a new change based on the <main> branch jj co main
git switch -c topic main
or git checkout -b topic main
(may need to stash or commit first) Move branch A onto 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 branch>
(may need to rebase other descendant branches separately) Reorder changes from A-B-C-D to A-C-B-D jj rebase -r C -d A; rebase -s B -d C
(pass change IDs, not commit IDs, to not have to look up commit ID of rewritten C) 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 move --to 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 move -i --from X --to 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>
List branches jj branch list
git branch
Create a branch jj branch create <name> -r <revision>
git branch <name> <revision>
Move a branch forward jj branch set <name> -r <revision>
git branch -f <name> <revision>
Move a branch backward or sideways jj branch set <name> -r <revision> --allow-backwards
git branch -f <name> <revision>
Delete a branch jj branch 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"},{"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 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 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.
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 init --git-repo=.
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 git pack-refs --all
to speed up the import.
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 st\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) is stored outside of the Git repo (currently in .jj/store/extra/
).
Paths with conflicts cannot be represented in Git. They appear as files with a .jjconflict
suffix in the Git repo. They contain a JSON representation with information about the conflict. They are not meant to be human-readable.
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 December 2022 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 branch when you need to push the stack to a remote. There are two primary workflows, using a generated branch name or naming a branch.
"},{"location":"github/#using-a-generated-branch-name","title":"Using a generated branch name","text":"In this example we're letting Jujutsu auto-create a branch.
# Start a new commit off of the default branch.\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 branch name and push that to GitHub\n$ jj git push -c @-\n
"},{"location":"github/#using-a-named-branch","title":"Using a named branch","text":"In this example, we create a branch named bar
and then push it to the remote.
# Start a new commit off of the default branch.\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 branch so we can push it to GitHub\n$ jj branch create bar -r @- # create a branch `bar` that now contains the previous two commits.\n# Push the branch to GitHub (pushes only `bar`)\n$ jj git push\n
While it's possible to create a branch and commit on top of it in a Git-like manner, you will then need to move the branch 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 December 2022, Jujutsu has no equivalent to a git pull
command. Until such a command is added, you need to use jj git fetch
followed by a jj rebase -d $main_branch
to update your changes.
After doing jj init --git-repo=.
, git will be in a detached HEAD state, which is unusual, as git mainly works with branches. In a co-located repository, jj
isn't the source of truth. But Jujutsu allows an incremental migration, as jj commit
updates the HEAD of the git repository.
$ nvim docs/tutorial.md\n$ # Do some more work.\n$ jj commit -m \"Update tutorial\"\n$ jj branch create doc-update\n$ # Move the previous revision to doc-update.\n$ jj branch set 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 branches, you just can generate one for a change. As Jujutsu is able to create a branch for a revision.
$ # Do your work\n$ jj commit\n$ # Push change \"mw\", letting Jujutsu automatically create a branch called \"push-mwmpwkwknuz\"\n$ jj git push --change mw
"},{"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 branch1. 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` branch from above.\n$ jj new your-feature\n$ # Address the comments, by updating the code\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 branch to point to the new commit.\n$ jj branch 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` branch from above.\n$ jj new your-feature\n$ # Address the comments, by updating the code\n$ jj diff\n$ # Give the fix a description.\n$ jj describe -m 'address pr comments'\n$ # Update the branch to point to the current commit.\n$ jj branch 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 reviews 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\n$ # Review the changes\n$ jj diff\n$ # Squash the changes into the parent commit\n$ jj squash\n$ # Push the updated branch to the remote. Jujutsu automatically makes it a force push\n$ jj git push --branch your-feature\n
The hyphen after your-feature
comes from revset syntax.
By default jj git clone
and jj git fetch
clone all active branches from the remote. This means that if you want to iterate or test another contributor's branch you can jj new <branchname>
onto it.
If your remote has a large amount of old, inactive branches or this feature is undesirable, set git.auto-local-branch = false
in the config file.
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 branches, which aren't on the main branch nor on any remote jj log -r 'branches() & ~(main | remote_branches())'
Log all revisions which you authored, across all branches which aren't on any remote jj log -r 'mine() & branches() & ~remote_branches()'
Log all remote branches, which you authored or committed to jj log -r 'remote_branches() & (mine() | committer(your@email.com))'
Log all descendants of the current working copy, which aren't on a remote jj log -r '::@ & ~remote_branches()'
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. In this case, 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\".
This is a GitHub Style review, as GitHub currently only is able to compare branches.\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 have any named branches 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/#branch","title":"Branch","text":"A branch is a named pointer to a commit. They automatically follow the commit if it gets rewritten. Branches are sometimes called \"named branches\" to distinguish them from anonymous branches, but note that they are more similar to Git's branches than to Mercurial's named branches. See here for details.
"},{"location":"glossary/#change","title":"Change","text":"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 branches. For example, if you moved a branch locally, and it was also moved on the remote, then the branch will be in a conflicted state after you pull from the remote. See here for details.
Similar to a branch 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 branch pointing to them) are visible at a given operation. Note that this is quite different from Git's HEAD.
A snapshot of the visible commits and branches 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/#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.
A view is a snapshot of branches 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/#linux","title":"Linux","text":""},{"location":"install-and-setup/#build-using-cargo","title":"Build usingcargo
","text":"First make sure that you have the libssl-dev
, openssl
, pkg-config
, and build-essential
packages 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/#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/#mac","title":"Mac","text":""},{"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/#from-source","title":"From Source","text":"You may 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/#windows","title":"Windows","text":"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 is the default\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/#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 branch, 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 as well as the -
operator (e.g. @-
) to get the parent of an operation.
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.
The commits listed by jj log
without arguments are called \"visible commits\". Other commits are only included if you explicitly mention them (e.g. by commit ID or a Git ref pointing to them).
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 branch.
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 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 & y
: Revisions that are in both x
and y
.x | y
: Revisions that are in either x
or y
(or both).x ~ y
: Revisions that are in x
but not in y
.~x
: Revisions that are not in x
.x-
: Parents of x
.x+
: Children of x
.::x
: Ancestors of x
, including the commits in x
itself.x::
: Descendants of x
, including the commits in x
itself.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
.::
: All visible commits in the repo. Equivalent to all()
.:x
, x:
, and x:y
: Deprecated versions of ::x
, x::
, and x::y
We plan to delete them in jj 0.15+.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)...x
: Ancestors of x
, including the commits in x
itself, but excluding the root commit. Equivalent to ::x ~ root()
.x..
: Revisions that are not ancestors of x
...
: All visible commits in the repo, but excluding the root commit. Equivalent to ~root()
.You can use parentheses to control evaluation order, such as (x & y) | z
or x & (y | z)
.
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)
: Same as x::
.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.branches([pattern])
: All local branch targets. If pattern
is specified, branches whose name contains the given string are selected. For example, branches(push)
would match the branches push-123
and repushed
but not the branch main
. If a branch is in a conflicted state, all its possible targets are included.remote_branches([branch_pattern[, [remote=]remote_pattern]])
: All remote branch targets across all remotes. If just the branch_pattern
is specified, branches whose name contains the given string across all remotes are selected. If both branch_pattern
and remote_pattern
are specified, the selection is further restricted to just the remotes whose name contains remote_pattern
. For example, remote_branches(push, ri)
would match the branches push-123@origin
and repushed@private
but not push-123@upstream
or main@origin
or main@upstream
. If a branch is in a conflicted state, all its possible targets are included.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. Equivalent to present(HEAD@git)
.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 with the given string in their description.author(pattern)
: Commits with the given string in the author's name or email.mine()
: Commits where the author's email matches the email of the current user.committer(pattern)
: Commits with the given string in the committer's name or email.empty()
: Commits modifying no files. This also includes merges()
without user modifications and root()
.file(pattern..)
: Commits modifying the paths specified by the pattern..
. 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, file(foo)
will match files foo
, foo/bar
, foo/bar/baz
, but not file foobar
.conflict()
: 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 branch name.)Functions that perform string matching support the following pattern syntax.
\"string\"
, 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
.New symbols and functions can be defined in the config file, by using any combination of the predefined symbols/functions and other aliases.
For example:
[revset-aliases]\n'mine' = 'author(martinvonz)'\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 branch of the remote named origin
or upstream
. The branches main
, master
, and trunk
are tried. If more than one potential trunk commit exists, the newest one is chosen. If none of the branches exist, the revset evaluates to root()
.You can override this as appropriate. If you do, make sure it always resolves to exactly one commit. For example:
[revset-aliases]\n'trunk()' = 'your-branch@your-remote'\n
"},{"location":"revsets/#examples","title":"Examples","text":"Show the parent(s) of the working-copy commit (like git log -1 HEAD
):
jj log -r @-\n
Show commits not on any remote branch:
jj log -r 'remote_branches()..'\n
Show commits not on origin
(if you have other remotes like fork
):
jj log -r 'remote_branches(remote=origin)..'\n
Show all ancestors of the working copy (almost like plain git log
)
jj log -r ::@\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() | branches()'\n
Show local commits leading up to the working copy, as well as descendants of those commits:
jj log -r '(remote_branches()..@)::'\n
Show commits authored by \"martinvonz\" and containing the word \"reset\" in the description:
jj log -r 'author(martinvonz) & description(reset)'\n
"},{"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 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.
sl shelve
.<<<<<<<
etc.). This also has several advantages: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.
"},{"location":"templates/#commit-keywords","title":"Commit keywords","text":"The following keywords can be used in jj log
/jj obslog
templates.
description: String
change_id: ChangeId
commit_id: CommitId
parents: List<Commit>
author: Signature
committer: Signature
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.branches: String
tags: String
git_refs: String
git_head: String
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).conflict: Boolean
: True if the commit contains merge conflicts.empty: Boolean
: True if the commit modifies no files.root: Boolean
: True if the commit is the root commit.The following keywords can be used in jj op log
templates.
current_operation: Boolean
description: String
id: OperationId
tags: String
time: TimestampRange
user: String
The following operators are supported.
x.f()
: Method call.x ++ y
: Concatenate x
and y
templates.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
.label(label: Template, content: Template) -> Template
: Apply label to the content. The label
is evaluated as a space-separated string.if(condition: Boolean, then: Template[, else: Template]) -> Template
: Conditionally evaluate then
/else
template content.concat(content: Template...) -> Template
: Same as content_1 ++ ... ++ content_n
.separate(separator: Template, content: Template...) -> Template
: Insert separator between non-empty contents.No methods are defined. Can be constructed with false
or true
literal.
This type cannot be printed. All commit keywords are accessible as 0-argument methods.
"},{"location":"templates/#commitid-changeid-type","title":"CommitId / ChangeId type","text":"The following methods are defined.
.short([len: Integer]) -> String
.shortest([min_len: Integer]) -> ShortestIdPrefix
: Shortest unique prefix.No methods are defined.
"},{"location":"templates/#list-type","title":"List type","text":"The following methods are defined.
.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
The following methods are defined.
.short([len: Integer]) -> String
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
A string can be implicitly converted to Boolean
. The following methods are defined.
.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. Negative values count from the end.String literals must be surrounded by double quotes (\"
). The following escape sequences starting with a backslash have their usual meaning: \\\"
, \\\\
, \\n
, \\r
, \\t
, \\0
. 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.
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.The following methods are defined.
.start() -> Timestamp
.end() -> Timestamp
.duration() -> String
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.
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":"tutorial/","title":"Tutorial","text":"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: d7439b06fbef (no description set)\nAdded 1 files, modified 0 files, removed 0 files\n$ cd Hello-World\n
Running jj st
(short forjj status
) now yields something like this:
$ jj st\nParent commit: 7fd1a60b01f9 Merge pull request #6 from Spaceghost/patch-1\nWorking copy : d7439b06fbef (no description set)\nThe working copy is clean\n
We can see from the output above that our working copy is a real commit with a commit ID (d7439b06fbef
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: e427edcfd0ba Say goodbye\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\nParent commit: 7fd1a60b01f9 Merge pull request #6 from Spaceghost/patch-1\nWorking copy : 5d39e19dac36 Say goodbye\nWorking copy changes:\nM README\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 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. For familiarity for user coming from other VCSs, there is also a jj checkout/co
command, which is practically a synonym for jj new
(you can specify a destination for jj new
as well).
So, let's say we're now done with this change, so we create a new change:
$ jj new\nWorking copy now at: aef4df99ea11 (no description set)\n$ jj st\nParent commit: 5d39e19dac36 Say goodbye\nWorking copy : aef4df99ea11 (no description set)\nThe working copy is clean\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 checkout-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 checkout
so you can easily review your adjustments with jj diff
before running jj squash
.
You're probably familiar with git log
. Jujutsu has very similar functionality in its jj log
command:
$ jj log\n@ mpqrykypylvy martinvonz@google.com 2023-02-12 15:00:22.000 -08:00 aef4df99ea11\n\u2502 (empty) (no description set)\n\u25c9 kntqzsqtnspv martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19dac36\n\u2502 Say goodbye\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\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. \"mpqrykypylvy\" 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 -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, branches()
refers to all commits pointed to by branches. We can combine expressions with |
for union, &
for intersection and ~
for difference. For example:
$ jj log -r '@ | root() | branches()'\n@ mpqrykypylvy martinvonz@google.com 2023-02-12 15:00:22.000 -08:00 aef4df99ea11\n\u2577 (empty) (no description set)\n\u2577 \u25c9 kowxouwzwxmv octocat@nowhere.com 2014-06-10 15:22:26.000 -07:00 test b3cbd5bbd7e8\n\u256d\u2500\u256f Create CONTRIBUTING.md\n\u2502 \u25c9 tpstlustrvsn support+octocat@github.com 2018-05-10 12:55:19.000 -05:00 octocat-patch-1 b1b3f9723831\n\u251c\u2500\u256f sentence case\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\n\u2577 (empty) Merge pull request #6 from Spaceghost/patch-1\n\u25c9 zzzzzzzzzzzz 1970-01-01 00:00:00.000 +00:00 000000000000\n(empty) (no description set)\n
The 000000000000
commit (change ID zzzzzzzzzzzz
) 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). There are also a few more functions, such as heads(<set>)
, which filters out revisions in the input set if they're ancestors of other revisions in the set.
Now let's see how Jujutsu deals with merge conflicts. We'll start by making some commits:
# Start creating a chain of commits off of the `master` branch\n$ jj new master -m A; echo a > file1\nWorking copy now at: 00a2aeed556a A\nAdded 0 files, modified 1 files, removed 0 files\n$ jj new -m B1; echo b1 > file1\nWorking copy now at: 967d9f9fd288 B1\n$ jj new -m B2; echo b2 > file1\nWorking copy now at: 8ebeaffa332b B2\n$ jj new -m C; echo c > file2\nWorking copy now at: 62a3c6d315cd C\n$ jj log\n@ qzvqqupxlkot martinvonz@google.com 2023-02-12 15:07:41.946 -08:00 2370ddf3fa39\n\u2502 C\n\u25c9 puqltuttrvzp martinvonz@google.com 2023-02-12 15:07:33.000 -08:00 daa6ffd5a09a\n\u2502 B2\n\u25c9 ovknlmrokpkl martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6bd0b4\n\u2502 B1\n\u25c9 nuvyytnqlquo martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f097aa9\n\u2502 A\n\u2502 \u25c9 kntqzsqtnspv martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19dac36\n\u251c\u2500\u256f Say goodbye\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\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:
$ jj rebase -s puqltuttrvzp -d nuvyytnqlquo\nRebased 2 commits\nWorking copy now at: 1978b53430cd C\nAdded 0 files, modified 1 files, removed 0 files\n$ jj log\n@ qzvqqupxlkot martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 1978b53430cd conflict\n\u2502 C\n\u25c9 puqltuttrvzp martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 f7fb5943ee41 conflict\n\u2502 B2\n\u2502 \u25c9 ovknlmrokpkl martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6bd0b4\n\u251c\u2500\u256f B1\n\u25c9 nuvyytnqlquo martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f097aa9\n\u2502 A\n\u2502 \u25c9 kntqzsqtnspv martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19dac36\n\u251c\u2500\u256f Say goodbye\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\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 jj log
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 puqltuttrvzp # Replace the ID by what you have for B2\nWorking copy now at: c7068d1c23fd (no description set)\nAdded 0 files, modified 0 files, removed 1 files\n$ jj st\nParent commit: f7fb5943ee41 B2\nWorking copy : c7068d1c23fd (no description set)\nThe working copy is clean\nThere are unresolved conflicts at these paths:\nfile1 2-sided conflict\n$ cat file1\n<<<<<<<\n%%%%%%%\n-b1\n+a\n+++++++\nb2\n>>>>>>>\n$ echo resolved > file1\n$ jj squash\nRebased 1 descendant commits\nWorking copy now at: e3c279cc2043 (no description set)\n$ jj log\n@ ntxxqymrlvxu martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 e3c279cc2043\n\u2502 (empty) (no description set)\n\u2502 \u25c9 qzvqqupxlkot martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 b9da9d28b26b\n\u251c\u2500\u256f C\n\u25c9 puqltuttrvzp martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 2c7a658e2586\n\u2502 B2\n\u2502 \u25c9 ovknlmrokpkl martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6bd0b4\n\u251c\u2500\u256f B1\n\u25c9 nuvyytnqlquo martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f097aa9\n\u2502 A\n\u2502 \u25c9 kntqzsqtnspv martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19dac36\n\u251c\u2500\u256f Say goodbye\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\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 ovknlmrokpkl
. 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 2023-02-12 19:34:09.549 -08:00 - 2023-02-12 19:34:09.552 -08:00\n\u2502 squash commit 63874fe6c4fba405ffc38b0dd926f03b715cf7ef\n\u2502 args: jj squash\n\u25c9 6fc1873c1180 martinvonz@vonz.svl.corp.google.com 2023-02-12 19:34:09.548 -08:00 - 2023-02-12 19:34:09.549 -08:00\n\u2502 snapshot working copy\n\u25c9 ed91f7bcc1fb martinvonz@vonz.svl.corp.google.com 2023-02-12 19:32:46.007 -08:00 - 2023-02-12 19:32:46.008 -08:00\n\u2502 new empty commit\n\u2502 args: jj new puqltuttrvzp\n\u25c9 367400773f87 martinvonz@vonz.svl.corp.google.com 2023-02-12 15:08:33.917 -08:00 - 2023-02-12 15:08:33.920 -08:00\n\u2502 rebase commit daa6ffd5a09a8a7d09a65796194e69b7ed0a566d and descendants\n\u2502 args: jj rebase -s puqltuttrvzp -d nuvyytnqlquo\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\nWorking copy now at: 63874fe6c4fb (no description set)\n$ jj log\n@ zxoosnnpvvpn martinvonz@google.com 2023-02-12 19:34:09.000 -08:00 63874fe6c4fb\n\u2502 (no description set)\n\u2502 \u25c9 qzvqqupxlkot martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 1978b53430cd conflict\n\u251c\u2500\u256f C\n\u25c9 puqltuttrvzp martinvonz@google.com 2023-02-12 15:08:33.000 -08:00 f7fb5943ee41 conflict\n\u2502 B2\n\u2502 \u25c9 ovknlmrokpkl martinvonz@google.com 2023-02-12 15:07:24.000 -08:00 7d7c6e6bd0b4\n\u251c\u2500\u256f B1\n\u25c9 nuvyytnqlquo martinvonz@google.com 2023-02-12 15:07:05.000 -08:00 5dda2f097aa9\n\u2502 A\n\u2502 \u25c9 kntqzsqtnspv martinvonz@google.com 2023-02-12 14:56:59.000 -08:00 5d39e19dac36\n\u251c\u2500\u256f Say goodbye\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\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. These commands assume that you have meld
installed. If you prefer a terminal-based diff editor, you can configure scm-diff-editor
instead.
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: f94e49cf2547 abc\nAdded 0 files, modified 0 files, removed 1 files\n$ jj new -m ABC; printf 'A\\nB\\nc\\n' > file\nWorking copy now at: 6f30cd1fb351 ABC\n$ jj new -m ABCD; printf 'A\\nB\\nC\\nD\\n' > file\nWorking copy now at: a67491542e10 ABCD\n$ jj log -r master::@\n@ mrxqplykzpkw martinvonz@google.com 2023-02-12 19:38:21.000 -08:00 b98c607bf87f\n\u2502 ABCD\n\u25c9 kwtuwqnmqyqp martinvonz@google.com 2023-02-12 19:38:12.000 -08:00 30aecc0871ea\n\u2502 ABC\n\u25c9 ztqrpvnwqqnq martinvonz@google.com 2023-02-12 19:38:03.000 -08:00 510022615871\n\u2502 abc\n\u25c9 orrkosyozysx octocat@nowhere.com 2012-03-06 15:06:50.000 -08:00 master 7fd1a60b01f9\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 -i
(short for jj squash --interactive
) 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 'meld'; you can change this by setting ui.diff-editor\nWorking copy now at: 52a6c7fda1e3 ABCD\n
That will bring up Meld with a diff of the changes in the \"ABCD\" commit. Modify the right side of the diff to have the desired end state in \"ABC\" by removing the \"D\" line. Then save the changes and close Meld. 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 'meld'; you can change this by setting ui.diff-editor\nCreated 70985eaa924f ABC\nRebased 1 descendant commits\nWorking copy now at: 1c72cd50525d ABCD\nAdded 0 files, modified 1 files, removed 0 files\n
When Meld starts, edit the right side by e.g. adding something to the first line. Then save the changes and close Meld. 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. Other commands for rewriting contents of existing commits are jj split
, jj unsquash -i
and jj move -i
. Now that you've seen how jj squash -i
and jj diffedit
work, you can hopefully figure out how those work (with the help of the instructions in the diff).
The working copy is where the current working-copy commit's files are written so you can interact with them. It 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. 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. To untrack a file while keeping it in the working copy, first make sure it's ignored and then run jj untrack <path>
.
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
. However, $GIT_DIR/info/exclude
or equivalent way (maybe .jj/gitignore
) of specifying per-clone ignores is not yet supported.
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).
When you modify workspace A's working-copy commit from workspace B, workspace A's working copy will become stale. By \"stale\", we mean that the files in the working copy don't match the desired commit indicated by the @
symbol in jj log
. When that happens, use jj workspace update-stale
to update the files in the working copy.
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_branches(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 revelant 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 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: - CPU and memory available on the machine running the commands. 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. - External resources that are not immediately known to jj. For example, commands run in parallel may wish to limit the total number of connections to a server. We might choose to defer any handling of this to the implementation of the command being invoked, instead of trying to communicate that information to jj.
The base command of any jj command should be usable. By default jj run
works on the @
the current working copy. * --command, explicit name of the first argument * -x, for git compatibility (may alias another command) * -j, --jobs, the amount of parallelism to use * -k, --keep-going, continue on failure (may alias another command) * --show, display the diff for an affected revision * --dry-run, do the command execution without doing any work, logging all intended files and arguments * --rebase, rebase all parents on the consulitng diff (may alias another command) * --reparent, change the parent of an effected revision to the new change (may alias another command) * --clean, remove existing workspaces and remove the ignored files * --readonly, ignore changes across multiple run invocations * --error-strategy=continue|stop|fatal
, see Dealing with failure
jj 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
. @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-branch
config can mitigate this problem, but we'll get locally-deleted branches instead.
The goal of this plan is to implement * proper support for tracking/non-tracking remote branches * logically consistent data model for importing/exporting Git refs
"},{"location":"design/tracking-branches/#current-data-model-as-of-jj-080","title":"Current data model (as of jj 0.8.0)","text":"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: 1. jj branch forget
2. 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 * naturally support remote branches which have no local counterparts * deduplicate 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-branch
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) {\nif remote == \"git\" {\nState::Tracking\n} else if config[\"git.auto-local-branch\"] {\nState::Tracking\n} else {\nState::New\n}\n}\n
A branch target to be merged is calculated based on the state
.
fn target_in_merge_context(known_target, state) {\nmatch state {\nState::New => RefTarget::absent(),\nState::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-branch
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
1. Fetches remote changes to the backing Git repo. 2. Import changes only for remotes[remote].branches[glob]
(see below)
.tags
?jj git import
1. Copies git_refs
from the backing Git repo. 2. Calculates diff from the known 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]
3. Merges diff in local branches
and tags
if state
is tracking
.target
is absent
, the default state
should be calculated. This also applies to previously-forgotten branches. 4. Updates remotes
reflecting the import. 5. Abandons commits that are no longer referenced.jj git push
1. Calculates diff from the known 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
) 2. Pushes diff to the remote Git repo (as well as remote tracking branches in the backing Git repo.) 3. Updates remotes[remote]
and git_refs
reflecting the push.jj git export
1. Copies local 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
) 2. Calculates diff from the known git_refs
to the new remotes[remote]
. 3. Applies diff to the backing Git repo. 4. Updates 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.
"},{"location":"design/tracking-branches/#initclone","title":"init/clone","text":"jj init
git.auto_local_branch
config.If !git.auto_local_branch
, no tracking
state will be set.
jj git clone
git.auto_local_branch
config.git.auto_local_branch
config. (Because local branch is created for the default remote branch, it makes sense to track.)jj branch set {name}
1. Sets local branches[name]
entry.jj branch delete {name}
1. Removes local branches[name]
entry.jj branch forget {name}
1. Removes local branches[name]
entry if exists. 2. Removes remotes[remote].branches[name]
entries if exist. TODO: maybe better to not remove non-tracking remote branches?jj branch track {name}@{remote}
(new command) 1. Merges [local, remote] - [absent]
in local branch.remotes[remote].branches[name].state = tracking
.jj branch untrack {name}@{remote}
(new command) 1. Sets remotes[remote].branches[name].state = new
.jj branch list
Note: desired behavior of jj branch forget
is to * discard both local and remote branches (without actually removing branches at remotes) * not abandon commits which belongs to those branches (even if the branch is removed at a remote)
state = new|tracking
based on git.auto_local_branch
2. If new state
is tracking
, merges [absent, new_remote] - [absent]
(i.e. creates local branch with new_remote
target) 3. Sets remotes[remote].branches[name].state
[local, new_remote] - [known_remote]
state = new|tracking
based on git.auto_local_branch
2. If new state
is tracking
, merges [local, new_remote] - [absent]
3. Sets remotes[remote].branches[name].state
[local, absent] - [known_remote]
2. Removes remotes[remote].branches[name]
(target
becomes absent
) (i.e. the remote branch is no longer tracked) 3. Abandons commits in the deleted branchstate = new|tracking
based on git.auto_local_branch
2. Noop anyway since [local, absent] - [absent]
-> local
state = new|tracking
based on git.auto_local_branch
2. If new state
is tracking
, merges [absent, new_remote] - [absent]
-> new_remote
3. Sets remotes[remote].branches[name].state
state = new
[local, absent] - [absent]
-> local
2. Sets remotes[remote].branches[name].target = local
, .state = tracking
[local, remote] - [absent]
local
moved backwards or sideways 2. Sets remotes[remote].branches[name].target = local
, .state = tracking
[local, remote] - [remote]
-> local
local
moved backwards or sideways, and if remote
is out of sync 2. Sets remotes[remote].branches[name].target = local
[absent, remote] - [remote]
-> absent
remote
is out of sync? 2. Removes 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
2. Exports [local, absent] - [absent]
-> local
[local, git] - [absent]
-> failremotes[\"git\"].branches[name].target = local
2. Exports [local, git] - [git]
-> local
remotes[\"git\"].branches[name]
2. Exports [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.git.auto_local_branch = false
by default to help Git interop?tracking
remotes?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;\n
"},{"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 branches, etc.). If conflicting changes were made, they will appear as conflicts. For example, if a branch 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 branch pointers. Note that, unlike in pure Git, losing a branch 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 concurrent 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, branches, 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 concurrent 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 concurrent 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 concurrent 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 branch 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 branches (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 branch. 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 branch 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 within the tree object as a special conflict object (not a file object with conflict markers). Conflicts are stored as a lists of states to add and another list of states to remove. A \"state\" here can be a normal file, a symlink, or a tree. These two lists together can be a viewed as a simple algebraic expression of positive and negative terms. The order of terms is undefined.
For example, a regular 3-way merge between B and C, with A as base, is B+C-A
({ removes=[A], adds=[B,C] }
). A modify/remove conflict is B-A
. An add/add conflict is B+C
. An octopus merge of N commits usually has N positive terms and N-1 negative terms. A non-conflict state A is equivalent to a conflict state containing just the term A
. An empty expression indicates absence of any content at that path. A conflict can thus encode a superset of what can be encoded in a regular path state.
Remember that a 3-way merge can be written B+C-A
. If one of those states is itself a conflict, then we simply insert the conflict expression there. Then we simplify by removing canceling terms.
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).