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 theHEADreference (usually the tip of your current branch). Injj, 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.
Navigating the Graph: Parents and Children
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) ofcommit_id. If a commit has multiple parents (a merge commit), this selects all of them.commit_id--: The grandparent(s) ofcommit_id. You can chain this multiple times.commit_id+: The direct child(ren) ofcommit_id.commit_id++: The grandchild(ren) ofcommit_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.
| Operator | Description | Example |
|---|---|---|
& | 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) |
~N | Nth Ancestor: The Nth parent. ~1 is the same as -. | @~3 (the great-grandparent of the working copy) |
^N | Nth 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:
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.
| Predicate | Description | Example |
|---|---|---|
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.
Your current working copy commit (
@):jj log -r @ -T 'commit_id description'This should show your latest commit:
WIP: working on a new idea.The direct parent of your working copy (
@-):jj log -r @- -T 'commit_id description'You’ll see
feat: implement another feature.The grandparent of your working copy (
@--):jj log -r @-- -T 'commit_id description'This reveals
fix: resolve feature bug.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.
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 ideaandfeat: implement another feature.Show all commits between
mainand the working copy (exclusive ofmain) (..for exclusive range):jj log -r 'main..@' -T 'commit_id description'This should show all commits after
feat: initial setupup to and including your current WIP commit.Show all commits from
mainup to the working copy (inclusive ofmain) (..=for inclusive range):jj log -r 'main..=@' -T 'commit_id description'This is similar to the above, but includes the
maincommit itself.Show all ancestors of the
mainbranch (ancestors()predicate):jj log -r 'ancestors(main)' -T 'commit_id description'In our simple linear history, this will show
mainand all commits before it, effectively the entire history up to and including themaincommit.
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.
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>.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, andfeat: implement another feature.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 ideaandfeat: 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.
Find all local commits that are not yet part of any public Git remote (useful before
jj push): (For this example, assume you’vejj git pushedmainat some point, making its ancestorspublic(). 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 commitjjknows about, then subtracts (-) any commits that are consideredpublic()(i.e., known to a Git remote). This is your “unpushed local work.”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 yieldfix: resolve feature bug.Rebase all your commits since
mainonto themainbranch’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 wheremainis now.”jj rebase -s 'main..@' -d mainThis command is a prime example of Revsets enabling precise control over mutable history. It tells
jjto take the commits starting aftermainup to and including the working copy (-s 'main..@'), and rebase them onto themaincommit itself (-d main). This is how you keep your local stack of changes clean and up-to-date with a shared baseline.
Mini-Challenge: Targeted Commit Search
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
mainbranch (meaning, any commit that is an ancestor ofmain), 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:
(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.& ancestors(main): This intersects the previous set with all commits that are ancestors of themainbranch, narrowing the scope to only relevant history that’s part of the main lineage.- 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.
jjwill 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.
- 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
Misunderstanding
all()vs. Current History:all()refers to every commitjjknows 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..@(ifmainis your base) orancestors(@). For commits that are currently visible and active,heads()might be more appropriate.
- Solution: For commits on your current line of work, consider
Performance on Large Repositories: Very complex Revsets, especially those involving
all()and extensive filtering on large histories, can be slow. This is particularly true ifjjhas 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.
- Solution: For quick checks or when performance is critical, try to use more specific starting points or limit the scope with
Git Interoperability vs.
jjNative: Remember thegit_prefixes (git_ref,git_head,git_remote_branch) when you need to refer to Git-specific references within ajjRevset.jj’s own branches (branches()) and tags (tags()) are distinct from their Git counterparts.- Solution: Be explicit about whether you’re targeting a
jjconcept or a Git concept. For example,mainmight refer to ajjbranch, whilegit_ref("origin/main")refers to the remote Git branch.
- Solution: Be explicit about whether you’re targeting a
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 -rbefore combining them. If a combination yields unexpected results, simplify it back to its parts.
- Solution: Build your complex Revset incrementally. Test each component with
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(), andpublic()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
jjrepositories to build intuition and truly understand howjjinterprets 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
- Jujutsu (jj) GitHub Repository: https://github.com/jj-vcs/jj
- Jujutsu Revsets Documentation (main branch): https://github.com/jj-vcs/jj/blob/main/docs/revsets.md
- Jujutsu Tutorial (main branch): https://github.com/martinvonz/jj/blob/main/docs/tutorial.md
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.