ok/jj
1
0
Fork 0
forked from mirrors/jj

docs: rewrite README to target users, not developers

The project is starting to work well enough that I think it's time
that the documentation targets users instead of VCS hackers. This
patch rewrites much of the README to describe how the features help
the user instead of describing how they work. It also adds a tutorial.
This commit is contained in:
Martin von Zweigbergk 2021-05-23 14:42:20 -07:00
parent 080a9b37ff
commit 0328381c6c

600
README.md
View file

@ -15,173 +15,511 @@ I started the project mostly in order to test the viability of some UX ideas in
practice. I continue to use it for that, but my short-term goal now is to make
it useful as an alternative CLI for Git repos.
The storage design is similar to Git's in that it stores commits, trees, and
blobs. However, the blobs are actually split into three types: normal files,
symlinks (Unicode paths), and conflicts (more about that later).
The command-line tool is called `jj` for now because it's easy to type and easy
to replace (rare in English). The project is called "Jujutsu" because it matches
"jj" (I initially called it "Jujube", but changed since jujutsu is more
well-known).
Features:
## Features
* **Compatible with Git**
The following subsections describe the current features. The text is aimed at
readers who are already familiar with other VCSs.
Jujutsu has two backends. One of them is a Git backend (the other is a
native one). This lets you use Jujutsu as an alternative interface to Git.
The commits you create will look like regular Git commits. You can always
switch back to Git.
### Compatible with Git
* **The working copy is automatically committed**
The tool currently has two backends. One is called "local store" and is very
simple and inefficient. The other backend uses a Git repo as storage. The
commits are stored as regular Git commits. Commits can be read from and written
to an existing Git repo. This makes it possible to create a Jujutsu repo and use
it as an alternative interface for a Git repo (it will be backed by the Git repo
just like additional Git worktrees are).
Most Jujutsu commands automatically commit the working copy. This leads to a
simpler and more powerful interface, since all commands work the same way on
the working copy or any other commit. It also means that you can always check
out a different commit without first explicitly committing the working copy
changes (you can even check out a different commit while resolving merge
conflicts).
### Written as a library
* **Operations update the repo first, then possibly the working copy**
The project consists of two main parts: the lib crate and the main (CLI)
crate. Most of the code lives in the lib crate. The lib crate does not print
anything to the terminal. The separate lib crate should make it relatively
straight-forward to add a GUI.
The working copy is only updated at the end of an operation, after all other
changes have already been recorded. This means that you can run any command
(such as `jj rebase`) even if the working copy is dirty.
### Operations are performed repo-first
* **Entire repo is under version control**
Almost all operations are done in the repo first and then possibly reflected in
the working copy. The only exception so far is when committing the working copy,
which naturally uses the working copy as input.
All operations you perform in the repo are recorded, along with a snapshot of
the repo state after the operation. This means that you can easily revert to
an earlier repo state, or to simply undo a particular operation (which does
not necessarily have to be the most recent operation).
This makes it faster because the working copy doesn't need to get updated. It
also means that the working copy won't see spurious changes e.g. during a rebase
operation. It makes it safe to update the working copy while some operation is
running.
* **Conflicts can be recorded in commits**
### Supports Evolution
If an operation results in conflicts, information about those conflicts will
be recorded in the commit(s). The operation will succeed. You can then
resolve the conflicts later. One consequence of this design is that there's
no need to continue interrupted operations. Instead, you get a single
workflow for resolving conflicts, regardless of which command caused them.
This design also lets Jujutsu rebase merge commits correctly (unlike both Git
and Mercurial).
Jujutsu copies the Evolution feature from Mercurial. It keeps track of when a
commit gets rewritten. A commit has a list of predecessors in addition to the
usual list of parents. This lets the tool figure out where to rebase descendant
commits to when a commit has been rewritten (amended, rebased, etc.). See
https://www.mercurial-scm.org/wiki/ChangesetEvolution for more information.
* **Supports Evolution**
Jujutsu copies the Evolution feature from Mercurial (see
https://www.mercurial-scm.org/wiki/ChangesetEvolution). However, you will
often not need to know that because the conflict design described above lets
Jujutsu keep commits rebased even if there are conflicts.
### The working copy is a commit
The working copy gets automatically committed when you interact with the
tool. This simplifies both implementation and UX. It also means that the working
copy is frequently backed up.
## Tutorial
Any changes to the working copy stay in place when you check out another
commit. That is different from Git and Mercurial, but I think it's more
intuitive for new users. To replicate the default behavior of Git/Mercurial, use
`jj rebase -r @ -d <destination>` (`@` is a name for the working copy
commit). There is no need to stash/unstash.
This text assumes that the reader is familiar with Git.
Commands become more consistent because the same command can operate on the repo
or another commit. For example, `jj log` includes the working copy (much like
`gitk` and other tools include a node for the working copy). `jj squash`
squashes a commit into its parent, including if it's the working copy (like `git
commit --amend`/`hg amend`).
### Setup
A commit description can be added to the working copy before "commit". The same
command (`jj describe`) is used for changing the description of any commit.
First install Jujutsu:
```shell script
$ git clone https://github.com/martinvonz/jj.git jj-git
$ cd jj-git
$ cargo install --path .
$ cd ..
```
### Commits can contain conflicts
You may also want to configure your name and email so commits are made in your
name. Create a `~/.jjconfig` file and make it look something like this:
```shell script
$ cat ~/.jjconfig
[user]
name = "Martin von Zweigbergk"
email = "martinvonz@google.com"
```
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
regular 3-way merge adds [B,C] and removes [A] (the common ancestor). A
modify/remove conflict adds [B] and removes [A]. An add/add conflict adds
[B,C]. An octopus merge of N commits adds N states and removes N-1 states. A
non-conflict state A is equivalent to a conflict state that just adds [A]. A
"state" here can be a normal file, a symlink, or a tree. This support for
in-tree conflicts has some interesting effects on both implementation and UX.
### Cloning a Git repo
It means that there is a consistent way of resolving conflicts: check out a
commit with conflicts in, resolve the conflicts, and amend them into the
conflicted commit. Then evolve descendant commits.
Now let's clone the same repo using `jj`:
```shell script
# Note the "git" before "clone" (there is no support for cloning native jj
# repos yet)
$ jj git clone https://github.com/martinvonz/jj.git jj-jj
Fetching into new repo in "<dir>/jj-jj"
$ cd jj-jj
```
It naturally enables collaborative conflict resolution.
Running `jj st` (short for`jj status`) now yields something like this:
```shell script
$ jj st
Parent commit: 000000000000
Working copy : 60c89901778d
The working copy is clean
```
The in-tree conflicts means that there is no need for book-keeping in
rebase-like commands to support continue/abort operations. Instead, the rebase
can simply continue and create the desired new DAG shape.
The `000000000000` commit is a virtual commit that's called the "root commit".
It's the root commit of every repo. The reason that it is our working copy's
parent is that `jj git clone` doesn't yet check out a particular commit.
Conflicts get simplified on rebase by removing pairs of matching states in the
"add" and "remove" lists. For example, let's say commit B is based on A and is
rebased to C, where it results in conflicts, which the user leaves
unresolved. If the commit is then rebased to D, it will be a regular 3-way merge
between B and D with A as base (no trace of C). This means that you can keep old
commits rebased to head without resolving conflicts, and you still won't have
messy recursive conflicts.
We can also see from the output above that our working copy has a commit id
(`60c89901778d` in the example).
The conflict handling also results in some Darcs-/Pijul-like properties. For
example, if you rebase a commit and it results in conflicts, and you then back
out that commit, the conflict will go away. (I plan to make that work even if
there had been unrelated changes in the file, but I haven't gotten around to it
yet.)
Let's check out a different commit so we get some files to work with in the
working copy:
```shell script
$ jj co 080a9b37ff7e
Leaving: 60c89901778d
Now at: 608c179a60df
added 84 files, modified 0 files, removed 0 files
$ jj st
Parent commit: 080a9b37ff7e cli: make `jj st` show parent commit before working copy commit
Working copy : 608c179a60df
The working copy is clean
```
The criss-cross merge case becomes simpler. In Git, the virtual ancestor may
have conflicts and you may get nested conflict markers in the working copy. In
Jujutsu, the result is a merge with multiple parts, which may even get simplified
to not be recursive.
You might have noticed that even though we asked to check out some commit
(`080a9b37ff7e`), our working copy commit ended being another commit
(`608c179a60df`). That is because `jj co` (short for `jj checkout`) creates a
new commit on top of the commit you asked it to check out. The new commit is for
the working copy changes. (There's some more nuance to this. We'll go through
that in a bit.)
The in-tree conflicts make it natural and easy to define the contents of a merge
commit to be the difference compared to the merged parents (the so-called "evil"
part of the merge), so that's what Jujutsu does. Rebasing merge commits
therefore works as you would expect (Git and Mercurial both handle rebasing of
merge commits poorly). It's even possible to change the number of parents while
rebasing, so if A is non-merge commit, you can make it a merge commit with `jj
rebase -r A -d B -d C`. `jj diff -r <commit>` will show you the diff compared to
the merged parents.
### Creating our first change
I intend for commands that present the contents of a tree (such as listing
files) to use the "add" state(s) of the conflict, but that's not yet done.
Now let's say we want to edit the `README.md` file in the repo (i.e. what you're
reading right now) to say that Jujutsu is ready for use. Let's start by
describing the change (adding a commit message) so we don't forget what we're
working on:
```shell script
# This will bring up $EDITOR (or `pico` by default). Enter something like
# "Jujutsu is ready!" in the editor and then close it.
$ jj describe
Leaving: 608c179a60df
Now at: b2985d68096d Jujutsu is ready!
```
### Operations are logged
Now make the change in the README:
```shell script
# Adjust as necessary for compatibility with your flavor of `sed`
$ sed -i 's/not ready/ready/' README.md
$ jj st
Parent commit: 080a9b37ff7e cli: make `jj st` show parent commit before working copy commit
Working copy : 5f80190c44b9
Working copy changes:
M README.md
```
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. However, the flip side of that is that you need to be
careful keep your `.gitignore` up to date since there's currently no easy way
to say that you want an already added file to not be tracked
(https://github.com/martinvonz/jj/issues/14).
Each write operation is logged to a content-addressed storage, much like the
commit storage. The Operation object has an associated View object, much like
the Commit object has a Tree object. The view object contains all the heads
currently in the repo, as well as the checked-out commit. It will also contain
the refs if I add support for that. The operation object can have multiple
parent operations, so it forms a DAG just like the commit graph does. There is
normally only one parent operation, but there can be multiple parents if
concurrent operations happened.
To see the diff, run `jj diff`:
```shell script
$ jj diff
modified file README.md:
...
4 4: ## Disclaimer
5 5:
6 6: This is not a Google product. It is an experimental version-control system
7 7: (VCS). It is not ready for use. It was written by me, Martin von Zweigbergk
8 8: (martinvonz@google.com). It is my personal hobby project. It does not indicate
9 9: any commitment or direction from Google.
10 10:
...
```
Jujutsu's diff format currently only has inline coloring of the diff (like
`git diff --color-words`), which makes the diff impossible to see in the
un-colorized output above (the "not" in "not ready" is red).
I added the operation log as a solution for the problem of making concurrent
repo edits safe. When the repo is loaded, it is loaded at a particular
operation, which provides an immutable view of the repo. For a caller of the
library to start making changes, they then have to start a transaction. Once
they are done making changes to the transaction, they commit the
transaction. The operation object is then created. This step cannot fail (except
if the file system runs out of space or such). Pointers to the heads of the
operation DAG are kept as files in a directory (the filename is the operation
id). When a new operation object has been created, its operation id is added to
the directory. The transaction's base operation id is then removed from that
directory. If concurrent operations happened, there would be multiple new
operation ids in the directory and only one base operation id would have been
removed. If a reader sees the repo in this state, it will attempt to merge the
views and create a new operation with multiple parents. If there are conflicts,
the user will have to resolve it (I haven't implemented that yet).
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 working copy
commit? The answer is that we need to "close" the commit. When we close a
commit, we indicate that we're done making changes to the commit. As described
earlier, when we check out a commit, a new working copy commit is created on
top. However, that is only true for closed commits. If the commit is open, then
that commit itself will be checked out instead. (There's still more nuance to
this. We'll get back to that when we talk about conflicts.)
As a nice side-effect of adding the operation log to solve the concurrent-edits
problem, we get some very useful UX features. Many UX features come from mapping
commands that work on the commit graph onto the operation graph. For example, if
you map `git revert`/`hg backout` onto the operation graph, you get an operation
that undoes a previous operation (called `jj op undo`). Note that any operation
can be undone, not just the latest one. If you map `git restore`/`hg revert`
onto the operation graph, you get an operation that rewinds the repo state to an
earlier point (called `jj op restore`).
So, let's say we're now done with this commit, so we close it:
```shell script
$ jj close
Leaving: 5f80190c44b9 Jujutsu is ready!
Now at: 192b456b024b
$ jj st
Parent commit: fb563a4c6d26 Jujutsu is ready!
Working copy : 192b456b024b
The working copy is clean
```
Note that a commit id printed in green indicate an open commit and blue
indicates a closed commit.
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.
### The log command, "revsets", and aliases
You're probably familiar with `git log`. Jujutsu has the very similar
functionality in its `jj log` command. It produces hundreds of lines of output,
so let's pipe its output into `head`:
```shell script
$ jj log | head
o <-- 192b456b024b f39aeb1a0200 martinvonz@google.com 2021-05-23 23:10:27.000 -07:00
|
o fb563a4c6d26 f63e76f175b9 martinvonz@google.com 2021-05-23 22:13:45.000 -07:00
| Jujutsu is ready!
o 080a9b37ff7e 080a9b37ff7e martinvonz@google.com 2021-05-23 22:08:37.000 -07:00 refs/remotes/origin/main
| cli: make `jj st` show parent commit before working copy commit
o ba8ff31e32fd ba8ff31e32fd martinvonz@google.com 2021-05-23 22:08:12.000 -07:00
| cli: make the working copy changes in `jj status` clearer
o dcfc888f50b3 dcfc888f50b3 martinvonz@google.com 2021-05-23 22:07:40.000 -07:00
| cli: remove "Done" message at end of git clone
```
The `<--` indicates the working copy commit. The first hash on a line is the
commit id. The second hash is a "change id", which is an id that follows the
commit as it's rewritten (similar to Gerrit's Change-Id).
By default, `jj log` lists all revisions (commits) in the repo that have not
been rewritten (roughly speaking). We can use the `-r` flag to restrict which
revisions we want to list. The flag accepts a "revset", which is an expression
in a simple language for specifying revision. For example, `@` refers to the
working copy commit, `root` refers to the root commit, `git_refs()` refers to
all commits pointed to by git refs. We can combine expression with `|` for
union, `&` for intersection and `-` for difference. For example:
```shell script
$ jj log -r '@ | root | git_refs()'
o <-- 192b456b024b f39aeb1a0200 martinvonz@google.com 2021-05-23 23:10:27.000 -07:00
:
o 080a9b37ff7e 080a9b37ff7e martinvonz@google.com 2021-05-23 22:08:37.000 -07:00 refs/remotes/origin/main
: cli: make `jj st` show parent commit before working copy commit
o 000000000000 000000000000 1970-01-01 00:00:00.000 +00:00
```
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`, like Git's `foo..bar`). There
are also a few more functions, such as `public_heads()`, which is the set of
revisions that have Git remote-tracking branches pointing to them, except those
that are ancestors of other revisions in the set. Now define an alias based on
that by adding the following to `~/.jjconfig`:
```
[alias]
l = ["log", "-r", "(public_heads(),,,@),,"]
```
The alias lets us run `jj l` to see the commits we have created between public
heads (exclusive) and the working copy (inclusive), as well as their
descendants:
```shell script
$ jj l
o <-- 192b456b024b f39aeb1a0200 martinvonz@google.com 2021-05-23 23:10:27.000 -07:00
|
o fb563a4c6d26 f63e76f175b9 martinvonz@google.com 2021-05-23 22:13:45.000 -07:00
~ Jujutsu is ready!
```
### Conflicts
Now let's see how Jujutsu deals with merge conflicts. We'll start by making some
commits:
```shell script
# Check out the grandparent of the working copy
$ jj co ::@
Leaving: 192b456b024b
Now at: 9164f1d6a011
added 0 files, modified 1 files, removed 0 files
$ echo a > file1; jj close -m A
Leaving: 58a7b6f1826e
Now at: 5be91b2b5b69
$ echo b1 > file1; jj close -m B1
Leaving: c4239476586a
Now at: a0331f1eeece
$ echo b2 > file1; jj close -m B2
Leaving: 85136b7495f9
Now at: fd571967346e
$ echo c > file2; jj close -m C
Leaving: e6a47cec0e0d
Now at: 4ae1e0587eef
$ jj co ::::@
Leaving: 4ae1e0587eef
Now at: 9195b6d2e8dc
added 0 files, modified 1 files, removed 1 files
$ jj l
o <-- 9195b6d2e8dc 47684978bf4b martinvonz@google.com 2021-05-26 12:39:56.000 -07:00
|
| o 1769bdaa8d6d 8e6178b84ffb martinvonz@google.com 2021-05-26 12:39:35.000 -07:00
| | C
| o de5690380f40 5548374c0794 martinvonz@google.com 2021-05-26 12:39:30.000 -07:00
| | B2
| o 47e336632333 ce619d39bd96 martinvonz@google.com 2021-05-26 12:39:20.000 -07:00
|/ B1
o 661432c51c08 cf49e6bec410 martinvonz@google.com 2021-05-26 12:39:12.000 -07:00
~ A
```
We now have a few commits, where A, B1, and B2 modify the same file, while C
modifies a different file. We checked out A in order to simplify the next steps.
Let's now rebase B2 directly onto A:
```shell script
$ jj rebase -r de5690380f40 -d 661432c51c08
Rebased 1 descendant commits
$ jj l
o 66274d5a7d2d 8e6178b84ffb martinvonz@google.com 2021-05-26 12:39:35.000 -07:00 conflict
| C
o 0c305a9e6b27 5548374c0794 martinvonz@google.com 2021-05-26 12:39:30.000 -07:00 conflict
| B2
| o <-- 9195b6d2e8dc 47684978bf4b martinvonz@google.com 2021-05-26 12:39:56.000 -07:00
|/
| o 47e336632333 ce619d39bd96 martinvonz@google.com 2021-05-26 12:39:20.000 -07:00
|/ B1
o 661432c51c08 cf49e6bec410 martinvonz@google.com 2021-05-26 12:39:12.000 -07:00
~ A
```
There are several things worth noting here. First, the `jj rebase` command said
"Rebased 1 descendant commits". That's because we asked it to rebase commit B2,
but commit C was on top of it, so it also rebased that commit as well. Second,
because B2 modified the same file (and word) as B1, rebasing it resulted in
conflicts, as the `jj l` 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 checking out B2, which
will create a new commit on top (even if B2 had been an open commit). That way
the conflict resolution doesn't get mixed in with the changes in B2. Once we've
resolved the conflict, we'll squash the conflict resolution into the conflicted
B2. That might look like this:
```shell script
$ jj co 0c305a9e6b27 # Replace the hash by what you have for B2
Leaving: 9195b6d2e8dc
Now at: 619f58d8a988
added 0 files, modified 1 files, removed 0 files
$ cat file1
<<<<<<<
a
|||||||
b1
=======
b2
>>>>>>>
$ echo resolved > file1
$ jj squash
Rebased 1 descendant commits
Leaving: 41f0d2289b56
Now at: e659edc4a9fc
$ jj l
o <-- e659edc4a9fc 461f38324592 martinvonz@google.com 2021-05-26 12:53:08.000 -07:00
|
| o 69dbcf76642a 8e6178b84ffb martinvonz@google.com 2021-05-26 12:39:35.000 -07:00
|/ C
o 576d647acf36 5548374c0794 martinvonz@google.com 2021-05-26 12:39:30.000 -07:00
| B2
| o 47e336632333 ce619d39bd96 martinvonz@google.com 2021-05-26 12:39:20.000 -07:00
|/ B1
o 661432c51c08 cf49e6bec410 martinvonz@google.com 2021-05-26 12:39:12.000 -07:00
~ A
```
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 prune 47e336632333`.
That will hide the commit from the log output and will rebase any descendants to
its parent.
### The operation log
Jujutsu keeps a record of all changes you've made to the repo in what's called
the "operation log". Use the `jj op` (short for `jj operation`) family of
commands to interact with it. To list the operations, use `jj op log`:
```shell script
$ jj op log
o 5bd3845073420b3be0a2cd5508c7ffc1e242fdb5907f4a1d8bc91f9e6cd8beb9e006a50c8e35477bec247e7c4ef73bb84f62e73d13ec9fd709ff64711f94472f martinvonz@<hostname> 2021-05-26 12:53:08.339 -07:00 - 2021-05-26 12:53:08.350 -07:00
| squash commit 41f0d2289b568bfcdcf35f73d4f70f3ab6696398
| args: jj squash
o 2fd266a8a2e0a82ab639676321eccf7b9ec3912db9c1fc6bd3eb5219941c8b839e53f524afb3ce08cb373d0af84040b05733818c79d1b23fa57bd017e873d373 martinvonz@<hostname> 2021-05-26 12:53:08.335 -07:00 - 2021-05-26 12:53:08.338 -07:00
| commit working copy
o 1e6dd15305a3c019209ff19f31ce4a6bd6c9e9de8cfeaae8aacc21e1fc9fedeb2d7f2d057a7a1a1d151651b121dd2d1589cd701d919ce080b48485c447b7e3bd martinvonz@<hostname> 2021-05-26 12:52:39.374 -07:00 - 2021-05-26 12:52:39.382 -07:00
| check out commit 0c305a9e6b274bc09b2bca85635299dcfdc6811c
| args: jj co 0c305a9e6b27
o 401652a2f61e7701e339a25f93cb8c27925df54493c538f623dfd2d0837f2c67f239dd584325989470305e0c21033a795c17cc47d5275604776183dcd12fb825 martinvonz@<hostname> 2021-05-26 12:44:51.872 -07:00 - 2021-05-26 12:44:51.882 -07:00
| rebase commit de5690380f40f3f7fc6b7d66d43a4f68ee606228
| args: jj rebase -r de5690380f40 -d 661432c51c08
[many more lines]
```
The most useful command is `jj op undo`, which will undo an operation. By
default, it will undo the most recent operation. Let's try it:
```shell script
$ jj op undo
Leaving: e659edc4a9fc
Now at: 41f0d2289b56
$ jj l
o <-- 41f0d2289b56 b1e3a4afde5e martinvonz@google.com 2021-05-26 12:52:39.000 -07:00
|
| o 66274d5a7d2d 8e6178b84ffb martinvonz@google.com 2021-05-26 12:39:35.000 -07:00 conflict
|/ C
o 0c305a9e6b27 5548374c0794 martinvonz@google.com 2021-05-26 12:39:30.000 -07:00 conflict
| B2
| o 47e336632333 ce619d39bd96 martinvonz@google.com 2021-05-26 12:39:20.000 -07:00
|/ B1
o 661432c51c08 cf49e6bec410 martinvonz@google.com 2021-05-26 12:39:12.000 -07:00
~ A
```
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 l` output right after the `jj rebase` operation,
try `jj l --at-op=401652a2f61e7701e339a25f93cb8c27925df54493c538f623dfd2d0837f2c67f239dd584325989470305e0c21033a795c17cc47d5275604776183dcd12fb825`
but use the hash from your own `jj op log` (you unfortunately need to provide
the full hash until https://github.com/martinvonz/jj/issues/16 has been fixed).
### Moving content changes between commits
You have already seen how `jj squash` can combine the changes from two commits
into one. There are several other commands for changing the contents of existing
commits. These commands require you to have `meld` installed for now
(https://github.com/martinvonz/jj/issues/10). We'll need some more complex
content to test these commands, so let's create a few more commits:
```shell script
$ jj co origin/main
Leaving: 6d296c9e0f3d
Now at: 61b0efa09dbe
added 0 files, modified 0 files, removed 1 files
$ printf 'a\nb\nc\n' > file; jj close -m abc
Leaving: 81720067a6bd
Now at: f9147a088c0d
$ printf 'A\nB\nc\n' > file; jj close -m ABC
Leaving: fdc61584b2a1
Now at: 9d97c5018b23
$ printf 'A\nB\nC\nD\n' > file; jj close -m ABCD
Leaving: cab37b2580fa
Now at: c5a985bc3f41
$ jj l
o <-- c5a985bc3f41 3568f6e332d5 martinvonz@google.com 2021-05-26 14:36:46.000 -07:00
|
o 687009839bae 874f2d307594 martinvonz@google.com 2021-05-26 14:36:38.000 -07:00
| ABCD
o ad9b1ce3b5d0 2bbc0c1eb382 martinvonz@google.com 2021-05-26 14:36:26.000 -07:00
| ABC
o a355fb177b21 3680117711f5 martinvonz@google.com 2021-05-26 14:36:05.000 -07:00
~ abc
```
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:
```shell script
$ jj squash -i -r :@
Rebased 1 descendant commits
Leaving: c5a985bc3f41
Now at: 4b4c714b36aa
```
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 close Meld. If we look the diff of the second commit, we
now see that all three lines got capitalized:
```shell script
$ jj diff -r ::@
modified file file:
1 1: aA
2 2: bB
3 3: cC
```
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 edit`, which lets you edit the contents of a commit without
checking it out.
```shell script
$ jj edit -r ::@
Created 2423c134ea70 ABC
Rebased 2 descendant commits
Leaving: 4b4c714b36aa
Now at: d31c52e8ca41
```
When Meld starts, edit the right side by e.g. adding something to the first
line. Then 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 edit` (typically) results in a different state, which means that descendant
commits may have conflicts.
Other commands for rewriting contents of existing commits are `jj restore -i`,
`jj split`, `jj unsquash -i`. Now that you've seen how `jj squash -i` and
`jj edit` work, you can hopefully figure out how those work (with the help of
the instructions in the diff).
You can also see what the repo looked like at an earlier point with `jj
--at-op=<operation id> log`. As mentioned earlier, the checkout is also part of
the view, so that command will show you where the working copy was at that
operation. If you do `jj op restore -o <operation id>`, it will also update the
working copy accordingly. This is actually how the working copy is always
updated: we first commit a transaction with a pointer to the new checkout and
then the working copy is updated to reflect that.
## Future plans