Introduction

Welcome back, jj adventurer! In previous chapters, you’ve learned the basics of jj and started to appreciate its mutable history. Now, it’s time to unlock one of jj’s most powerful features: Revsets. Think of Revsets as jj’s query language for your commit history. Just as SQL allows you to precisely select data from a database, Revsets empower you to select exactly the commits you need from your repository’s graph.

This chapter will transform you from a casual jj user to a history-querying wizard. We’ll delve into the advanced syntax, operators, and predicates that make Revsets indispensable for complex refactoring, targeted rebases, and seamless Git interoperability. By the end, you’ll be able to craft intricate queries to find, filter, and manipulate your commits with surgical precision.

The Language of History: Why Revsets Matter

Imagine your commit history not as a simple linear list, but as a rich, interconnected graph of changes. Traditional VCS tools often rely on simple branch names or commit hashes, which can become cumbersome as your history grows or when dealing with complex relationships like merges or stacked changes.

Revsets solve this by providing a flexible, powerful syntax to describe sets of commits based on their relationships, attributes, and content. This ability to precisely target commits is crucial for jj’s mutable history model, allowing you to rebase, squash, or amend specific changes without affecting others.

📌 Key Idea: Revsets are jj’s built-in query language for your commit graph, enabling precise selection and manipulation of commits.

Core Concepts: Building Blocks of Revsets

At its heart, a Revset is an expression that evaluates to a set of commits. These expressions can be simple, like @ for the working copy, or incredibly complex, combining multiple operators and predicates.

Basic Revset Syntax (A Quick Recap)

You’ve likely encountered some basic Revsets already. These foundational elements help you pinpoint key points in your history:

  • @: The working copy commit. This represents the commit your workspace is currently based on.
  • HEAD: The commit pointed to by the HEAD reference (usually the tip of your current branch). In jj, this is often equivalent to @ if you’re working on a branch.
  • main, master, my-feature: A specific branch name.
  • a1b2c3d4: A full or partial commit hash.
  • root(): The initial commit(s) in the repository with no parents.

jj provides intuitive ways to move through the commit graph relative to a given commit. This is essential for understanding the lineage of changes.

  • commit_id-: The direct parent(s) of commit_id. If a commit has multiple parents (a merge commit), this selects all of them.
  • commit_id--: The grandparent(s) of commit_id. You can chain this multiple times.
  • commit_id+: The direct child(ren) of commit_id.
  • commit_id++: The grandchild(ren) of commit_id.

Operators for Combining Commit Sets

Revsets truly shine when you start combining commit sets using logical operators, similar to set theory. These operators allow you to define complex relationships between different groups of commits.

OperatorDescriptionExample
&Intersection: Commits present in BOTH sets.@ & main (commit that is both WC and main)
``Union: Commits present in EITHER set.
-Difference: Commits in the first set, NOT in the second.all() - public() (local-only commits)
..Range (exclusive): Commits reachable from A but not B, excluding A.main..@ (commits on current branch since main, excluding main)
..=Range (inclusive): Commits reachable from A but not B, including A.main..=@ (commits on current branch since main, including main)
::Ancestors/Descendants: All ancestors up to a commit, or all descendants from a commit.main::@ (all commits from main to @, including both)
~NNth Ancestor: The Nth parent. ~1 is the same as -.@~3 (the great-grandparent of the working copy)
^NNth Parent: For merge commits, ^1 is the first parent, ^2 is the second.a1b2c3^2 (the second parent of merge commit a1b2c3)

Let’s visualize how some of these operators might combine commit sets:

flowchart LR A[Revset A] --> Combine[Combine Operator] B[Revset B] --> Combine Combine --> Result[Resulting Commit Set] subgraph Operators["Operators"] And[Intersection] Or[Union] Minus[Difference] Range[Range] end Combine --> And Combine --> Or Combine --> Minus Combine --> Range

Predicates for Filtering by Attributes

Beyond relationships, you can filter commits based on their metadata or content using predicates. These allow you to select commits that match specific criteria, such as author, message, or files changed.

PredicateDescriptionExample
author(pattern)Commits by a specific author (regex or glob).`author(“Alice
committer(pattern)Commits by a specific committer.committer("GitHub Actions")
date(from..to)Commits within a date range.date("2 weeks ago..now")
description(pattern)Commits with a message matching a pattern.description("feat:.*")
file(pattern)Commits that modified files matching a pattern.file("src/utils.js")
root()The root commit(s) of the repository.root()
heads()Commits that are heads (have no children).heads()
branches()Commits that are the tips of jj branches.branches()
tags()Commits that are jj tags.tags()
public()Commits that are part of a public Git remote.public()
hidden()Commits that are hidden (e.g., obsolete).hidden()
mine()Commits authored by the current user.mine()
not(revset)Negates a revset (commits NOT in the given set).not(public())
latest(N, revset)The N latest commits within the specified revset.latest(5, all()) (last 5 commits overall)
ancestors(revset)All ancestors of the commits in the revset.ancestors(@)
descendants(revset)All descendants of the commits in the revset.descendants(main)
grep(pattern)Commits whose message contains the pattern.grep("bugfix")
git_ref(ref_name)A specific Git reference (branch, tag, HEAD).git_ref("origin/main")
git_head()The current Git HEAD.git_head()

Quick Note: Many predicates accept regular expressions or glob patterns depending on the context. Always check the official jj documentation for specifics if you encounter unexpected behavior. As of 2026-05-19, the jj project is actively developed, and its main branch documentation is the most up-to-date resource.

Step-by-Step Implementation: Querying Your History

Let’s put these concepts into practice. We’ll create a dummy jj repository and populate it with some commits to experiment with. This hands-on exercise will solidify your understanding.

First, create a new directory and initialize a jj repo. We’ll use the --git flag to enable Git interoperability from the start.

mkdir jj-revset-playground
cd jj-revset-playground
jj init --git

Now, let’s add a few commits to build a small, representative history. We’ll create initial content, then add features and a bug fix, also introducing different authors to demonstrate filtering.

echo "Initial content for the project." > file.txt
jj new -m "feat: initial setup"
jj branch create main @

echo "Adding more functionality to the application." >> file.txt
jj new -m "feat: add more features"

echo "Fixing a critical bug in the newly added feature." >> file.txt
jj new -m "fix: resolve feature bug" --author "Alice <alice@example.com>"

echo "Implementing another exciting feature." >> file.txt
jj new -m "feat: implement another feature"

echo "WIP: Exploring a half-baked idea for future development." >> file.txt
jj new -m "WIP: working on a new idea" --author "Bob <bob@example.com>"

# Let's see our current history with relevant details
jj log -T 'commit_id description author'

You should see a history similar to this (commit IDs will vary, but the order and descriptions should match):

o  a1b2c3d4 WIP: working on a new idea Bob <bob@example.com>
o  e5f6g7h8 feat: implement another feature Your Name <your.email@example.com>
o  i9j0k1l2 fix: resolve feature bug Alice <alice@example.com>
o  m3n4o5p6 feat: add more features Your Name <your.email@example.com>
o  q7r8s9t0 feat: initial setup Your Name <your.email@example.com>

Scenario 1: Finding Specific Commits with Basic Revsets

Let’s use jj log -r to view specific parts of our history. The -T flag allows us to customize the output format.

  1. Your current working copy commit (@):

    jj log -r @ -T 'commit_id description'
    

    This should show your latest commit: WIP: working on a new idea.

  2. The direct parent of your working copy (@-):

    jj log -r @- -T 'commit_id description'
    

    You’ll see feat: implement another feature.

  3. The grandparent of your working copy (@--):

    jj log -r @-- -T 'commit_id description'
    

    This reveals fix: resolve feature bug.

  4. The root commit(s) of the repository (root()):

    jj log -r 'root()' -T 'commit_id description'
    

    This will show feat: initial setup.

Scenario 2: Combining Revisions with Operators

Now, let’s use operators to build more complex selections. Remember, parentheses are crucial for defining the order of operations in complex Revsets.

  1. Show the working copy commit AND its parent (| for union):

    jj log -r '@ | @-' -T 'commit_id description'
    

    This will list both WIP: working on a new idea and feat: implement another feature.

  2. Show all commits between main and the working copy (exclusive of main) (.. for exclusive range):

    jj log -r 'main..@' -T 'commit_id description'
    

    This should show all commits after feat: initial setup up to and including your current WIP commit.

  3. Show all commits from main up to the working copy (inclusive of main) (..= for inclusive range):

    jj log -r 'main..=@' -T 'commit_id description'
    

    This is similar to the above, but includes the main commit itself.

  4. Show all ancestors of the main branch (ancestors() predicate):

    jj log -r 'ancestors(main)' -T 'commit_id description'
    

    In our simple linear history, this will show main and all commits before it, effectively the entire history up to and including the main commit.

Scenario 3: Filtering with Predicates

Let’s filter based on commit attributes. This is where you can quickly narrow down your search based on metadata.

  1. Find all commits authored by ‘Alice’ (author() predicate):

    jj log -r 'author("Alice")' -T 'commit_id description author'
    

    You should see fix: resolve feature bug Alice <alice@example.com>.

  2. Find all commits with ‘feat:’ in their description (description() predicate):

    jj log -r 'description("feat:")' -T 'commit_id description'
    

    This will list feat: initial setup, feat: add more features, and feat: implement another feature.

  3. Find the latest 2 commits in the entire repository (latest() predicate):

    jj log -r 'latest(2, all())' -T 'commit_id description'
    

    This should give you your two most recent commits, WIP: working on a new idea and feat: implement another feature.

Scenario 4: Advanced Combination for Real-World Scenarios

This is where Revsets become incredibly powerful for daily jj workflows. Combining operators and predicates allows for surgical precision, especially in branchless workflows.

  1. Find all local commits that are not yet part of any public Git remote (useful before jj push): (For this example, assume you’ve jj git pushed main at some point, making its ancestors public(). If not, public() might be empty, and this will show all your commits.)

    jj log -r 'all() - public()' -T 'commit_id description'
    

    This Revset uses the all() pseudo-commit to represent every commit jj knows about, then subtracts (-) any commits that are considered public() (i.e., known to a Git remote). This is your “unpushed local work.”

  2. Find all bugfix commits (fix:) in the current branch’s history (main..@):

    jj log -r 'description("fix:") & (main..@)' -T 'commit_id description'
    

    This command combines two powerful ideas: filtering by description and limiting the search to a specific range. The parentheses around main..@ ensure that the range is evaluated first, then the intersection (&) with the description filter is applied. This should yield fix: resolve feature bug.

  3. Rebase all your commits since main onto the main branch’s current tip: This is a common operation in branchless workflows. You’re saying, “take all my work (main..@) and put it on top of where main is now.”

    jj rebase -s 'main..@' -d main
    

    This command is a prime example of Revsets enabling precise control over mutable history. It tells jj to take the commits starting after main up to and including the working copy (-s 'main..@'), and rebase them onto the main commit itself (-d main). This is how you keep your local stack of changes clean and up-to-date with a shared baseline.

It’s your turn to craft a Revset! This challenge will test your ability to combine multiple concepts.

  • Challenge: Find all commits authored by “Bob” or “Alice” in the history of the main branch (meaning, any commit that is an ancestor of main), but exclude any commits that have “WIP” in their description. List their commit ID and description.

  • Hint: You’ll need author(), ancestors(), description(), | (union), and - (difference) operators. Pay attention to parentheses for grouping!

  • What to observe/learn: This challenge requires you to combine multiple predicates and operators, demonstrating how flexible and powerful Revsets can be for isolating specific sets of changes in a complex history.

💡 Need a little help? Here's a possible solution:
jj log -r '(author("Bob") | author("Alice")) & ancestors(main) - description("WIP")' -T 'commit_id description'

Explanation:

  1. (author("Bob") | author("Alice")): This part selects all commits authored by either Bob or Alice. The parentheses ensure this union is treated as a single set.
  2. & ancestors(main): This intersects the previous set with all commits that are ancestors of the main branch, narrowing the scope to only relevant history that’s part of the main lineage.
  3. - description("WIP"): Finally, this subtracts any commits that have “WIP” in their description from the result, giving you the final, refined set of commits.

Common Pitfalls & Troubleshooting

Mastering Revsets takes practice, and you’ll likely encounter some head-scratching moments. Here are common issues and how to approach them:

  • Syntax Errors: A single misplaced parenthesis, an incorrect operator, or a typo can break your Revset. jj will usually provide a helpful error message pointing to the approximate location of the issue.

    • Solution: Start with smaller, simpler Revsets and gradually build up complexity. If a complex Revset fails, try breaking it down into smaller, testable parts using jj log -r 'partial_revset' to isolate the problem.
  • Misunderstanding all() vs. Current History: all() refers to every commit jj knows about, including hidden or obsolete ones from its operation log. If you only want commits reachable from your current branch or a specific reference, all() might be too broad.

    • Solution: For commits on your current line of work, consider main..@ (if main is your base) or ancestors(@). For commits that are currently visible and active, heads() might be more appropriate.
  • Performance on Large Repositories: Very complex Revsets, especially those involving all() and extensive filtering on large histories, can be slow. This is particularly true if jj has to traverse a vast graph.

    • Solution: For quick checks or when performance is critical, try to use more specific starting points or limit the scope with latest(N, revset). Consider if a simpler Revset can achieve a similar goal.
  • Git Interoperability vs. jj Native: Remember the git_ prefixes (git_ref, git_head, git_remote_branch) when you need to refer to Git-specific references within a jj Revset. jj’s own branches (branches()) and tags (tags()) are distinct from their Git counterparts.

    • Solution: Be explicit about whether you’re targeting a jj concept or a Git concept. For example, main might refer to a jj branch, while git_ref("origin/main") refers to the remote Git branch.
  • Debugging Revsets: The most effective way to debug a Revset is to use jj log -r 'YOUR_REVSET' and observe the output. This allows you to see exactly which commits are being selected.

    • Solution: Build your complex Revset incrementally. Test each component with jj log -r before combining them. If a combination yields unexpected results, simplify it back to its parts.

Summary

Congratulations! You’ve taken a significant step towards truly mastering jj by diving deep into Revsets. This powerful query language is a cornerstone of jj’s flexible and efficient workflow.

Here are the key takeaways:

  • Revsets are jj’s powerful query language for selecting commits based on their relationships, attributes, and content, acting like a database query for your history.
  • Operators like & (intersection), | (union), - (difference), and .. (range) allow you to combine and refine commit sets, defining precise relationships.
  • Predicates such as author(), description(), file(), heads(), and public() enable filtering based on specific criteria, giving you granular control.
  • Precise commit selection through Revsets is fundamental to leveraging jj’s mutable history, enabling advanced operations like targeted rebases and complex refactoring with confidence.
  • Practice is key! Experiment with different combinations in your jj repositories to build intuition and truly understand how jj interprets your queries.

With Revsets in your toolkit, you’re now equipped to navigate and manipulate your jj history with unparalleled control. In the next chapter, we’ll explore Stacked Changes and Branchless Workflows, where the power of Revsets truly shines, enabling you to manage your work in a linear, review-friendly manner without the overhead of traditional branches. Get ready to streamline your development process even further!

References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.