Paul Smith

Seeing this reminded me of how much I like jj absorb. It's a powerful command, and one of those features of jj that can put a smile on your face when you see it in action. If you've ever made multiple fixes in your working directory and wished you could automatically send each fix to the right commit in your history, that's exactly what jj absorb does. It's a feature that doesn't exist out of the box in Git and can be clunky to replicate, so it's a great example of the power of jj's design. And it's underdocumented in my view. There aren't any examples in the man page or official website docs as far as I can tell.

Here's everything the man page and jj website has to say about jj absorb:

$ jj help absorb
Move changes from a revision into the stack of mutable revisions

This command splits changes in the source revision and moves each change to the
closest mutable ancestor where the corresponding lines were modified last. If
the destination revision cannot be determined unambiguously, the change will be
left in the source revision.

The source revision will be abandoned if all changes are absorbed into the
destination revisions, and if the source revision has no description.

The modification made by `jj absorb` can be reviewed by `jj op show -p`.

Usage: jj absorb [OPTIONS] [FILESETS]...

Arguments:
  [FILESETS]...
          Move only changes to these paths (instead of all paths)

Options:
  -f, --from <REVSET>
          Source revision to absorb from

          [default: @]

  -t, --into <REVSETS>
          Destination revisions to absorb into

          Only ancestors of the source revision will be considered.

          [default: mutable()]
          [aliases: to]

From this I gather that it's a little like jj squash: it combines changes with earlier ones, in keeping with jj's overall spirit of stable but mutable changes. But this piece is a little unclear: "moves each change to the closest mutable ancestor where the corresponding lines were modified last." In other words, it will distribute diffs from multiple files back in time to previous revisions where those files were created or edited. Also, unlike a squash which updates a single previous revision, absorb will potentially update many revisions. Further, since it works at the level of the line, it may split changes in a single file across multiple revisions. Let's test our our understanding by trying it out.

$ jj git init

I'll create a few files with different kinds of content in a series of commits.

$ echo $'a b c\nA B C\n1 2 3' > a && jj commit -m "Add file a"
$ echo "The time is now $(date)" > b && jj commit -m "Add file b"
$ echo $'- red\n- green\n- blue' > c && jj commit -m "Add file c"

Our history at this point looks like:

@  zmmkxkyk paulsmith@pobox.com 2025-08-20 10:45:48 d5483ce5
│  (empty) (no description set)
○  qnwlwkrk paulsmith@pobox.com 2025-08-20 10:45:48 da79bd35
│  Add file c
○  rpxrzqqq paulsmith@pobox.com 2025-08-20 10:45:48 df1c52d0
│  Add file b
○  qozqypuz paulsmith@pobox.com 2025-08-20 10:45:48 a0308780
│  Add file a
◆  zzzzzzzz root() 00000000

Now I'm going to edit each file, making a small change to each.

$ vim a b c
$ jj diff --git

Our change has this diff:

diff --git a/a b/a
index 31de6f3761..ce7afc22d7 100644
--- a/a
+++ b/a
@@ -1,3 +1,4 @@
-a b c
-A B C
+a b c d
+x y z
+B C
 1 2 3
diff --git a/b b/b
index d904e394e9..d38c71e8a0 100644
--- a/b
+++ b/b
@@ -1,1 +1,2 @@
-The time is now Wed Aug 20 10:45:48 CDT 2025
+The time is now:
+Wed Aug 20 10:45:48 CDT 2025
diff --git a/c b/c
index d7b1eb9e88..02a89b7084 100644
--- a/c
+++ b/c
@@ -1,3 +1,7 @@
 - red
+- orange
+- yellow
 - green
 - blue
+- indigo
+- violet

Let's absorb this change now. Our expectation, according to that help text, is that jj will spread these individual files diffs across the previous revisions it finds where those individual files were last changed.

$ jj absorb
Absorbed changes into 3 revisions:
  qnwlwkrk bb89dd81 Add file c
  rpxrzqqq 7981140d Add file b
  qozqypuz fc76ab5e Add file a
Working copy  (@) now at: qsmyrsyu b0f4d79f (empty) (no description set)
Parent commit (@-)      : qnwlwkrk bb89dd81 Add file c

Indeed, "absorbed changed into 3 revisions". And we can see that our working copy revision is also now empty, having been abandoned after all of its changes were moved out.

What jj did was, for each change in the working copy, find in the history the last (mutable) revision that affected the same lines as the change, and apply the diff as a patch, which produced a new commit (in the Git sense) for that change (in the jj sense, a stable ID across potentially many modifications to a particular revision).

The way I've found it to be very very useful are those occasions when I'm in the middle of editing some files, and realize I need to make an edit to one or more files unrelated to those other in-progress ones - maybe a typo, maybe a tweak that provides a more stable foundation for the work in was in the middle of. I could jj split, but then I'd have a minor commit in the history, which is sometimes fine, or jj squash, but then I'd have to find the right previous revision to squash into. And in either case I'd have to take a little care to get it just right so as leave the original working copy files alone. jj absorb is a way of saying, this edit is out of time with the rest of this working copy, and should be with the revision that those files/lines are connected with, and jj will figure that out. It tends to produce tidier histories with more logically consolidated revisions.

There's a final bit in the help text about using jj op show to see what absorb did in detail:

$ jj op show -p --git
bacc682f6f3c paul@oberon 1 minute ago, lasted 26 milliseconds
absorb changes into 3 commits
args: jj absorb

Changed commits:
○  + qsmyrsyu b0f4d79f (empty) (no description set)   - zmmkxkyk hidden 061c0eb3 (no description set)
├─╯  diff --git a/a b/a
│    index 31de6f3761..ce7afc22d7 100644    --- a/a
│    +++ b/a
│    @@ -1,3 +1,4 @@
│    -a b c
│    -A B C
│    +a b c d
│    +x y z
│    +B C
│     1 2 3    diff --git a/b b/b
│    index d904e394e9..d38c71e8a0 100644    --- a/b
│    +++ b/b
│    @@ -1,1 +1,2 @@
│    -The time is now Wed Aug 20 10:45:48 CDT 2025    +The time is now:
│    +Wed Aug 20 10:45:48 CDT 2025    diff --git a/c b/c
│    index d7b1eb9e88..02a89b7084 100644    --- a/c
│    +++ b/c
│    @@ -1,3 +1,7 @@
│     - red
│    +- orange
│    +- yellow
│     - green
│     - blue
│    +- indigo
│    +- violet
○  + qnwlwkrk bb89dd81 Add file c
│  - qnwlwkrk hidden da79bd35 Add file c
│  diff --git a/c b/c
│  index d7b1eb9e88..02a89b7084 100644  --- a/c
│  +++ b/c
│  @@ -1,3 +1,7 @@
│   - red
│  +- orange
│  +- yellow
│   - green
│   - blue
│  +- indigo
│  +- violet
○  + rpxrzqqq 7981140d Add file b
│  - rpxrzqqq hidden df1c52d0 Add file b
│  diff --git a/b b/b
│  index d904e394e9..d38c71e8a0 100644  --- a/b
│  +++ b/b
│  @@ -1,1 +1,2 @@
│  -The time is now Wed Aug 20 10:45:48 CDT 2025  +The time is now:
│  +Wed Aug 20 10:45:48 CDT 2025  + qozqypuz fc76ab5e Add file a
   - qozqypuz hidden a0308780 Add file a
   diff --git a/a b/a
   index 31de6f3761..ce7afc22d7 100644
   --- a/a
   +++ b/a
   @@ -1,3 +1,4 @@
   -a b c
   -A B C
   +a b c d
   +x y z
   +B C
    1 2 3

Changed working copy default@:
+ qsmyrsyu b0f4d79f (empty) (no description set)
- zmmkxkyk hidden 061c0eb3 (no description set)

Which also reminds that every action in jj that changes the state of the repo can be trivially rolled back:

$ jj undo
Undid operation: bacc682f6f3c (2025-08-20 11:04:55) absorb changes into 3 commits
Working copy  (@) now at: zmmkxkyk 061c0eb3 (no description set)
Parent commit (@-)      : qnwlwkrk da79bd35 Add file c