Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Copying a change with jj duplicate

Every history command we’ve used so far has moved work. jj rebase moves a change to a new parent, jj squash moves it into another commit, and jj absorb moves hunks around. Sometimes we want to copy a change instead. That’s what jj duplicate does: the original stays where it is, and a second, independent change appears somewhere else.

This is the equivalent of git cherry-pick. A common example is a fix that landed on our main line of work and is also needed on a release branch:

$ jj log
@  qrmutqws steve@steveklabnik.com 2024-03-27 10:30:47 5e653078
│  fix a bug
│ ○  myvqszlv steve@steveklabnik.com 2024-03-27 10:30:47 c25044fc
├─╯  release prep
○  unrvswsv steve@steveklabnik.com 2024-03-27 10:30:47 ecc089fe
│  initial
◆  zzzzzzzz root() 00000000
$ jj duplicate qrmutqws --onto myvqszlv
Duplicated 5e653078ccee as mzrozlul 5b488518 fix a bug
$ jj log
@  qrmutqws steve@steveklabnik.com 2024-03-27 10:30:47 5e653078
│  fix a bug
│ ○  mzrozlul steve@steveklabnik.com 2024-03-27 10:30:47 5b488518
│ │  fix a bug
│ ○  myvqszlv steve@steveklabnik.com 2024-03-27 10:30:47 c25044fc
├─╯  release prep
○  unrvswsv steve@steveklabnik.com 2024-03-27 10:30:47 ecc089fe
│  initial
◆  zzzzzzzz root() 00000000

Excellent. The fix is now in both places, and our working copy never moved.

The copy is a different change

Let’s look closely at the two IDs. The original is qrmutqws; the copy is mzrozlul. Same description, same contents, different change.

This is the important distinction between duplicate and rebase. A rebased commit keeps its change ID because it’s still the same change in a new place. A duplicated commit gets a new one because there are now two independent changes. If we edit one, the other doesn’t follow.

It also means the two copies will be pushed as separate commits and reviewed separately, which is what you want for a backport, and is exactly what a git cherry-pick does.

Where the copy lands

We used --onto to put the copy on a destination. The same -A and -B flags we saw with jj rebase work here too. They let us insert the copy into the middle of a stack rather than on top of something:

$ jj duplicate qrmutqws -A myvqszlv

With no destination at all, the copy becomes a sibling of the original and shares its parents. We can use that form when we want to try two versions of the same idea side by side and keep both.

You can duplicate several changes at once by naming a revset, which is how you backport a whole feature rather than one commit.

When not to

If we find ourselves duplicating the same fix onto several release branches every time, a merge may be a better fit. Duplicating makes sense when the destination genuinely needs its own version of the change, such as a backport that will be maintained separately or an experiment we want two versions of. If we only want to move a change, jj rebase leaves us with one copy to keep track of.